From fb8eeac563d79e79182d84362917b177009f35d6 Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Sun, 7 Jul 2024 17:15:38 +0200 Subject: [PATCH] Refactor Tado to use runtime_data (#121373) --- homeassistant/components/tado/__init__.py | 344 ++---------------- .../components/tado/binary_sensor.py | 16 +- homeassistant/components/tado/climate.py | 8 +- .../components/tado/device_tracker.py | 10 +- homeassistant/components/tado/entity.py | 2 +- homeassistant/components/tado/helper.py | 2 +- homeassistant/components/tado/sensor.py | 10 +- homeassistant/components/tado/services.py | 11 +- .../components/tado/tado_connector.py | 332 +++++++++++++++++ homeassistant/components/tado/water_heater.py | 14 +- 10 files changed, 393 insertions(+), 356 deletions(-) create mode 100644 homeassistant/components/tado/tado_connector.py diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index be58c68be91..2c853a0e6e3 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -1,22 +1,19 @@ """Support for the (unofficial) Tado API.""" -from datetime import datetime, timedelta +from dataclasses import dataclass +from datetime import timedelta import logging +from typing import Any -from PyTado.interface import Tado -from requests import RequestException import requests.exceptions -from homeassistant.components.climate import PRESET_AWAY, PRESET_HOME from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType -from homeassistant.util import Throttle from .const import ( CONF_FALLBACK, @@ -24,18 +21,13 @@ from .const import ( CONST_OVERLAY_TADO_DEFAULT, CONST_OVERLAY_TADO_MODE, CONST_OVERLAY_TADO_OPTIONS, - DATA, DOMAIN, - INSIDE_TEMPERATURE_MEASUREMENT, - PRESET_AUTO, - SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED, - SIGNAL_TADO_UPDATE_RECEIVED, - TEMP_OFFSET, UPDATE_LISTENER, UPDATE_MOBILE_DEVICE_TRACK, UPDATE_TRACK, ) from .services import setup_services +from .tado_connector import TadoConnector _LOGGER = logging.getLogger(__name__) @@ -63,7 +55,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +type TadoConfigEntry = ConfigEntry[TadoRuntimeData] + + +@dataclass +class TadoRuntimeData: + """Dataclass for Tado runtime data.""" + + tadoconnector: TadoConnector + update_track: Any + update_mobile_device_track: Any + update_listener: Any + + +async def async_setup_entry(hass: HomeAssistant, entry: TadoConfigEntry) -> bool: """Set up Tado from a config entry.""" _async_import_options_from_data_if_missing(hass, entry) @@ -108,13 +113,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_listener = entry.add_update_listener(_async_update_listener) - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA: tadoconnector, - UPDATE_TRACK: update_track, - UPDATE_MOBILE_DEVICE_TRACK: update_mobile_devices, - UPDATE_LISTENER: update_listener, - } + entry.runtime_data = TadoRuntimeData( + tadoconnector=tadoconnector, + update_track=update_track, + update_mobile_device_track=update_mobile_devices, + update_listener=update_listener, + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -155,301 +159,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -class TadoConnector: - """An object to store the Tado data.""" - - def __init__(self, hass, username, password, fallback): - """Initialize Tado Connector.""" - self.hass = hass - self._username = username - self._password = password - self._fallback = fallback - - self.home_id = None - self.home_name = None - self.tado = None - self.zones = None - self.devices = None - self.data = { - "device": {}, - "mobile_device": {}, - "weather": {}, - "geofence": {}, - "zone": {}, - } - - @property - def fallback(self): - """Return fallback flag to Smart Schedule.""" - return self._fallback - - def setup(self): - """Connect to Tado and fetch the zones.""" - self.tado = Tado(self._username, self._password) - # Load zones and devices - self.zones = self.tado.get_zones() - self.devices = self.tado.get_devices() - tado_home = self.tado.get_me()["homes"][0] - self.home_id = tado_home["id"] - self.home_name = tado_home["name"] - - def get_mobile_devices(self): - """Return the Tado mobile devices.""" - return self.tado.get_mobile_devices() - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update the registered zones.""" - self.update_devices() - self.update_mobile_devices() - self.update_zones() - self.update_home() - - def update_mobile_devices(self) -> None: - """Update the mobile devices.""" - try: - mobile_devices = self.get_mobile_devices() - except RuntimeError: - _LOGGER.error("Unable to connect to Tado while updating mobile devices") - return - - if not mobile_devices: - _LOGGER.debug("No linked mobile devices found for home ID %s", self.home_id) - return - - # Errors are planned to be converted to exceptions - # in PyTado library, so this can be removed - if isinstance(mobile_devices, dict) and mobile_devices.get("errors"): - _LOGGER.error( - "Error for home ID %s while updating mobile devices: %s", - self.home_id, - mobile_devices["errors"], - ) - return - - for mobile_device in mobile_devices: - self.data["mobile_device"][mobile_device["id"]] = mobile_device - _LOGGER.debug( - "Dispatching update to %s mobile device: %s", - self.home_id, - mobile_device, - ) - - dispatcher_send( - self.hass, - SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED.format(self.home_id), - ) - - def update_devices(self): - """Update the device data from Tado.""" - try: - devices = self.tado.get_devices() - except RuntimeError: - _LOGGER.error("Unable to connect to Tado while updating devices") - return - - if not devices: - _LOGGER.debug("No linked devices found for home ID %s", self.home_id) - return - - # Errors are planned to be converted to exceptions - # in PyTado library, so this can be removed - if isinstance(devices, dict) and devices.get("errors"): - _LOGGER.error( - "Error for home ID %s while updating devices: %s", - self.home_id, - devices["errors"], - ) - return - - for device in devices: - device_short_serial_no = device["shortSerialNo"] - _LOGGER.debug("Updating device %s", device_short_serial_no) - try: - if ( - INSIDE_TEMPERATURE_MEASUREMENT - in device["characteristics"]["capabilities"] - ): - device[TEMP_OFFSET] = self.tado.get_device_info( - device_short_serial_no, TEMP_OFFSET - ) - except RuntimeError: - _LOGGER.error( - "Unable to connect to Tado while updating device %s", - device_short_serial_no, - ) - return - - self.data["device"][device_short_serial_no] = device - - _LOGGER.debug( - "Dispatching update to %s device %s: %s", - self.home_id, - device_short_serial_no, - device, - ) - dispatcher_send( - self.hass, - SIGNAL_TADO_UPDATE_RECEIVED.format( - self.home_id, "device", device_short_serial_no - ), - ) - - def update_zones(self): - """Update the zone data from Tado.""" - try: - zone_states = self.tado.get_zone_states()["zoneStates"] - except RuntimeError: - _LOGGER.error("Unable to connect to Tado while updating zones") - return - - for zone in zone_states: - self.update_zone(int(zone)) - - def update_zone(self, zone_id): - """Update the internal data from Tado.""" - _LOGGER.debug("Updating zone %s", zone_id) - try: - data = self.tado.get_zone_state(zone_id) - except RuntimeError: - _LOGGER.error("Unable to connect to Tado while updating zone %s", zone_id) - return - - self.data["zone"][zone_id] = data - - _LOGGER.debug( - "Dispatching update to %s zone %s: %s", - self.home_id, - zone_id, - data, - ) - dispatcher_send( - self.hass, - SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "zone", zone_id), - ) - - def update_home(self): - """Update the home data from Tado.""" - try: - self.data["weather"] = self.tado.get_weather() - self.data["geofence"] = self.tado.get_home_state() - dispatcher_send( - self.hass, - SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "home", "data"), - ) - except RuntimeError: - _LOGGER.error( - "Unable to connect to Tado while updating weather and geofence data" - ) - return - - def get_capabilities(self, zone_id): - """Return the capabilities of the devices.""" - return self.tado.get_capabilities(zone_id) - - def get_auto_geofencing_supported(self): - """Return whether the Tado Home supports auto geofencing.""" - return self.tado.get_auto_geofencing_supported() - - def reset_zone_overlay(self, zone_id): - """Reset the zone back to the default operation.""" - self.tado.reset_zone_overlay(zone_id) - self.update_zone(zone_id) - - def set_presence( - self, - presence=PRESET_HOME, - ): - """Set the presence to home, away or auto.""" - if presence == PRESET_AWAY: - self.tado.set_away() - elif presence == PRESET_HOME: - self.tado.set_home() - elif presence == PRESET_AUTO: - self.tado.set_auto() - - # Update everything when changing modes - self.update_zones() - self.update_home() - - def set_zone_overlay( - self, - zone_id=None, - overlay_mode=None, - temperature=None, - duration=None, - device_type="HEATING", - mode=None, - fan_speed=None, - swing=None, - fan_level=None, - vertical_swing=None, - horizontal_swing=None, - ): - """Set a zone overlay.""" - _LOGGER.debug( - ( - "Set overlay for zone %s: overlay_mode=%s, temp=%s, duration=%s," - " type=%s, mode=%s fan_speed=%s swing=%s fan_level=%s vertical_swing=%s horizontal_swing=%s" - ), - zone_id, - overlay_mode, - temperature, - duration, - device_type, - mode, - fan_speed, - swing, - fan_level, - vertical_swing, - horizontal_swing, - ) - - try: - self.tado.set_zone_overlay( - zone_id, - overlay_mode, - temperature, - duration, - device_type, - "ON", - mode, - fan_speed=fan_speed, - swing=swing, - fan_level=fan_level, - vertical_swing=vertical_swing, - horizontal_swing=horizontal_swing, - ) - - except RequestException as exc: - _LOGGER.error("Could not set zone overlay: %s", exc) - - self.update_zone(zone_id) - - def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"): - """Set a zone to off.""" - try: - self.tado.set_zone_overlay( - zone_id, overlay_mode, None, None, device_type, "OFF" - ) - except RequestException as exc: - _LOGGER.error("Could not set zone overlay: %s", exc) - - self.update_zone(zone_id) - - def set_temperature_offset(self, device_id, offset): - """Set temperature offset of device.""" - try: - self.tado.set_temp_offset(device_id, offset) - except RequestException as exc: - _LOGGER.error("Could not set temperature offset: %s", exc) - - def set_meter_reading(self, reading: int) -> dict[str, str]: - """Send meter reading to Tado.""" - dt: str = datetime.now().strftime("%Y-%m-%d") - try: - return self.tado.set_eiq_meter_readings(date=dt, reading=reading) - except RequestException as exc: - raise HomeAssistantError("Could not set meter reading") from exc diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py index 0e8cbd1d175..ec8eb9331ac 100644 --- a/homeassistant/components/tado/binary_sensor.py +++ b/homeassistant/components/tado/binary_sensor.py @@ -12,16 +12,13 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import TadoConnector +from . import TadoConfigEntry from .const import ( - DATA, - DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED, TYPE_AIR_CONDITIONING, TYPE_BATTERY, @@ -30,6 +27,7 @@ from .const import ( TYPE_POWER, ) from .entity import TadoDeviceEntity, TadoZoneEntity +from .tado_connector import TadoConnector _LOGGER = logging.getLogger(__name__) @@ -68,9 +66,9 @@ OVERLAY_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription( key="overlay", translation_key="overlay", state_fn=lambda data: data.overlay_active, - attributes_fn=lambda data: {"termination": data.overlay_termination_type} - if data.overlay_active - else {}, + attributes_fn=lambda data: ( + {"termination": data.overlay_termination_type} if data.overlay_active else {} + ), device_class=BinarySensorDeviceClass.POWER, ) OPEN_WINDOW_ENTITY_DESCRIPTION = TadoBinarySensorEntityDescription( @@ -119,11 +117,11 @@ ZONE_SENSORS = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: TadoConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Tado sensor platform.""" - tado = hass.data[DOMAIN][entry.entry_id][DATA] + tado: TadoConnector = entry.runtime_data.tadoconnector devices = tado.devices zones = tado.zones entities: list[BinarySensorEntity] = [] diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 116985796d5..314a2315d0a 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -22,7 +22,6 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform @@ -30,7 +29,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import VolDictType -from . import TadoConnector +from . import TadoConfigEntry, TadoConnector from .const import ( CONST_EXCLUSIVE_OVERLAY_GROUP, CONST_FAN_AUTO, @@ -42,7 +41,6 @@ from .const import ( CONST_MODE_SMART_SCHEDULE, CONST_OVERLAY_MANUAL, CONST_OVERLAY_TADO_OPTIONS, - DATA, DOMAIN, HA_TERMINATION_DURATION, HA_TERMINATION_TYPE, @@ -100,11 +98,11 @@ CLIMATE_TEMP_OFFSET_SCHEMA: VolDictType = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: TadoConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Tado climate platform.""" - tado = hass.data[DOMAIN][entry.entry_id][DATA] + tado: TadoConnector = entry.runtime_data.tadoconnector entities = await hass.async_add_executor_job(_generate_entities, tado) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index 1caea1b3103..b4456591b49 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -9,27 +9,27 @@ from homeassistant.components.device_tracker import ( SourceType, TrackerEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_HOME, STATE_NOT_HOME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import TadoConnector -from .const import DATA, DOMAIN, SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED +from . import TadoConfigEntry +from .const import DOMAIN, SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED +from .tado_connector import TadoConnector _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, - entry: ConfigEntry, + entry: TadoConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Tado device scannery entity.""" _LOGGER.debug("Setting up Tado device scanner entity") - tado: TadoConnector = hass.data[DOMAIN][entry.entry_id][DATA] + tado: TadoConnector = entry.runtime_data.tadoconnector tracked: set = set() # Fix non-string unique_id for device trackers diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index 38110b4fded..6bb90ab849a 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -43,7 +43,7 @@ class TadoHomeEntity(Entity): self.home_id = tado.home_id self._attr_device_info = DeviceInfo( configuration_url="https://app.tado.com", - identifiers={(DOMAIN, tado.home_id)}, + identifiers={(DOMAIN, str(tado.home_id))}, manufacturer=DEFAULT_NAME, model=TADO_HOME, name=tado.home_name, diff --git a/homeassistant/components/tado/helper.py b/homeassistant/components/tado/helper.py index 81bff1e36c3..558aee164d0 100644 --- a/homeassistant/components/tado/helper.py +++ b/homeassistant/components/tado/helper.py @@ -1,11 +1,11 @@ """Helper methods for Tado.""" -from . import TadoConnector from .const import ( CONST_OVERLAY_TADO_DEFAULT, CONST_OVERLAY_TADO_MODE, CONST_OVERLAY_TIMER, ) +from .tado_connector import TadoConnector def decide_overlay_mode( diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index f8572ac3bc8..e5e2948b3a9 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -13,18 +13,15 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import TadoConnector +from . import TadoConfigEntry from .const import ( CONDITIONS_MAP, - DATA, - DOMAIN, SENSOR_DATA_CATEGORY_GEOFENCE, SENSOR_DATA_CATEGORY_WEATHER, SIGNAL_TADO_UPDATE_RECEIVED, @@ -33,6 +30,7 @@ from .const import ( TYPE_HOT_WATER, ) from .entity import TadoHomeEntity, TadoZoneEntity +from .tado_connector import TadoConnector _LOGGER = logging.getLogger(__name__) @@ -197,11 +195,11 @@ ZONE_SENSORS = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: TadoConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Tado sensor platform.""" - tado = hass.data[DOMAIN][entry.entry_id][DATA] + tado: TadoConnector = entry.runtime_data.tadoconnector zones = tado.zones entities: list[SensorEntity] = [] diff --git a/homeassistant/components/tado/services.py b/homeassistant/components/tado/services.py index d2a0250016f..8401f1925eb 100644 --- a/homeassistant/components/tado/services.py +++ b/homeassistant/components/tado/services.py @@ -5,17 +5,17 @@ import logging import voluptuous as vol from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import selector from .const import ( ATTR_MESSAGE, CONF_CONFIG_ENTRY, CONF_READING, - DATA, DOMAIN, SERVICE_ADD_METER_READING, ) +from .tado_connector import TadoConnector _LOGGER = logging.getLogger(__name__) SCHEMA_ADD_METER_READING = vol.Schema( @@ -40,7 +40,12 @@ def setup_services(hass: HomeAssistant) -> None: reading: int = call.data[CONF_READING] _LOGGER.debug("Add meter reading %s", reading) - tadoconnector = hass.data[DOMAIN][entry_id][DATA] + entry = hass.config_entries.async_get_entry(entry_id) + if entry is None: + raise ServiceValidationError("Config entry not found") + + tadoconnector: TadoConnector = entry.runtime_data.tadoconnector + response: dict = await hass.async_add_executor_job( tadoconnector.set_meter_reading, call.data[CONF_READING] ) diff --git a/homeassistant/components/tado/tado_connector.py b/homeassistant/components/tado/tado_connector.py new file mode 100644 index 00000000000..5ed53675153 --- /dev/null +++ b/homeassistant/components/tado/tado_connector.py @@ -0,0 +1,332 @@ +"""Tado Connector a class to store the data as an object.""" + +from datetime import datetime, timedelta +import logging +from typing import Any + +from PyTado.interface import Tado +from requests import RequestException + +from homeassistant.components.climate import PRESET_AWAY, PRESET_HOME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.util import Throttle + +from .const import ( + INSIDE_TEMPERATURE_MEASUREMENT, + PRESET_AUTO, + SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED, + SIGNAL_TADO_UPDATE_RECEIVED, + TEMP_OFFSET, +) + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4) +SCAN_INTERVAL = timedelta(minutes=5) +SCAN_MOBILE_DEVICE_INTERVAL = timedelta(seconds=30) + + +_LOGGER = logging.getLogger(__name__) + + +class TadoConnector: + """An object to store the Tado data.""" + + def __init__( + self, hass: HomeAssistant, username: str, password: str, fallback: str + ) -> None: + """Initialize Tado Connector.""" + self.hass = hass + self._username = username + self._password = password + self._fallback = fallback + + self.home_id: int = 0 + self.home_name = None + self.tado = None + self.zones: list[dict[Any, Any]] = [] + self.devices: list[dict[Any, Any]] = [] + self.data: dict[str, dict] = { + "device": {}, + "mobile_device": {}, + "weather": {}, + "geofence": {}, + "zone": {}, + } + + @property + def fallback(self): + """Return fallback flag to Smart Schedule.""" + return self._fallback + + def setup(self): + """Connect to Tado and fetch the zones.""" + self.tado = Tado(self._username, self._password) + # Load zones and devices + self.zones = self.tado.get_zones() + self.devices = self.tado.get_devices() + tado_home = self.tado.get_me()["homes"][0] + self.home_id = tado_home["id"] + self.home_name = tado_home["name"] + + def get_mobile_devices(self): + """Return the Tado mobile devices.""" + return self.tado.get_mobile_devices() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update the registered zones.""" + self.update_devices() + self.update_mobile_devices() + self.update_zones() + self.update_home() + + def update_mobile_devices(self) -> None: + """Update the mobile devices.""" + try: + mobile_devices = self.get_mobile_devices() + except RuntimeError: + _LOGGER.error("Unable to connect to Tado while updating mobile devices") + return + + if not mobile_devices: + _LOGGER.debug("No linked mobile devices found for home ID %s", self.home_id) + return + + # Errors are planned to be converted to exceptions + # in PyTado library, so this can be removed + if isinstance(mobile_devices, dict) and mobile_devices.get("errors"): + _LOGGER.error( + "Error for home ID %s while updating mobile devices: %s", + self.home_id, + mobile_devices["errors"], + ) + return + + for mobile_device in mobile_devices: + self.data["mobile_device"][mobile_device["id"]] = mobile_device + _LOGGER.debug( + "Dispatching update to %s mobile device: %s", + self.home_id, + mobile_device, + ) + + dispatcher_send( + self.hass, + SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED.format(self.home_id), + ) + + def update_devices(self): + """Update the device data from Tado.""" + try: + devices = self.tado.get_devices() + except RuntimeError: + _LOGGER.error("Unable to connect to Tado while updating devices") + return + + if not devices: + _LOGGER.debug("No linked devices found for home ID %s", self.home_id) + return + + # Errors are planned to be converted to exceptions + # in PyTado library, so this can be removed + if isinstance(devices, dict) and devices.get("errors"): + _LOGGER.error( + "Error for home ID %s while updating devices: %s", + self.home_id, + devices["errors"], + ) + return + + for device in devices: + device_short_serial_no = device["shortSerialNo"] + _LOGGER.debug("Updating device %s", device_short_serial_no) + try: + if ( + INSIDE_TEMPERATURE_MEASUREMENT + in device["characteristics"]["capabilities"] + ): + device[TEMP_OFFSET] = self.tado.get_device_info( + device_short_serial_no, TEMP_OFFSET + ) + except RuntimeError: + _LOGGER.error( + "Unable to connect to Tado while updating device %s", + device_short_serial_no, + ) + return + + self.data["device"][device_short_serial_no] = device + + _LOGGER.debug( + "Dispatching update to %s device %s: %s", + self.home_id, + device_short_serial_no, + device, + ) + dispatcher_send( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format( + self.home_id, "device", device_short_serial_no + ), + ) + + def update_zones(self): + """Update the zone data from Tado.""" + try: + zone_states = self.tado.get_zone_states()["zoneStates"] + except RuntimeError: + _LOGGER.error("Unable to connect to Tado while updating zones") + return + + for zone in zone_states: + self.update_zone(int(zone)) + + def update_zone(self, zone_id): + """Update the internal data from Tado.""" + _LOGGER.debug("Updating zone %s", zone_id) + try: + data = self.tado.get_zone_state(zone_id) + except RuntimeError: + _LOGGER.error("Unable to connect to Tado while updating zone %s", zone_id) + return + + self.data["zone"][zone_id] = data + + _LOGGER.debug( + "Dispatching update to %s zone %s: %s", + self.home_id, + zone_id, + data, + ) + dispatcher_send( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "zone", zone_id), + ) + + def update_home(self): + """Update the home data from Tado.""" + try: + self.data["weather"] = self.tado.get_weather() + self.data["geofence"] = self.tado.get_home_state() + dispatcher_send( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, "home", "data"), + ) + except RuntimeError: + _LOGGER.error( + "Unable to connect to Tado while updating weather and geofence data" + ) + return + + def get_capabilities(self, zone_id): + """Return the capabilities of the devices.""" + return self.tado.get_capabilities(zone_id) + + def get_auto_geofencing_supported(self): + """Return whether the Tado Home supports auto geofencing.""" + return self.tado.get_auto_geofencing_supported() + + def reset_zone_overlay(self, zone_id): + """Reset the zone back to the default operation.""" + self.tado.reset_zone_overlay(zone_id) + self.update_zone(zone_id) + + def set_presence( + self, + presence=PRESET_HOME, + ): + """Set the presence to home, away or auto.""" + if presence == PRESET_AWAY: + self.tado.set_away() + elif presence == PRESET_HOME: + self.tado.set_home() + elif presence == PRESET_AUTO: + self.tado.set_auto() + + # Update everything when changing modes + self.update_zones() + self.update_home() + + def set_zone_overlay( + self, + zone_id=None, + overlay_mode=None, + temperature=None, + duration=None, + device_type="HEATING", + mode=None, + fan_speed=None, + swing=None, + fan_level=None, + vertical_swing=None, + horizontal_swing=None, + ): + """Set a zone overlay.""" + _LOGGER.debug( + ( + "Set overlay for zone %s: overlay_mode=%s, temp=%s, duration=%s," + " type=%s, mode=%s fan_speed=%s swing=%s fan_level=%s vertical_swing=%s horizontal_swing=%s" + ), + zone_id, + overlay_mode, + temperature, + duration, + device_type, + mode, + fan_speed, + swing, + fan_level, + vertical_swing, + horizontal_swing, + ) + + try: + self.tado.set_zone_overlay( + zone_id, + overlay_mode, + temperature, + duration, + device_type, + "ON", + mode, + fan_speed=fan_speed, + swing=swing, + fan_level=fan_level, + vertical_swing=vertical_swing, + horizontal_swing=horizontal_swing, + ) + + except RequestException as exc: + _LOGGER.error("Could not set zone overlay: %s", exc) + + self.update_zone(zone_id) + + def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"): + """Set a zone to off.""" + try: + self.tado.set_zone_overlay( + zone_id, overlay_mode, None, None, device_type, "OFF" + ) + except RequestException as exc: + _LOGGER.error("Could not set zone overlay: %s", exc) + + self.update_zone(zone_id) + + def set_temperature_offset(self, device_id, offset): + """Set temperature offset of device.""" + try: + self.tado.set_temp_offset(device_id, offset) + except RequestException as exc: + _LOGGER.error("Could not set temperature offset: %s", exc) + + def set_meter_reading(self, reading: int) -> dict[str, Any]: + """Send meter reading to Tado.""" + dt: str = datetime.now().strftime("%Y-%m-%d") + if self.tado is None: + raise HomeAssistantError("Tado client is not initialized") + + try: + return self.tado.set_eiq_meter_readings(date=dt, reading=reading) + except RequestException as exc: + raise HomeAssistantError("Could not set meter reading") from exc diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index 0954db71460..896c10acf67 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -9,7 +9,6 @@ from homeassistant.components.water_heater import ( WaterHeaterEntity, WaterHeaterEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform @@ -17,7 +16,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import VolDictType -from . import TadoConnector +from . import TadoConfigEntry from .const import ( CONST_HVAC_HEAT, CONST_MODE_AUTO, @@ -27,14 +26,13 @@ from .const import ( CONST_OVERLAY_MANUAL, CONST_OVERLAY_TADO_MODE, CONST_OVERLAY_TIMER, - DATA, - DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED, TYPE_HOT_WATER, ) from .entity import TadoZoneEntity from .helper import decide_duration, decide_overlay_mode from .repairs import manage_water_heater_fallback_issue +from .tado_connector import TadoConnector _LOGGER = logging.getLogger(__name__) @@ -65,11 +63,11 @@ WATER_HEATER_TIMER_SCHEMA: VolDictType = { async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: TadoConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Tado water heater platform.""" - tado = hass.data[DOMAIN][entry.entry_id][DATA] + tado: TadoConnector = entry.runtime_data.tadoconnector entities = await hass.async_add_executor_job(_generate_entities, tado) platform = entity_platform.async_get_current_platform() @@ -95,7 +93,9 @@ def _generate_entities(tado: TadoConnector) -> list: for zone in tado.zones: if zone["type"] == TYPE_HOT_WATER: - entity = create_water_heater_entity(tado, zone["name"], zone["id"], zone) + entity = create_water_heater_entity( + tado, zone["name"], zone["id"], str(zone["name"]) + ) entities.append(entity) return entities