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 __future__ import annotations
from pysensibo.exceptions import AuthenticationError from pysensibo.exceptions import AuthenticationError

View File

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

View File

@ -1,24 +1,44 @@
"""Button platform for Sensibo integration.""" """Button platform for Sensibo integration."""
from __future__ import annotations 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.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN
from .coordinator import SensiboDataUpdateCoordinator from .coordinator import SensiboDataUpdateCoordinator
from .entity import SensiboDeviceBaseEntity from .entity import SensiboDeviceBaseEntity, async_handle_api_call
PARALLEL_UPDATES = 0 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", key="reset_filter",
name="Reset filter", name="Reset filter",
icon="mdi:air-filter", icon="mdi:air-filter",
entity_category=EntityCategory.CONFIG, 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] coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[SensiboDeviceButton] = [] async_add_entities(
entities.extend(
SensiboDeviceButton(coordinator, device_id, DEVICE_BUTTON_TYPES) SensiboDeviceButton(coordinator, device_id, DEVICE_BUTTON_TYPES)
for device_id, device_data in coordinator.data.parsed.items() for device_id, device_data in coordinator.data.parsed.items()
) )
async_add_entities(entities)
class SensiboDeviceButton(SensiboDeviceBaseEntity, ButtonEntity): class SensiboDeviceButton(SensiboDeviceBaseEntity, ButtonEntity):
"""Representation of a Sensibo Device Binary Sensor.""" """Representation of a Sensibo Device Binary Sensor."""
entity_description: ButtonEntityDescription entity_description: SensiboButtonEntityDescription
def __init__( def __init__(
self, self,
coordinator: SensiboDataUpdateCoordinator, coordinator: SensiboDataUpdateCoordinator,
device_id: str, device_id: str,
entity_description: ButtonEntityDescription, entity_description: SensiboButtonEntityDescription,
) -> None: ) -> None:
"""Initiate Sensibo Device Button.""" """Initiate Sensibo Device Button."""
super().__init__( super().__init__(
@ -60,8 +76,18 @@ class SensiboDeviceButton(SensiboDeviceBaseEntity, ButtonEntity):
async def async_press(self) -> None: async def async_press(self) -> None:
"""Press the button.""" """Press the button."""
result = await self.async_send_command("reset_filter") await self.async_send_api_call(
if result["status"] == "success": device_data=self.device_data,
await self.coordinator.async_request_refresh() key=self.entity_description.data_key,
return value=False,
raise HomeAssistantError(f"Could not set calibration for device {self.name}") )
@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 bisect import bisect_left
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from pysensibo.model import SensiboDevice
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
@ -24,7 +25,7 @@ from homeassistant.util.temperature import convert as convert_temperature
from .const import DOMAIN from .const import DOMAIN
from .coordinator import SensiboDataUpdateCoordinator from .coordinator import SensiboDataUpdateCoordinator
from .entity import SensiboDeviceBaseEntity from .entity import SensiboDeviceBaseEntity, async_handle_api_call
SERVICE_ASSUME_STATE = "assume_state" SERVICE_ASSUME_STATE = "assume_state"
SERVICE_ENABLE_TIMER = "enable_timer" SERVICE_ENABLE_TIMER = "enable_timer"
@ -123,7 +124,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
def __init__( def __init__(
self, coordinator: SensiboDataUpdateCoordinator, device_id: str self, coordinator: SensiboDataUpdateCoordinator, device_id: str
) -> None: ) -> None:
"""Initiate SensiboClimate.""" """Initiate Sensibo Climate."""
super().__init__(coordinator, device_id) super().__init__(coordinator, device_id)
self._attr_unique_id = device_id self._attr_unique_id = device_id
self._attr_temperature_unit = ( self._attr_temperature_unit = (
@ -173,6 +174,11 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
) )
return None 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 @property
def target_temperature(self) -> float | None: def target_temperature(self) -> float | None:
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
@ -242,69 +248,99 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
return return
new_temp = _find_valid_target_temp(temperature, self.device_data.temp_list) 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: async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode.""" """Set new target fan mode."""
if "fanLevel" not in self.device_data.active_features: if "fanLevel" not in self.device_data.active_features:
raise HomeAssistantError("Current mode doesn't support setting Fanlevel") 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: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target operation mode.""" """Set new target operation mode."""
if hvac_mode == HVACMode.OFF: 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 return
# Turn on if not currently on. # Turn on if not currently on.
if not self.device_data.device_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.async_send_api_call(
await self.coordinator.async_request_refresh() 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: async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set new target swing operation.""" """Set new target swing operation."""
if "swing" not in self.device_data.active_features: if "swing" not in self.device_data.active_features:
raise HomeAssistantError("Current mode doesn't support setting Swing") 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: async def async_turn_on(self) -> None:
"""Turn Sensibo unit on.""" """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: async def async_turn_off(self) -> None:
"""Turn Sensibo unit on.""" """Turn Sensibo unit on."""
await self._async_set_ac_state_property("on", False) await self.async_send_api_call(
device_data=self.device_data,
async def _async_set_ac_state_property( key=AC_STATE_TO_DATA["on"],
self, name: str, value: str | int | bool, assumed_state: bool = False value=False,
) -> None: name="on",
"""Set AC state.""" assumed_state=False,
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}"
) )
async def async_assume_state(self, state: str) -> None: async def async_assume_state(self, state: str) -> None:
"""Sync state with api.""" """Sync state with api."""
await self._async_set_ac_state_property("on", state != HVACMode.OFF, True) await self.async_send_api_call(
await self.coordinator.async_refresh() 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: async def async_enable_timer(self, minutes: int) -> None:
"""Enable the timer.""" """Enable the timer."""
@ -313,11 +349,13 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
"minutesFromNow": minutes, "minutesFromNow": minutes,
"acState": {**self.device_data.ac_states, "on": new_state}, "acState": {**self.device_data.ac_states, "on": new_state},
} }
result = await self.async_send_command("set_timer", params) await self.api_call_custom_service_timer(
device_data=self.device_data,
if result["status"] == "success": key="timer_on",
return await self.coordinator.async_request_refresh() value=True,
raise HomeAssistantError(f"Could not enable timer for device {self.name}") command="set_timer",
data=params,
)
async def async_enable_pure_boost( async def async_enable_pure_boost(
self, self,
@ -343,5 +381,57 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
if outdoor_integration is not None: if outdoor_integration is not None:
params["primeIntegration"] = outdoor_integration params["primeIntegration"] = outdoor_integration
await self.async_send_command("set_pure_boost", params) await self.api_call_custom_service_pure_boost(
await self.coordinator.async_refresh() 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 import config_entries
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY
from homeassistant.data_entry_flow import FlowResult 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 .const import DEFAULT_NAME, DOMAIN
from .util import NoDevicesError, NoUsernameError, async_validate_api from .util import NoDevicesError, NoUsernameError, async_validate_api
DATA_SCHEMA = vol.Schema( 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.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, TIMEOUT from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, TIMEOUT
REQUEST_REFRESH_DELAY = 0.35
class SensiboDataUpdateCoordinator(DataUpdateCoordinator): class SensiboDataUpdateCoordinator(DataUpdateCoordinator):
"""A Sensibo Data Update Coordinator.""" """A Sensibo Data Update Coordinator."""
@ -34,6 +37,11 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator):
LOGGER, LOGGER,
name=DOMAIN, name=DOMAIN,
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), 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: async def _async_update_data(self) -> SensiboData:

View File

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

View File

@ -1,10 +1,12 @@
"""Base entity for Sensibo integration.""" """Base entity for Sensibo integration."""
from __future__ import annotations 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 import async_timeout
from pysensibo.model import MotionSensor, SensiboDevice from pysensibo.model import MotionSensor, SensiboDevice
from typing_extensions import Concatenate, ParamSpec
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC 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 .const import DOMAIN, LOGGER, SENSIBO_ERRORS, TIMEOUT
from .coordinator import SensiboDataUpdateCoordinator 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]): class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]):
"""Representation of a Sensibo entity.""" """Representation of a Sensibo Base Entity."""
def __init__( def __init__(
self, self,
@ -35,7 +67,7 @@ class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]):
class SensiboDeviceBaseEntity(SensiboBaseEntity): class SensiboDeviceBaseEntity(SensiboBaseEntity):
"""Representation of a Sensibo device.""" """Representation of a Sensibo Device."""
_attr_has_entity_name = True _attr_has_entity_name = True
@ -44,7 +76,7 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity):
coordinator: SensiboDataUpdateCoordinator, coordinator: SensiboDataUpdateCoordinator,
device_id: str, device_id: str,
) -> None: ) -> None:
"""Initiate Sensibo Number.""" """Initiate Sensibo Device."""
super().__init__(coordinator, device_id) super().__init__(coordinator, device_id)
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.device_data.id)}, identifiers={(DOMAIN, self.device_data.id)},
@ -58,63 +90,9 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity):
suggested_area=self.device_data.name, 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): class SensiboMotionBaseEntity(SensiboBaseEntity):
"""Representation of a Sensibo motion entity.""" """Representation of a Sensibo Motion Entity."""
_attr_has_entity_name = True _attr_has_entity_name = True
@ -141,7 +119,7 @@ class SensiboMotionBaseEntity(SensiboBaseEntity):
@property @property
def sensor_data(self) -> MotionSensor | None: def sensor_data(self) -> MotionSensor | None:
"""Return data for device.""" """Return data for Motion Sensor."""
if TYPE_CHECKING: if TYPE_CHECKING:
assert self.device_data.motion_sensors assert self.device_data.motion_sensors
return self.device_data.motion_sensors[self._sensor_id] return self.device_data.motion_sensors[self._sensor_id]

