Sensibo clean code (#74437)

This commit is contained in:
G Johansson 2022-09-04 21:42:08 +02:00 committed by GitHub
parent b3596fdea1
commit 03d804123a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 425 additions and 319 deletions

View file

@ -1,4 +1,4 @@
"""The sensibo component."""
"""The Sensibo component."""
from __future__ import annotations
from pysensibo.exceptions import AuthenticationError

View file

@ -65,7 +65,6 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionBinarySensorEntityDescription, ...] = (
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
name="Alive",
icon="mdi:wifi",
value_fn=lambda data: data.alive,
),
SensiboMotionBinarySensorEntityDescription(
@ -104,7 +103,6 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
name="Pure Boost linked with AC",
icon="mdi:connection",
value_fn=lambda data: data.pure_ac_integration,
),
SensiboDeviceBinarySensorEntityDescription(
@ -112,7 +110,6 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
name="Pure Boost linked with presence",
icon="mdi:connection",
value_fn=lambda data: data.pure_geo_integration,
),
SensiboDeviceBinarySensorEntityDescription(
@ -120,7 +117,6 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
name="Pure Boost linked with indoor air quality",
icon="mdi:connection",
value_fn=lambda data: data.pure_measure_integration,
),
SensiboDeviceBinarySensorEntityDescription(
@ -128,12 +124,13 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = (
entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
name="Pure Boost linked with outdoor air quality",
icon="mdi:connection",
value_fn=lambda data: data.pure_prime_integration,
),
FILTER_CLEAN_REQUIRED_DESCRIPTION,
)
DESCRIPTION_BY_MODELS = {"pure": PURE_SENSOR_TYPES}
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
@ -161,15 +158,10 @@ async def async_setup_entry(
)
entities.extend(
SensiboDeviceSensor(coordinator, device_id, description)
for description in PURE_SENSOR_TYPES
for device_id, device_data in coordinator.data.parsed.items()
if device_data.model == "pure"
)
entities.extend(
SensiboDeviceSensor(coordinator, device_id, description)
for description in DEVICE_SENSOR_TYPES
for device_id, device_data in coordinator.data.parsed.items()
if device_data.model != "pure"
for description in DESCRIPTION_BY_MODELS.get(
device_data.model, DEVICE_SENSOR_TYPES
)
)
async_add_entities(entities)

View file

