diff --git a/.coveragerc b/.coveragerc index 654e1a3e4d7..7bee7669e16 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1038,6 +1038,7 @@ omit = homeassistant/components/sensibo/climate.py homeassistant/components/sensibo/coordinator.py homeassistant/components/sensibo/diagnostics.py + homeassistant/components/sensibo/number.py homeassistant/components/serial/sensor.py homeassistant/components/serial_pm/sensor.py homeassistant/components/sesame/lock.py diff --git a/homeassistant/components/sensibo/const.py b/homeassistant/components/sensibo/const.py index e6d5bebff42..683a403cb08 100644 --- a/homeassistant/components/sensibo/const.py +++ b/homeassistant/components/sensibo/const.py @@ -1,14 +1,25 @@ """Constants for Sensibo.""" +import asyncio import logging +from aiohttp.client_exceptions import ClientConnectionError +from pysensibo.exceptions import AuthenticationError, SensiboError + from homeassistant.const import Platform LOGGER = logging.getLogger(__package__) DEFAULT_SCAN_INTERVAL = 60 DOMAIN = "sensibo" -PLATFORMS = [Platform.CLIMATE] +PLATFORMS = [Platform.CLIMATE, Platform.NUMBER] ALL = ["all"] DEFAULT_NAME = "Sensibo" TIMEOUT = 8 + +SENSIBO_ERRORS = ( + ClientConnectionError, + asyncio.TimeoutError, + AuthenticationError, + SensiboError, +) diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py new file mode 100644 index 00000000000..eff953592ac --- /dev/null +++ b/homeassistant/components/sensibo/number.py @@ -0,0 +1,130 @@ +"""Number platform for Sensibo integration.""" +from __future__ import annotations + +from dataclasses import dataclass + +import async_timeout + +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 DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, LOGGER, SENSIBO_ERRORS, TIMEOUT +from .coordinator import SensiboDataUpdateCoordinator + + +@dataclass +class SensiboEntityDescriptionMixin: + """Mixin values for Sensibo entities.""" + + remote_key: str + + +@dataclass +class SensiboNumberEntityDescription( + NumberEntityDescription, SensiboEntityDescriptionMixin +): + """Class describing Sensibo Number entities.""" + + +NUMBER_TYPES = ( + SensiboNumberEntityDescription( + key="calibration_temp", + remote_key="temperature", + name="Temperature calibration", + icon="mdi:thermometer", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + min_value=-10, + max_value=10, + step=0.1, + ), + SensiboNumberEntityDescription( + key="calibration_hum", + remote_key="humidity", + name="Humidity calibration", + icon="mdi:water", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + min_value=-10, + max_value=10, + step=0.1, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Sensibo number platform.""" + + coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + SensiboNumber(coordinator, device_id, description) + for device_id, device_data in coordinator.data.items() + for description in NUMBER_TYPES + if device_data["hvac_modes"] and device_data["temp"] + ) + + +class SensiboNumber(CoordinatorEntity, NumberEntity): + """Representation of a Sensibo numbers.""" + + coordinator: SensiboDataUpdateCoordinator + entity_description: SensiboNumberEntityDescription + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + entity_description: SensiboNumberEntityDescription, + ) -> None: + """Initiate Sensibo Number.""" + super().__init__(coordinator) + self.entity_description = entity_description + self._device_id = device_id + self._client = coordinator.client + self._attr_unique_id = f"{device_id}-{entity_description.key}" + self._attr_name = ( + f"{coordinator.data[device_id]['name']} {entity_description.name}" + ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.data[device_id]["id"])}, + name=coordinator.data[device_id]["name"], + manufacturer="Sensibo", + configuration_url="https://home.sensibo.com/", + model=coordinator.data[device_id]["model"], + sw_version=coordinator.data[device_id]["fw_ver"], + hw_version=coordinator.data[device_id]["fw_type"], + suggested_area=coordinator.data[device_id]["name"], + ) + + @property + def value(self) -> float: + """Return the value from coordinator data.""" + return self.coordinator.data[self._device_id][self.entity_description.key] + + async def async_set_value(self, value: float) -> None: + """Set value for calibration.""" + data = {self.entity_description.remote_key: value} + try: + async with async_timeout.timeout(TIMEOUT): + result = await self._client.async_set_calibration( + self._device_id, + data, + ) + except SENSIBO_ERRORS as err: + raise HomeAssistantError( + f"Failed to set calibration for device {self.name} to Sensibo servers: {err}" + ) from err + LOGGER.debug("Result: %s", result) + if result["status"] == "success": + self.coordinator.data[self._device_id][self.entity_description.key] = value + self.async_write_ha_state() + return + raise HomeAssistantError(f"Could not set calibration for device {self.name}")