View File

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

View File

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

View File

@ -63,7 +63,7 @@ class SensiboMotionSensorEntityDescription(
class SensiboDeviceSensorEntityDescription( class SensiboDeviceSensorEntityDescription(
SensorEntityDescription, DeviceBaseEntityDescriptionMixin SensorEntityDescription, DeviceBaseEntityDescriptionMixin
): ):
"""Describes Sensibo Motion sensor entity.""" """Describes Sensibo Device sensor entity."""
FILTER_LAST_RESET_DESCRIPTION = SensiboDeviceSensorEntityDescription( 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( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
@ -200,20 +202,9 @@ async def async_setup_entry(
entities.extend( entities.extend(
SensiboDeviceSensor(coordinator, device_id, description) SensiboDeviceSensor(coordinator, device_id, description)
for device_id, device_data in coordinator.data.parsed.items() for device_id, device_data in coordinator.data.parsed.items()
for description in PURE_SENSOR_TYPES for description in DESCRIPTION_BY_MODELS.get(
if device_data.model == "pure" device_data.model, DEVICE_SENSOR_TYPES
) )
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"
) )
async_add_entities(entities) async_add_entities(entities)

View File

@ -15,11 +15,17 @@
"user": { "user": {
"data": { "data": {
"api_key": "[%key:common::config_flow::data::api_key%]" "api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "Follow the documentation to get your api key."
} }
}, },
"reauth_confirm": { "reauth_confirm": {
"data": { "data": {
"api_key": "[%key:common::config_flow::data::api_key%]" "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.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN
from .coordinator import SensiboDataUpdateCoordinator from .coordinator import SensiboDataUpdateCoordinator
from .entity import SensiboDeviceBaseEntity from .entity import SensiboDeviceBaseEntity, async_handle_api_call
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@dataclass @dataclass
class DeviceBaseEntityDescriptionMixin: class DeviceBaseEntityDescriptionMixin:
"""Mixin for required Sensibo base description keys.""" """Mixin for required Sensibo Device description keys."""
value_fn: Callable[[SensiboDevice], bool | None] value_fn: Callable[[SensiboDevice], bool | None]
extra_fn: Callable[[SensiboDevice], dict[str, str | bool | None]] | None extra_fn: Callable[[SensiboDevice], dict[str, str | bool | None]] | None
command_on: str command_on: str
command_off: str command_off: str
remote_key: str data_key: str
@dataclass @dataclass
@ -52,7 +51,7 @@ DEVICE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = (
extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on},
command_on="set_timer", command_on="set_timer",
command_off="del_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, extra_fn=None,
command_on="set_pure_boost", command_on="set_pure_boost",
command_off="set_pure_boost", command_off="set_pure_boost",
remote_key="pure_boost_enabled", data_key="pure_boost_enabled",
), ),
) )
DESCRIPTION_BY_MODELS = {"pure": PURE_SWITCH_TYPES}
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
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up Sensibo binary sensor platform.""" """Set up Sensibo Switch platform."""
coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[SensiboDeviceSwitch] = [] async_add_entities(
entities.extend(
SensiboDeviceSwitch(coordinator, device_id, description) SensiboDeviceSwitch(coordinator, device_id, description)
for description in DEVICE_SWITCH_TYPES
for device_id, device_data in coordinator.data.parsed.items() 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): class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity):
@ -143,33 +113,33 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on.""" """Turn the entity on."""
params = build_params(self.entity_description.command_on, self.device_data) if self.entity_description.key == "timer_on_switch":
result = await self.async_send_command( await self.async_turn_on_timer(
self.entity_description.command_on, params device_data=self.device_data,
) key=self.entity_description.data_key,
value=True,
if result["status"] == "success": )
setattr(self.device_data, self.entity_description.remote_key, True) if self.entity_description.key == "pure_boost_switch":
self.async_write_ha_state() await self.async_turn_on_off_pure_boost(
return await self.coordinator.async_request_refresh() device_data=self.device_data,
raise HomeAssistantError( key=self.entity_description.data_key,
f"Could not execute {self.entity_description.command_on} for device {self.name}" value=True,
) )
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off.""" """Turn the entity off."""
params = build_params(self.entity_description.command_on, self.device_data) if self.entity_description.key == "timer_on_switch":
result = await self.async_send_command( await self.async_turn_off_timer(
self.entity_description.command_off, params device_data=self.device_data,
) key=self.entity_description.data_key,
value=False,
if result["status"] == "success": )
setattr(self.device_data, self.entity_description.remote_key, False) if self.entity_description.key == "pure_boost_switch":
self.async_write_ha_state() await self.async_turn_on_off_pure_boost(
return await self.coordinator.async_request_refresh() device_data=self.device_data,
raise HomeAssistantError( key=self.entity_description.data_key,
f"Could not execute {self.entity_description.command_off} for device {self.name}" value=False,
) )
@property @property
def extra_state_attributes(self) -> Mapping[str, Any] | None: def extra_state_attributes(self) -> Mapping[str, Any] | None:
@ -177,3 +147,43 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity):
if self.entity_description.extra_fn: if self.entity_description.extra_fn:
return self.entity_description.extra_fn(self.device_data) return self.entity_description.extra_fn(self.device_data)
return None 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": { "reauth_confirm": {
"data": { "data": {
"api_key": "API Key" "api_key": "API Key"
},
"data_description": {
"api_key": "Follow the documentation to get a new api key."
} }
}, },
"user": { "user": {
"data": { "data": {
"api_key": "API Key" "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: async def async_validate_api(hass: HomeAssistant, api_key: str) -> str:
"""Get data from API.""" """Validate the api and return username."""
client = SensiboClient( client = SensiboClient(
api_key, api_key,
session=async_get_clientsession(hass), 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_clean.state is STATE_ON
assert state_filter_last_reset.state == "2022-03-12T15:24:26+00:00" 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( with patch(
"homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data",
@ -53,13 +55,13 @@ async def test_button(
}, },
blocking=True, 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_clean", False)
monkeypatch.setattr( monkeypatch.setattr(
get_data.parsed["ABC999111"], get_data.parsed["ABC999111"],
"filter_last_reset", "filter_last_reset",
datetime(2022, 6, 19, 20, 0, 0, tzinfo=dt.UTC), today,
) )
with patch( with patch(
@ -75,11 +77,9 @@ async def test_button(
state_button = hass.states.get("button.hallway_reset_filter") state_button = hass.states.get("button.hallway_reset_filter")
state_filter_clean = hass.states.get("binary_sensor.hallway_filter_clean_required") state_filter_clean = hass.states.get("binary_sensor.hallway_filter_clean_required")
state_filter_last_reset = hass.states.get("sensor.hallway_filter_last_reset") state_filter_last_reset = hass.states.get("sensor.hallway_filter_last_reset")
assert ( assert state_button.state == today_str
state_button.state == datetime(2022, 6, 19, 20, 0, 0, tzinfo=dt.UTC).isoformat()
)
assert state_filter_clean.state is STATE_OFF 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( async def test_button_failure(

View File

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

View File

@ -1,7 +1,7 @@
"""The test for the sensibo entity.""" """The test for the sensibo entity."""
from __future__ import annotations from __future__ import annotations
from unittest.mock import AsyncMock, patch from unittest.mock import patch
from pysensibo.model import SensiboData from pysensibo.model import SensiboData
import pytest import pytest
@ -11,11 +11,6 @@ from homeassistant.components.climate.const import (
DOMAIN as CLIMATE_DOMAIN, DOMAIN as CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE, 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.components.sensibo.const import SENSIBO_ERRORS
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
@ -51,7 +46,7 @@ async def test_entity(
@pytest.mark.parametrize("p_error", SENSIBO_ERRORS) @pytest.mark.parametrize("p_error", SENSIBO_ERRORS)
async def test_entity_send_command( async def test_entity_failed_service_calls(
hass: HomeAssistant, hass: HomeAssistant,
p_error: Exception, p_error: Exception,
load_int: ConfigEntry, load_int: ConfigEntry,
@ -91,29 +86,3 @@ async def test_entity_send_command(
state = hass.states.get("climate.hallway") state = hass.states.get("climate.hallway")
assert state.attributes["fan_mode"] == "low" 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 import pytest
from pytest import MonkeyPatch from pytest import MonkeyPatch
from homeassistant.components.sensibo.switch import build_params
from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@ -134,6 +133,7 @@ async def test_switch_pure_boost(
await hass.async_block_till_done() await hass.async_block_till_done()
monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_boost_enabled", True) monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_boost_enabled", True)
monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_measure_integration", None)
with patch( with patch(
"homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data",
@ -223,28 +223,3 @@ async def test_switch_command_failure(
}, },
blocking=True, 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