@ -1,24 +1,44 @@
"""Button platform for Sensibo integration."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from pysensibo.model import SensiboDevice
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import SensiboDataUpdateCoordinator
from .entity import SensiboDeviceBaseEntity
from .entity import SensiboDeviceBaseEntity, async_handle_api_call
PARALLEL_UPDATES = 0
DEVICE_BUTTON_TYPES: ButtonEntityDescription = ButtonEntityDescription(
@dataclass
class SensiboEntityDescriptionMixin:
"""Mixin values for Sensibo entities."""
data_key: str
@dataclass
class SensiboButtonEntityDescription(
ButtonEntityDescription, SensiboEntityDescriptionMixin
):
"""Class describing Sensibo Button entities."""
DEVICE_BUTTON_TYPES = SensiboButtonEntityDescription(
key="reset_filter",
name="Reset filter",
icon="mdi:air-filter",
entity_category=EntityCategory.CONFIG,
data_key="filter_clean",
)
@ -29,26 +49,22 @@ async def async_setup_entry(
coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[SensiboDeviceButton] = []
entities.extend(
async_add_entities(
SensiboDeviceButton(coordinator, device_id, DEVICE_BUTTON_TYPES)
for device_id, device_data in coordinator.data.parsed.items()
)
async_add_entities(entities)
class SensiboDeviceButton(SensiboDeviceBaseEntity, ButtonEntity):
"""Representation of a Sensibo Device Binary Sensor."""
entity_description: ButtonEntityDescription
entity_description: SensiboButtonEntityDescription
def __init__(
self,
coordinator: SensiboDataUpdateCoordinator,
device_id: str,
entity_description: ButtonEntityDescription,
entity_description: SensiboButtonEntityDescription,
) -> None:
"""Initiate Sensibo Device Button."""
super().__init__(
@ -60,8 +76,18 @@ class SensiboDeviceButton(SensiboDeviceBaseEntity, ButtonEntity):
async def async_press(self) -> None:
"""Press the button."""
result = await self.async_send_command("reset_filter")
if result["status"] == "success":
await self.coordinator.async_request_refresh()
return
raise HomeAssistantError(f"Could not set calibration for device {self.name}")
await self.async_send_api_call(
device_data=self.device_data,
key=self.entity_description.data_key,
value=False,
)
@async_handle_api_call
async def async_send_api_call(
self, device_data: SensiboDevice, key: Any, value: Any
) -> bool:
"""Make service call to api."""
result = await self._client.async_reset_filter(
self._device_id,
)
return bool(result.get("status") == "success")

View file

@ -4,6 +4,7 @@ from __future__ import annotations
from bisect import bisect_left
from typing import TYPE_CHECKING, Any
from pysensibo.model import SensiboDevice
import voluptuous as vol
from homeassistant.components.climate import ClimateEntity
@ -24,7 +25,7 @@ from homeassistant.util.temperature import convert as convert_temperature
from .const import DOMAIN
from .coordinator import SensiboDataUpdateCoordinator
from .entity import SensiboDeviceBaseEntity
from .entity import SensiboDeviceBaseEntity, async_handle_api_call
SERVICE_ASSUME_STATE = "assume_state"
SERVICE_ENABLE_TIMER = "enable_timer"
@ -123,7 +124,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
def __init__(
self, coordinator: SensiboDataUpdateCoordinator, device_id: str
) -> None:
"""Initiate SensiboClimate."""
"""Initiate Sensibo Climate."""
super().__init__(coordinator, device_id)
self._attr_unique_id = device_id
self._attr_temperature_unit = (
@ -173,6 +174,11 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
)
return None
@property
def temperature_unit(self) -> str:
"""Return temperature unit."""
return TEMP_CELSIUS if self.device_data.temp_unit == "C" else TEMP_FAHRENHEIT
@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
@ -242,69 +248,99 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
return
new_temp = _find_valid_target_temp(temperature, self.device_data.temp_list)
await self._async_set_ac_state_property("targetTemperature", new_temp)
await self.async_send_api_call(
device_data=self.device_data,
key=AC_STATE_TO_DATA["targetTemperature"],
value=new_temp,
name="targetTemperature",
assumed_state=False,
)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
if "fanLevel" not in self.device_data.active_features:
raise HomeAssistantError("Current mode doesn't support setting Fanlevel")
await self._async_set_ac_state_property("fanLevel", fan_mode)
await self.async_send_api_call(
device_data=self.device_data,
key=AC_STATE_TO_DATA["fanLevel"],
value=fan_mode,
name="fanLevel",
assumed_state=False,
)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target operation mode."""
if hvac_mode == HVACMode.OFF:
await self._async_set_ac_state_property("on", False)
await self.async_send_api_call(
device_data=self.device_data,
key=AC_STATE_TO_DATA["on"],
value=False,
name="on",
assumed_state=False,
)
return
# Turn on if not currently on.
if not self.device_data.device_on:
await self._async_set_ac_state_property("on", True)
await self.async_send_api_call(
device_data=self.device_data,
key=AC_STATE_TO_DATA["on"],
value=True,
name="on",
assumed_state=False,
)
await self._async_set_ac_state_property("mode", HA_TO_SENSIBO[hvac_mode])
await self.coordinator.async_request_refresh()
await self.async_send_api_call(
device_data=self.device_data,
key=AC_STATE_TO_DATA["mode"],
value=HA_TO_SENSIBO[hvac_mode],
name="mode",
assumed_state=False,
)
async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set new target swing operation."""
if "swing" not in self.device_data.active_features:
raise HomeAssistantError("Current mode doesn't support setting Swing")
await self._async_set_ac_state_property("swing", swing_mode)
await self.async_send_api_call(
device_data=self.device_data,
key=AC_STATE_TO_DATA["swing"],
value=swing_mode,
name="swing",
assumed_state=False,
)
async def async_turn_on(self) -> None:
"""Turn Sensibo unit on."""
await self._async_set_ac_state_property("on", True)
await self.async_send_api_call(
device_data=self.device_data,
key=AC_STATE_TO_DATA["on"],
value=True,
name="on",
assumed_state=False,
)
async def async_turn_off(self) -> None:
"""Turn Sensibo unit on."""
await self._async_set_ac_state_property("on", False)
async def _async_set_ac_state_property(
self, name: str, value: str | int | bool, assumed_state: bool = False
) -> None:
"""Set AC state."""
params = {
"name": name,
"value": value,
"ac_states": self.device_data.ac_states,
"assumed_state": assumed_state,
}
result = await self.async_send_command("set_ac_state", params)
if result["result"]["status"] == "Success":
setattr(self.device_data, AC_STATE_TO_DATA[name], value)
self.async_write_ha_state()
return
failure = result["result"]["failureReason"]
raise HomeAssistantError(
f"Could not set state for device {self.name} due to reason {failure}"
await self.async_send_api_call(
device_data=self.device_data,
key=AC_STATE_TO_DATA["on"],
value=False,
name="on",
assumed_state=False,
)
async def async_assume_state(self, state: str) -> None:
"""Sync state with api."""
await self._async_set_ac_state_property("on", state != HVACMode.OFF, True)
await self.coordinator.async_refresh()
await self.async_send_api_call(
device_data=self.device_data,
key=AC_STATE_TO_DATA["on"],
value=state != HVACMode.OFF,
name="on",
assumed_state=True,
)
async def async_enable_timer(self, minutes: int) -> None:
"""Enable the timer."""
@ -313,11 +349,13 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
"minutesFromNow": minutes,
"acState": {**self.device_data.ac_states, "on": new_state},
}
result = await self.async_send_command("set_timer", params)
if result["status"] == "success":
return await self.coordinator.async_request_refresh()
raise HomeAssistantError(f"Could not enable timer for device {self.name}")
await self.api_call_custom_service_timer(
device_data=self.device_data,
key="timer_on",
value=True,
command="set_timer",
data=params,
)
async def async_enable_pure_boost(
self,
@ -343,5 +381,57 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
if outdoor_integration is not None:
params["primeIntegration"] = outdoor_integration
await self.async_send_command("set_pure_boost", params)
await self.coordinator.async_refresh()
await self.api_call_custom_service_pure_boost(
device_data=self.device_data,
key="pure_boost_enabled",
value=True,
command="set_pure_boost",
data=params,
)
@async_handle_api_call
async def async_send_api_call(
self,
device_data: SensiboDevice,
key: Any,
value: Any,
name: str,
assumed_state: bool = False,
) -> bool:
"""Make service call to api."""
result = await self._client.async_set_ac_state_property(
self._device_id,
name,
value,
self.device_data.ac_states,
assumed_state,
)
return bool(result.get("result", {}).get("status") == "Success")
@async_handle_api_call
async def api_call_custom_service_timer(
self,
device_data: SensiboDevice,
key: Any,
value: Any,
command: str,
data: dict,
) -> bool:
"""Make service call to api."""
result = {}
result = await self._client.async_set_timer(self._device_id, data)
return bool(result.get("status") == "success")
@async_handle_api_call
async def api_call_custom_service_pure_boost(
self,
device_data: SensiboDevice,
key: Any,
value: Any,
command: str,
data: dict,
) -> bool:
"""Make service call to api."""
result = {}
result = await self._client.async_set_pureboost(self._device_id, data)
return bool(result.get("status") == "success")

View file

@ -10,14 +10,14 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.selector import TextSelector
from .const import DEFAULT_NAME, DOMAIN
from .util import NoDevicesError, NoUsernameError, async_validate_api
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_API_KEY): TextSelector(),
}
)

View file

@ -12,10 +12,13 @@ from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, TIMEOUT
REQUEST_REFRESH_DELAY = 0.35
class SensiboDataUpdateCoordinator(DataUpdateCoordinator):
"""A Sensibo Data Update Coordinator."""
@ -34,6 +37,11 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator):
LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
# We don't want an immediate refresh since the device
# takes a moment to reflect the state change
request_refresh_debouncer=Debouncer(
hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
),
)
async def _async_update_data(self) -> SensiboData:

View file

@ -31,6 +31,6 @@ TO_REDACT = {
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
"""Return diagnostics for Sensibo config entry."""
coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
return async_redact_data(coordinator.data.raw, TO_REDACT)

View file

@ -1,10 +1,12 @@
"""Base entity for Sensibo integration."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from collections.abc import Callable, Coroutine
from typing import TYPE_CHECKING, Any, TypeVar
import async_timeout
from pysensibo.model import MotionSensor, SensiboDevice
from typing_extensions import Concatenate, ParamSpec
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
@ -14,9 +16,39 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, LOGGER, SENSIBO_ERRORS, TIMEOUT
from .coordinator import SensiboDataUpdateCoordinator
_T = TypeVar("_T", bound="SensiboDeviceBaseEntity")
_P = ParamSpec("_P")
def async_handle_api_call(
function: Callable[Concatenate[_T, _P], Coroutine[Any, Any, Any]]
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, Any]]:
"""Decorate api calls."""
async def wrap_api_call(*args: Any, **kwargs: Any) -> None:
"""Wrap services for api calls."""
res: bool = False
try:
async with async_timeout.timeout(TIMEOUT):
res = await function(*args, **kwargs)
except SENSIBO_ERRORS as err:
raise HomeAssistantError from err
LOGGER.debug("Result %s for entity %s with arguments %s", res, args[0], kwargs)
entity: SensiboDeviceBaseEntity = args[0]
if res is not True:
raise HomeAssistantError(f"Could not execute service for {entity.name}")
if kwargs.get("key") is not None and kwargs.get("value") is not None:
setattr(entity.device_data, kwargs["key"], kwargs["value"])
LOGGER.debug("Debug check key %s is now %s", kwargs["key"], kwargs["value"])
entity.async_write_ha_state()
await entity.coordinator.async_request_refresh()
return wrap_api_call
class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]):
"""Representation of a Sensibo entity."""
"""Representation of a Sensibo Base Entity."""
def __init__(
self,
@ -35,7 +67,7 @@ class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]):
class SensiboDeviceBaseEntity(SensiboBaseEntity):
"""Representation of a Sensibo device."""
"""Representation of a Sensibo Device."""
_attr_has_entity_name = True
@ -44,7 +76,7 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity):
coordinator: SensiboDataUpdateCoordinator,
device_id: str,
) -> None:
"""Initiate Sensibo Number."""
"""Initiate Sensibo Device."""
super().__init__(coordinator, device_id)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.device_data.id)},
@ -58,63 +90,9 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity):
suggested_area=self.device_data.name,
)
async def async_send_command(
self, command: str, params: dict[str, Any] | None = None
) -> dict[str, Any]:
"""Send command to Sensibo api."""
try:
async with async_timeout.timeout(TIMEOUT):
result = await self.async_send_api_call(command, params)
except SENSIBO_ERRORS as err:
raise HomeAssistantError(
f"Failed to send command {command} for device {self.name} to Sensibo servers: {err}"
) from err
LOGGER.debug("Result: %s", result)
return result
async def async_send_api_call(
self, command: str, params: dict[str, Any] | None = None
) -> dict[str, Any]:
"""Send api call."""
result: dict[str, Any] = {"status": None}
if command == "set_calibration":
if TYPE_CHECKING:
assert params is not None
result = await self._client.async_set_calibration(
self._device_id,
params["data"],
)
if command == "set_ac_state":
if TYPE_CHECKING:
assert params is not None
result = await self._client.async_set_ac_state_property(
self._device_id,
params["name"],
params["value"],
params["ac_states"],
params["assumed_state"],
)
if command == "set_timer":
if TYPE_CHECKING:
assert params is not None
result = await self._client.async_set_timer(self._device_id, params)
if command == "del_timer":
result = await self._client.async_del_timer(self._device_id)
if command == "set_pure_boost":
if TYPE_CHECKING:
assert params is not None
result = await self._client.async_set_pureboost(
self._device_id,
params,
)
if command == "reset_filter":
result = await self._client.async_reset_filter(self._device_id)
return result
class SensiboMotionBaseEntity(SensiboBaseEntity):
"""Representation of a Sensibo motion entity."""
"""Representation of a Sensibo Motion Entity."""
_attr_has_entity_name = True
@ -141,7 +119,7 @@ class SensiboMotionBaseEntity(SensiboBaseEntity):
@property
def sensor_data(self) -> MotionSensor | None:
"""Return data for device."""
"""Return data for Motion Sensor."""
if TYPE_CHECKING:
assert self.device_data.motion_sensors
return self.device_data.motion_sensors[self._sensor_id]

View file

@ -1,18 +1,21 @@
"""Number platform for Sensibo integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from pysensibo.model import SensiboDevice
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import SensiboDataUpdateCoordinator
from .entity import SensiboDeviceBaseEntity
from .entity import SensiboDeviceBaseEntity, async_handle_api_call
PARALLEL_UPDATES = 0
@ -22,6 +25,7 @@ class SensiboEntityDescriptionMixin:
"""Mixin values for Sensibo entities."""
remote_key: str
value_fn: Callable[[SensiboDevice], float | None]
@dataclass
@ -42,6 +46,7 @@ DEVICE_NUMBER_TYPES = (
native_min_value=-10,
native_max_value=10,
native_step=0.1,
value_fn=lambda data: data.calibration_temp,
),
SensiboNumberEntityDescription(
key="calibration_hum",
@ -53,6 +58,7 @@ DEVICE_NUMBER_TYPES = (
native_min_value=-10,
native_max_value=10,
native_step=0.1,
value_fn=lambda data: data.calibration_hum,
),
)
@ -90,15 +96,22 @@ class SensiboNumber(SensiboDeviceBaseEntity, NumberEntity):
@property
def native_value(self) -> float | None:
"""Return the value from coordinator data."""
value: float | None = getattr(self.device_data, self.entity_description.key)
return value
return self.entity_description.value_fn(self.device_data)
async def async_set_native_value(self, value: float) -> None:
"""Set value for calibration."""
await self.async_send_api_call(
device_data=self.device_data, key=self.entity_description.key, value=value
)
@async_handle_api_call
async def async_send_api_call(
self, device_data: SensiboDevice, key: Any, value: Any
) -> bool:
"""Make service call to api."""
data = {self.entity_description.remote_key: value}
result = await self.async_send_command("set_calibration", {"data": data})
if result["status"] == "success":
setattr(self.device_data, self.entity_description.key, value)
self.async_write_ha_state()
return
raise HomeAssistantError(f"Could not set calibration for device {self.name}")
result = await self._client.async_set_calibration(
self._device_id,
data,
)
return bool(result.get("status") == "success")

View file

@ -1,7 +1,11 @@
"""Number platform for Sensibo integration."""
"""Select platform for Sensibo integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from pysensibo.model import SensiboDevice
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
@ -11,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import SensiboDataUpdateCoordinator
from .entity import SensiboDeviceBaseEntity
from .entity import SensiboDeviceBaseEntity, async_handle_api_call
PARALLEL_UPDATES = 0
@ -20,31 +24,34 @@ PARALLEL_UPDATES = 0
class SensiboSelectDescriptionMixin:
"""Mixin values for Sensibo entities."""
remote_key: str
remote_options: str
data_key: str
value_fn: Callable[[SensiboDevice], str | None]
options_fn: Callable[[SensiboDevice], list[str] | None]
@dataclass
class SensiboSelectEntityDescription(
SelectEntityDescription, SensiboSelectDescriptionMixin
):
"""Class describing Sensibo Number entities."""
"""Class describing Sensibo Select entities."""
DEVICE_SELECT_TYPES = (
SensiboSelectEntityDescription(
key="horizontalSwing",
remote_key="horizontal_swing_mode",
remote_options="horizontal_swing_modes",
data_key="horizontal_swing_mode",
name="Horizontal swing",
icon="mdi:air-conditioner",
value_fn=lambda data: data.horizontal_swing_mode,
options_fn=lambda data: data.horizontal_swing_modes,
),
SensiboSelectEntityDescription(
key="light",
remote_key="light_mode",
remote_options="light_modes",
data_key="light_mode",
name="Light",
icon="mdi:flashlight",
value_fn=lambda data: data.light_mode,
options_fn=lambda data: data.light_modes,
),
)
@ -83,15 +90,15 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity):
@property
def current_option(self) -> str | None:
"""Return the current selected option."""
option: str | None = getattr(
self.device_data, self.entity_description.remote_key
)
return option
return self.entity_description.value_fn(self.device_data)
@property
def options(self) -> list[str]:
"""Return possible options."""
return getattr(self.device_data, self.entity_description.remote_options) or []
options = self.entity_description.options_fn(self.device_data)
if TYPE_CHECKING:
assert options is not None
return options
async def async_select_option(self, option: str) -> None:
"""Set state to the selected option."""
@ -100,20 +107,28 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity):
f"Current mode {self.device_data.hvac_mode} doesn't support setting {self.entity_description.name}"
)
params = {
await self.async_send_api_call(
device_data=self.device_data,
key=self.entity_description.data_key,
value=option,
)
@async_handle_api_call
async def async_send_api_call(
self, device_data: SensiboDevice, key: Any, value: Any
) -> bool:
"""Make service call to api."""
data = {
"name": self.entity_description.key,
"value": option,
"value": value,
"ac_states": self.device_data.ac_states,
"assumed_state": False,
}
result = await self.async_send_command("set_ac_state", params)
if result["result"]["status"] == "Success":
setattr(self.device_data, self.entity_description.remote_key, option)
self.async_write_ha_state()
return
failure = result["result"]["failureReason"]
raise HomeAssistantError(
f"Could not set state for device {self.name} due to reason {failure}"
result = await self._client.async_set_ac_state_property(
self._device_id,
data["name"],
data["value"],
data["ac_states"],
data["assumed_state"],
)
return bool(result.get("result", {}).get("status") == "Success")

View file

@ -63,7 +63,7 @@ class SensiboMotionSensorEntityDescription(
class SensiboDeviceSensorEntityDescription(
SensorEntityDescription, DeviceBaseEntityDescriptionMixin
):
"""Describes Sensibo Motion sensor entity."""
"""Describes Sensibo Device sensor entity."""
FILTER_LAST_RESET_DESCRIPTION = SensiboDeviceSensorEntityDescription(
@ -178,6 +178,8 @@ AIRQ_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = (
),
)
DESCRIPTION_BY_MODELS = {"pure": PURE_SENSOR_TYPES, "airq": AIRQ_SENSOR_TYPES}
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
@ -200,20 +202,9 @@ async def async_setup_entry(
entities.extend(
SensiboDeviceSensor(coordinator, device_id, description)
for device_id, device_data in coordinator.data.parsed.items()
for description in PURE_SENSOR_TYPES
if device_data.model == "pure"
)
entities.extend(
SensiboDeviceSensor(coordinator, device_id, description)
for device_id, device_data in coordinator.data.parsed.items()
for description in DEVICE_SENSOR_TYPES
if device_data.model != "pure"
)
entities.extend(
SensiboDeviceSensor(coordinator, device_id, description)
for device_id, device_data in coordinator.data.parsed.items()
for description in AIRQ_SENSOR_TYPES
if device_data.model == "airq"
for description in DESCRIPTION_BY_MODELS.get(
device_data.model, DEVICE_SENSOR_TYPES
)
)
async_add_entities(entities)

View file

@ -15,11 +15,17 @@
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "Follow the documentation to get your api key."
}
},
"reauth_confirm": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "Follow the documentation to get a new api key."
}
}
}

View file

@ -14,25 +14,24 @@ from homeassistant.components.switch import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import SensiboDataUpdateCoordinator
from .entity import SensiboDeviceBaseEntity
from .entity import SensiboDeviceBaseEntity, async_handle_api_call
PARALLEL_UPDATES = 0
@dataclass
class DeviceBaseEntityDescriptionMixin:
"""Mixin for required Sensibo base description keys."""
"""Mixin for required Sensibo Device description keys."""
value_fn: Callable[[SensiboDevice], bool | None]
extra_fn: Callable[[SensiboDevice], dict[str, str | bool | None]] | None
command_on: str
command_off: str
remote_key: str
data_key: str
@dataclass
@ -52,7 +51,7 @@ DEVICE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = (
extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on},
command_on="set_timer",
command_off="del_timer",
remote_key="timer_on",
data_key="timer_on",
),
)
@ -65,56 +64,27 @@ PURE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = (
extra_fn=None,
command_on="set_pure_boost",
command_off="set_pure_boost",
remote_key="pure_boost_enabled",
data_key="pure_boost_enabled",
),
)
def build_params(command: str, device_data: SensiboDevice) -> dict[str, Any] | None:
"""Build params for turning on switch."""
if command == "set_timer":
new_state = bool(device_data.ac_states["on"] is False)
params = {
"minutesFromNow": 60,
"acState": {**device_data.ac_states, "on": new_state},
}
return params
if command == "set_pure_boost":
new_state = bool(device_data.pure_boost_enabled is False)
params = {"enabled": new_state}
if device_data.pure_measure_integration is None:
params["sensitivity"] = "N"
params["measurementsIntegration"] = True
params["acIntegration"] = False
params["geoIntegration"] = False
params["primeIntegration"] = False
return params
return None
DESCRIPTION_BY_MODELS = {"pure": PURE_SWITCH_TYPES}
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up Sensibo binary sensor platform."""
"""Set up Sensibo Switch platform."""
coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[SensiboDeviceSwitch] = []
entities.extend(
async_add_entities(
SensiboDeviceSwitch(coordinator, device_id, description)
for description in DEVICE_SWITCH_TYPES
for device_id, device_data in coordinator.data.parsed.items()
if device_data.model != "pure"
for description in DESCRIPTION_BY_MODELS.get(
device_data.model, DEVICE_SWITCH_TYPES
)
)
entities.extend(
SensiboDeviceSwitch(coordinator, device_id, description)
for description in PURE_SWITCH_TYPES
for device_id, device_data in coordinator.data.parsed.items()
if device_data.model == "pure"
)
async_add_entities(entities)
class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity):
@ -143,33 +113,33 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
params = build_params(self.entity_description.command_on, self.device_data)
result = await self.async_send_command(
self.entity_description.command_on, params
)
if result["status"] == "success":
setattr(self.device_data, self.entity_description.remote_key, True)
self.async_write_ha_state()
return await self.coordinator.async_request_refresh()
raise HomeAssistantError(
f"Could not execute {self.entity_description.command_on} for device {self.name}"
)
if self.entity_description.key == "timer_on_switch":
await self.async_turn_on_timer(
device_data=self.device_data,
key=self.entity_description.data_key,
value=True,
)
if self.entity_description.key == "pure_boost_switch":
await self.async_turn_on_off_pure_boost(
device_data=self.device_data,
key=self.entity_description.data_key,
value=True,
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
params = build_params(self.entity_description.command_on, self.device_data)
result = await self.async_send_command(
self.entity_description.command_off, params
)
if result["status"] == "success":
setattr(self.device_data, self.entity_description.remote_key, False)
self.async_write_ha_state()
return await self.coordinator.async_request_refresh()
raise HomeAssistantError(
f"Could not execute {self.entity_description.command_off} for device {self.name}"
)
if self.entity_description.key == "timer_on_switch":
await self.async_turn_off_timer(
device_data=self.device_data,
key=self.entity_description.data_key,
value=False,
)
if self.entity_description.key == "pure_boost_switch":
await self.async_turn_on_off_pure_boost(
device_data=self.device_data,
key=self.entity_description.data_key,
value=False,
)
@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
@ -177,3 +147,43 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity):
if self.entity_description.extra_fn:
return self.entity_description.extra_fn(self.device_data)
return None
@async_handle_api_call
async def async_turn_on_timer(
self, device_data: SensiboDevice, key: Any, value: Any
) -> bool:
"""Make service call to api for setting timer."""
result = {}
new_state = bool(device_data.ac_states["on"] is False)
data = {
"minutesFromNow": 60,
"acState": {**device_data.ac_states, "on": new_state},
}
result = await self._client.async_set_timer(self._device_id, data)
return bool(result.get("status") == "success")
@async_handle_api_call
async def async_turn_off_timer(
self, device_data: SensiboDevice, key: Any, value: Any
) -> bool:
"""Make service call to api for deleting timer."""
result = {}
result = await self._client.async_del_timer(self._device_id)
return bool(result.get("status") == "success")
@async_handle_api_call
async def async_turn_on_off_pure_boost(
self, device_data: SensiboDevice, key: Any, value: Any
) -> bool:
"""Make service call to api for setting Pure Boost."""
result = {}
new_state = bool(device_data.pure_boost_enabled is False)
data: dict[str, Any] = {"enabled": new_state}
if device_data.pure_measure_integration is None:
data["sensitivity"] = "N"
data["measurementsIntegration"] = True
data["acIntegration"] = False
data["geoIntegration"] = False
data["primeIntegration"] = False
result = await self._client.async_set_pureboost(self._device_id, data)
return bool(result.get("status") == "success")

View file

@ -15,11 +15,17 @@
"reauth_confirm": {
"data": {
"api_key": "API Key"
},
"data_description": {
"api_key": "Follow the documentation to get a new api key."
}
},
"user": {
"data": {
"api_key": "API Key"
},
"data_description": {
"api_key": "Follow the documentation to get your api key."
}
}
}

View file

@ -12,7 +12,7 @@ from .const import LOGGER, SENSIBO_ERRORS, TIMEOUT
async def async_validate_api(hass: HomeAssistant, api_key: str) -> str:
"""Get data from API."""
"""Validate the api and return username."""
client = SensiboClient(
api_key,
session=async_get_clientsession(hass),

View file

@ -36,7 +36,9 @@ async def test_button(
assert state_filter_clean.state is STATE_ON
assert state_filter_last_reset.state == "2022-03-12T15:24:26+00:00"
freezer.move_to(datetime(2022, 6, 19, 20, 0, 0))
today = datetime(datetime.now().year + 1, 6, 19, 20, 0, 0).replace(tzinfo=dt.UTC)
today_str = today.isoformat()
freezer.move_to(today)
with patch(
"homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data",
@ -53,13 +55,13 @@ async def test_button(
},
blocking=True,
)
await hass.async_block_till_done()
await hass.async_block_till_done()
monkeypatch.setattr(get_data.parsed["ABC999111"], "filter_clean", False)
monkeypatch.setattr(
get_data.parsed["ABC999111"],
"filter_last_reset",
datetime(2022, 6, 19, 20, 0, 0, tzinfo=dt.UTC),
today,
)
with patch(
@ -75,11 +77,9 @@ async def test_button(
state_button = hass.states.get("button.hallway_reset_filter")
state_filter_clean = hass.states.get("binary_sensor.hallway_filter_clean_required")
state_filter_last_reset = hass.states.get("sensor.hallway_filter_last_reset")
assert (
state_button.state == datetime(2022, 6, 19, 20, 0, 0, tzinfo=dt.UTC).isoformat()
)
assert state_button.state == today_str
assert state_filter_clean.state is STATE_OFF
assert state_filter_last_reset.state == "2022-06-19T20:00:00+00:00"
assert state_filter_last_reset.state == today_str
async def test_button_failure(

View file

@ -115,6 +115,9 @@ async def test_climate_fan(
assert state1.attributes["fan_mode"] == "high"
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property",
return_value={"result": {"status": "Success"}},
):
@ -180,6 +183,9 @@ async def test_climate_swing(
assert state1.attributes["swing_mode"] == "stopped"
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property",
return_value={"result": {"status": "Success"}},
):
@ -189,7 +195,7 @@ async def test_climate_swing(
{ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "fixedTop"},
blocking=True,
)
await hass.async_block_till_done()
await hass.async_block_till_done()
state2 = hass.states.get("climate.hallway")
assert state2.attributes["swing_mode"] == "fixedTop"
@ -244,6 +250,9 @@ async def test_climate_temperatures(
assert state1.attributes["temperature"] == 25
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property",
return_value={"result": {"status": "Success"}},
):
@ -259,6 +268,9 @@ async def test_climate_temperatures(
assert state2.attributes["temperature"] == 20
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property",
return_value={"result": {"status": "Success"}},
):
@ -274,6 +286,9 @@ async def test_climate_temperatures(
assert state2.attributes["temperature"] == 16
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property",
return_value={"result": {"status": "Success"}},
):
@ -289,6 +304,9 @@ async def test_climate_temperatures(
assert state2.attributes["temperature"] == 19
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property",
return_value={"result": {"status": "Success"}},
):
@ -304,6 +322,9 @@ async def test_climate_temperatures(
assert state2.attributes["temperature"] == 20
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property",
return_value={"result": {"status": "Success"}},
):
@ -481,7 +502,7 @@ async def test_climate_hvac_mode(
{ATTR_ENTITY_ID: state1.entity_id, ATTR_HVAC_MODE: "off"},
blocking=True,
)
await hass.async_block_till_done()
await hass.async_block_till_done()
state2 = hass.states.get("climate.hallway")
assert state2.state == "off"
@ -540,6 +561,9 @@ async def test_climate_on_off(
assert state1.state == "heat"
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property",
return_value={"result": {"status": "Success"}},
):
@ -549,12 +573,15 @@ async def test_climate_on_off(
{ATTR_ENTITY_ID: state1.entity_id},
blocking=True,
)
await hass.async_block_till_done()
await hass.async_block_till_done()
state2 = hass.states.get("climate.hallway")
assert state2.state == "off"
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
return_value=get_data,
), patch(
"homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property",
return_value={"result": {"status": "Success"}},
):

View file

@ -1,7 +1,7 @@
"""The test for the sensibo entity."""
from __future__ import annotations
from unittest.mock import AsyncMock, patch
from unittest.mock import patch
from pysensibo.model import SensiboData
import pytest
@ -11,11 +11,6 @@ from homeassistant.components.climate.const import (
DOMAIN as CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
)
from homeassistant.components.number.const import (
ATTR_VALUE,
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.components.sensibo.const import SENSIBO_ERRORS
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID
@ -51,7 +46,7 @@ async def test_entity(
@pytest.mark.parametrize("p_error", SENSIBO_ERRORS)
async def test_entity_send_command(
async def test_entity_failed_service_calls(
hass: HomeAssistant,
p_error: Exception,
load_int: ConfigEntry,
@ -91,29 +86,3 @@ async def test_entity_send_command(
state = hass.states.get("climate.hallway")
assert state.attributes["fan_mode"] == "low"
async def test_entity_send_command_calibration(
hass: HomeAssistant,
entity_registry_enabled_by_default: AsyncMock,
load_int: ConfigEntry,
get_data: SensiboData,
) -> None:
"""Test the Sensibo send command for calibration."""
state = hass.states.get("number.hallway_temperature_calibration")
assert state.state == "0.1"
with patch(
"homeassistant.components.sensibo.util.SensiboClient.async_set_calibration",
return_value={"status": "success"},
):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: 0.2},
blocking=True,
)
state = hass.states.get("number.hallway_temperature_calibration")
assert state.state == "0.2"

View file

@ -8,7 +8,6 @@ from pysensibo.model import SensiboData
import pytest
from pytest import MonkeyPatch
from homeassistant.components.sensibo.switch import build_params
from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@ -134,6 +133,7 @@ async def test_switch_pure_boost(
await hass.async_block_till_done()
monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_boost_enabled", True)
monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_measure_integration", None)
with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
@ -223,28 +223,3 @@ async def test_switch_command_failure(
},
blocking=True,
)
async def test_build_params(
hass: HomeAssistant,
load_int: ConfigEntry,
monkeypatch: MonkeyPatch,
get_data: SensiboData,
) -> None:
"""Test the build params method."""
assert build_params("set_timer", get_data.parsed["ABC999111"]) == {
"minutesFromNow": 60,
"acState": {**get_data.parsed["ABC999111"].ac_states, "on": False},
}
monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_measure_integration", None)
assert build_params("set_pure_boost", get_data.parsed["AAZZAAZZ"]) == {
"enabled": True,
"sensitivity": "N",
"measurementsIntegration": True,
"acIntegration": False,
"geoIntegration": False,
"primeIntegration": False,
}
assert build_params("incorrect_command", get_data.parsed["ABC999111"]) is None