From 04b5d6c6976333d88877a2760874febc2052041d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Fri, 20 Dec 2019 13:24:43 +0100 Subject: [PATCH] Rework tado component (#29246) * Fix imports so it works in custom_components * Rework tado component * Code cleanup * Remove water_heater * Address pylint warnings * Remove water_heater from components * Raise PlatformNotReady when we couldn't connect * Revert PlatformNotReady since we are not a platform * Add debugging information * Add fallback setting * Import with relative path * Address race condition * Cleanup * Catch 422 Errors and log the real error * Use async_schedule_update_ha_state to update the entities * Forgot the True --- homeassistant/components/tado/__init__.py | 184 ++++++---- homeassistant/components/tado/climate.py | 390 ++++++++-------------- homeassistant/components/tado/const.py | 18 + homeassistant/components/tado/sensor.py | 173 +++++----- 4 files changed, 345 insertions(+), 420 deletions(-) create mode 100644 homeassistant/components/tado/const.py diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 1739cbb9254..ebf605bdc75 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -9,23 +9,29 @@ import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.util import Throttle +from .const import CONF_FALLBACK + _LOGGER = logging.getLogger(__name__) -DATA_TADO = "tado_data" DOMAIN = "tado" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) +SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}" TADO_COMPONENTS = ["sensor", "climate"] +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=15) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_FALLBACK, default=True): cv.boolean, } ) }, @@ -38,91 +44,106 @@ def setup(hass, config): username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] - try: - tado = Tado(username, password) - tado.setDebugging(True) - except (RuntimeError, urllib.error.HTTPError): - _LOGGER.error("Unable to connect to mytado with username and password") + tadoconnector = TadoConnector(hass, username, password) + if not tadoconnector.setup(): return False - hass.data[DATA_TADO] = TadoDataStore(tado) + hass.data[DOMAIN] = tadoconnector + # Do first update + tadoconnector.update() + + # Load components for component in TADO_COMPONENTS: - load_platform(hass, component, DOMAIN, {}, config) + load_platform( + hass, + component, + DOMAIN, + {CONF_FALLBACK: config[DOMAIN][CONF_FALLBACK]}, + config, + ) + + # Poll for updates in the background + hass.helpers.event.track_time_interval( + lambda now: tadoconnector.update(), SCAN_INTERVAL + ) return True -class TadoDataStore: +class TadoConnector: """An object to store the Tado data.""" - def __init__(self, tado): - """Initialize Tado data store.""" - self.tado = tado + def __init__(self, hass, username, password): + """Initialize Tado Connector.""" + self.hass = hass + self._username = username + self._password = password - self.sensors = {} - self.data = {} + self.tado = None + self.zones = None + self.devices = None + self.data = { + "zone": {}, + "device": {}, + } + + def setup(self): + """Connect to Tado and fetch the zones.""" + try: + self.tado = Tado(self._username, self._password) + except (RuntimeError, urllib.error.HTTPError) as exc: + _LOGGER.error("Unable to connect: %s", exc) + return False + + self.tado.setDebugging(True) + + # Load zones and devices + self.zones = self.tado.getZones() + self.devices = self.tado.getMe()["homes"] + + return True @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - """Update the internal data from mytado.com.""" - for data_id, sensor in list(self.sensors.items()): - data = None + """Update the registered zones.""" + for zone in self.zones: + self.update_sensor("zone", zone["id"]) + for device in self.devices: + self.update_sensor("device", device["id"]) - try: - if "zone" in sensor: - _LOGGER.debug( - "Querying mytado.com for zone %s %s", - sensor["id"], - sensor["name"], - ) - data = self.tado.getState(sensor["id"]) + def update_sensor(self, sensor_type, sensor): + """Update the internal data from Tado.""" + _LOGGER.debug("Updating %s %s", sensor_type, sensor) + try: + if sensor_type == "zone": + data = self.tado.getState(sensor) + elif sensor_type == "device": + data = self.tado.getDevices()[0] + else: + _LOGGER.debug("Unknown sensor: %s", sensor_type) + return + except RuntimeError: + _LOGGER.error( + "Unable to connect to Tado while updating %s %s", sensor_type, sensor, + ) + return - if "device" in sensor: - _LOGGER.debug( - "Querying mytado.com for device %s %s", - sensor["id"], - sensor["name"], - ) - data = self.tado.getDevices()[0] + self.data[sensor_type][sensor] = data - except RuntimeError: - _LOGGER.error( - "Unable to connect to myTado. %s %s", sensor["id"], sensor["id"] - ) + _LOGGER.debug("Dispatching update to %s %s: %s", sensor_type, sensor, data) + dispatcher_send( + self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format(sensor_type, sensor) + ) - self.data[data_id] = data - - def add_sensor(self, data_id, sensor): - """Add a sensor to update in _update().""" - self.sensors[data_id] = sensor - self.data[data_id] = None - - def get_data(self, data_id): - """Get the cached data.""" - data = {"error": "no data"} - - if data_id in self.data: - data = self.data[data_id] - - return data - - def get_zones(self): - """Wrap for getZones().""" - return self.tado.getZones() - - def get_capabilities(self, tado_id): - """Wrap for getCapabilities(..).""" - return self.tado.getCapabilities(tado_id) - - def get_me(self): - """Wrap for getMe().""" - return self.tado.getMe() + def get_capabilities(self, zone_id): + """Return the capabilities of the devices.""" + return self.tado.getCapabilities(zone_id) def reset_zone_overlay(self, zone_id): - """Wrap for resetZoneOverlay(..).""" + """Reset the zone back to the default operation.""" self.tado.resetZoneOverlay(zone_id) - self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg + self.update_sensor("zone", zone_id) def set_zone_overlay( self, @@ -133,13 +154,32 @@ class TadoDataStore: device_type="HEATING", mode=None, ): - """Wrap for setZoneOverlay(..).""" - self.tado.setZoneOverlay( - zone_id, overlay_mode, temperature, duration, device_type, "ON", mode + """Set a zone overlay.""" + _LOGGER.debug( + "Set overlay for zone %s: mode=%s, temp=%s, duration=%s, type=%s, mode=%s", + zone_id, + overlay_mode, + temperature, + duration, + device_type, + mode, ) - self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg + try: + self.tado.setZoneOverlay( + zone_id, overlay_mode, temperature, duration, device_type, "ON", mode + ) + except urllib.error.HTTPError as exc: + _LOGGER.error("Could not set zone overlay: %s", exc.read()) + + self.update_sensor("zone", zone_id) def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"): """Set a zone to off.""" - self.tado.setZoneOverlay(zone_id, overlay_mode, None, None, device_type, "OFF") - self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg + try: + self.tado.setZoneOverlay( + zone_id, overlay_mode, None, None, device_type, "OFF" + ) + except urllib.error.HTTPError as exc: + _LOGGER.error("Could not set zone overlay: %s", exc.read()) + + self.update_sensor("zone", zone_id) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 2baf1f380b5..88433db0991 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -1,6 +1,5 @@ -"""Support for Tado to create a climate device for each zone.""" +"""Support for Tado thermostats.""" import logging -from typing import List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -23,27 +22,20 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS -from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_TADO +from . import CONF_FALLBACK, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED +from .const import ( + CONST_MODE_OFF, + CONST_MODE_SMART_SCHEDULE, + CONST_OVERLAY_MANUAL, + CONST_OVERLAY_TADO_MODE, + TYPE_AIR_CONDITIONING, +) _LOGGER = logging.getLogger(__name__) -CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Default mytado mode -CONST_MODE_OFF = "OFF" # Switch off heating in a zone - -# When we change the temperature setting, we need an overlay mode -# wait until tado changes the mode automatic -CONST_OVERLAY_TADO_MODE = "TADO_MODE" -# the user has change the temperature or mode manually -CONST_OVERLAY_MANUAL = "MANUAL" -# the temperature will be reset after a timespan -CONST_OVERLAY_TIMER = "TIMER" - -CONST_MODE_FAN_HIGH = "HIGH" -CONST_MODE_FAN_MIDDLE = "MIDDLE" -CONST_MODE_FAN_LOW = "LOW" - FAN_MAP_TADO = {"HIGH": FAN_HIGH, "MIDDLE": FAN_MIDDLE, "LOW": FAN_LOW} HVAC_MAP_TADO_HEAT = { @@ -78,35 +70,29 @@ SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME] def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tado climate platform.""" - tado = hass.data[DATA_TADO] + tado = hass.data[DOMAIN] - try: - zones = tado.get_zones() - except RuntimeError: - _LOGGER.error("Unable to get zone info from mytado") - return + entities = [] + for zone in tado.zones: + entity = create_climate_entity( + tado, zone["name"], zone["id"], discovery_info[CONF_FALLBACK] + ) + if entity: + entities.append(entity) - climate_devices = [] - for zone in zones: - device = create_climate_device(tado, hass, zone, zone["name"], zone["id"]) - if not device: - continue - climate_devices.append(device) - - if climate_devices: - add_entities(climate_devices, True) + if entities: + add_entities(entities, True) -def create_climate_device(tado, hass, zone, name, zone_id): - """Create a Tado climate device.""" +def create_climate_entity(tado, name: str, zone_id: int, fallback: bool): + """Create a Tado climate entity.""" capabilities = tado.get_capabilities(zone_id) + _LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities) + + zone_type = capabilities["type"] - unit = TEMP_CELSIUS - ac_device = capabilities["type"] == "AIR_CONDITIONING" - hot_water_device = capabilities["type"] == "HOT_WATER" ac_support_heat = False - - if ac_device: + if zone_type == TYPE_AIR_CONDITIONING: # Only use heat if available # (you don't have to setup a heat mode, but cool is required) # Heat is preferred as it generally has a lower minimum temperature @@ -118,67 +104,56 @@ def create_climate_device(tado, hass, zone, name, zone_id): elif "temperatures" in capabilities: temperatures = capabilities["temperatures"] else: - _LOGGER.debug("Received zone %s has no temperature; not adding", name) - return + _LOGGER.debug("Not adding zone %s since it has no temperature", name) + return None min_temp = float(temperatures["celsius"]["min"]) max_temp = float(temperatures["celsius"]["max"]) step = temperatures["celsius"].get("step", PRECISION_TENTHS) - data_id = f"zone {name} {zone_id}" - device = TadoClimate( + entity = TadoClimate( tado, name, zone_id, - data_id, - hass.config.units.temperature(min_temp, unit), - hass.config.units.temperature(max_temp, unit), - step, - ac_device, - hot_water_device, - ac_support_heat, - ) - - tado.add_sensor( - data_id, {"id": zone_id, "zone": zone, "name": name, "climate": device} - ) - - return device - - -class TadoClimate(ClimateDevice): - """Representation of a Tado climate device.""" - - def __init__( - self, - store, - zone_name, - zone_id, - data_id, + zone_type, min_temp, max_temp, step, - ac_device, - hot_water_device, ac_support_heat, - tolerance=0.3, + fallback, + ) + return entity + + +class TadoClimate(ClimateDevice): + """Representation of a Tado climate entity.""" + + def __init__( + self, + tado, + zone_name, + zone_id, + zone_type, + min_temp, + max_temp, + step, + ac_support_heat, + fallback, ): - """Initialize of Tado climate device.""" - self._store = store - self._data_id = data_id + """Initialize of Tado climate entity.""" + self._tado = tado self.zone_name = zone_name self.zone_id = zone_id + self.zone_type = zone_type - self._ac_device = ac_device - self._hot_water_device = hot_water_device + self._ac_device = zone_type == TYPE_AIR_CONDITIONING self._ac_support_heat = ac_support_heat self._cooling = False self._active = False self._device_is_active = False - self._unit = TEMP_CELSIUS self._cur_temp = None self._cur_humidity = None self._is_away = False @@ -186,12 +161,34 @@ class TadoClimate(ClimateDevice): self._max_temp = max_temp self._step = step self._target_temp = None - self._tolerance = tolerance + + if fallback: + _LOGGER.debug("Default overlay is set to TADO MODE") + # Fallback to Smart Schedule at next Schedule switch + self._default_overlay = CONST_OVERLAY_TADO_MODE + else: + _LOGGER.debug("Default overlay is set to MANUAL MODE") + # Don't fallback to Smart Schedule, but keep in manual mode + self._default_overlay = CONST_OVERLAY_MANUAL self._current_fan = CONST_MODE_OFF self._current_operation = CONST_MODE_SMART_SCHEDULE self._overlay_mode = CONST_MODE_SMART_SCHEDULE + async def async_added_to_hass(self): + """Register for sensor updates.""" + + @callback + def async_update_callback(): + """Schedule an entity update.""" + self.async_schedule_update_ha_state(True) + + async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id), + async_update_callback, + ) + @property def supported_features(self): """Return the list of supported features.""" @@ -199,18 +196,19 @@ class TadoClimate(ClimateDevice): @property def name(self): - """Return the name of the device.""" + """Return the name of the entity.""" return self.zone_name + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False + @property def current_humidity(self): """Return the current humidity.""" return self._cur_humidity - def set_humidity(self, humidity: int) -> None: - """Set new target humidity.""" - pass - @property def current_temperature(self): """Return the sensor temperature.""" @@ -234,9 +232,9 @@ class TadoClimate(ClimateDevice): Need to be a subset of HVAC_MODES. """ - if self._ac_device and self._ac_support_heat: - return SUPPORT_HVAC_HEAT_COOL - if self._ac_device and not self._ac_support_heat: + if self._ac_device: + if self._ac_support_heat: + return SUPPORT_HVAC_HEAT_COOL return SUPPORT_HVAC_COOL return SUPPORT_HVAC_HEAT @@ -248,16 +246,10 @@ class TadoClimate(ClimateDevice): """ if not self._device_is_active: return CURRENT_HVAC_OFF - if self._ac_device and self._ac_support_heat and self._cooling: - if self._active: - return CURRENT_HVAC_COOL - return CURRENT_HVAC_IDLE - if self._ac_device and self._ac_support_heat and not self._cooling: - if self._active: - return CURRENT_HVAC_HEAT - return CURRENT_HVAC_IDLE - if self._ac_device and not self._ac_support_heat: + if self._ac_device: if self._active: + if self._ac_support_heat and not self._cooling: + return CURRENT_HVAC_HEAT return CURRENT_HVAC_COOL return CURRENT_HVAC_IDLE if self._active: @@ -284,7 +276,7 @@ class TadoClimate(ClimateDevice): @property def preset_mode(self): - """Return the current preset mode, e.g., home, away, temp.""" + """Return the current preset mode (home, away).""" if self._is_away: return PRESET_AWAY return PRESET_HOME @@ -301,7 +293,7 @@ class TadoClimate(ClimateDevice): @property def temperature_unit(self): """Return the unit of measurement used by the platform.""" - return self._unit + return TEMP_CELSIUS @property def target_temperature_step(self): @@ -313,23 +305,13 @@ class TadoClimate(ClimateDevice): """Return the temperature we try to reach.""" return self._target_temp - @property - def target_temperature_high(self): - """Return the upper bound temperature we try to reach.""" - return None - - @property - def target_temperature_low(self): - """Return the lower bound temperature we try to reach.""" - return None - def set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return - self._current_operation = CONST_OVERLAY_TADO_MODE + self._current_operation = self._default_overlay self._overlay_mode = None self._target_temp = temperature self._control_heating() @@ -343,50 +325,51 @@ class TadoClimate(ClimateDevice): elif hvac_mode == HVAC_MODE_AUTO: mode = CONST_MODE_SMART_SCHEDULE elif hvac_mode == HVAC_MODE_HEAT: - mode = CONST_OVERLAY_TADO_MODE + mode = self._default_overlay elif hvac_mode == HVAC_MODE_COOL: - mode = CONST_OVERLAY_TADO_MODE + mode = self._default_overlay elif hvac_mode == HVAC_MODE_HEAT_COOL: - mode = CONST_OVERLAY_TADO_MODE + mode = self._default_overlay self._current_operation = mode self._overlay_mode = None - if self._target_temp is None and self._ac_device: - self._target_temp = 27 + + # Set a target temperature if we don't have any + # This can happen when we switch from Off to On + if self._target_temp is None: + if self._ac_device: + self._target_temp = self.max_temp + else: + self._target_temp = self.min_temp + self.schedule_update_ha_state() + self._control_heating() @property def min_temp(self): """Return the minimum temperature.""" - return convert_temperature( - self._min_temp, self._unit, self.hass.config.units.temperature_unit - ) + return self._min_temp @property def max_temp(self): """Return the maximum temperature.""" - return convert_temperature( - self._max_temp, self._unit, self.hass.config.units.temperature_unit - ) + return self._max_temp def update(self): - """Update the state of this climate device.""" - self._store.update() - - data = self._store.get_data(self._data_id) - - if data is None: - _LOGGER.debug("Received no data for zone %s", self.zone_name) + """Handle update callbacks.""" + _LOGGER.debug("Updating climate platform for zone %d", self.zone_id) + try: + data = self._tado.data["zone"][self.zone_id] + except KeyError: + _LOGGER.debug("No data") return if "sensorDataPoints" in data: sensor_data = data["sensorDataPoints"] - unit = TEMP_CELSIUS - if "insideTemperature" in sensor_data: temperature = float(sensor_data["insideTemperature"]["celsius"]) - self._cur_temp = self.hass.config.units.temperature(temperature, unit) + self._cur_temp = temperature if "humidity" in sensor_data: humidity = float(sensor_data["humidity"]["percentage"]) @@ -398,7 +381,7 @@ class TadoClimate(ClimateDevice): and data["setting"]["temperature"] is not None ): setting = float(data["setting"]["temperature"]["celsius"]) - self._target_temp = self.hass.config.units.temperature(setting, unit) + self._target_temp = setting if "tadoMode" in data: mode = data["tadoMode"] @@ -468,135 +451,38 @@ class TadoClimate(ClimateDevice): self._current_fan = fan_speed def _control_heating(self): - """Send new target temperature to mytado.""" - if None not in (self._cur_temp, self._target_temp): - _LOGGER.info( - "Obtained current (%d) and target temperature (%d). " - "Tado thermostat active", - self._cur_temp, - self._target_temp, - ) - + """Send new target temperature to Tado.""" if self._current_operation == CONST_MODE_SMART_SCHEDULE: - _LOGGER.info( - "Switching mytado.com to SCHEDULE (default) for zone %s (%d)", + _LOGGER.debug( + "Switching to SMART_SCHEDULE for zone %s (%d)", self.zone_name, self.zone_id, ) - self._store.reset_zone_overlay(self.zone_id) + self._tado.reset_zone_overlay(self.zone_id) self._overlay_mode = self._current_operation return if self._current_operation == CONST_MODE_OFF: - if self._ac_device: - _LOGGER.info( - "Switching mytado.com to OFF for zone %s (%d) - AIR_CONDITIONING", - self.zone_name, - self.zone_id, - ) - self._store.set_zone_off( - self.zone_id, CONST_OVERLAY_MANUAL, "AIR_CONDITIONING" - ) - elif self._hot_water_device: - _LOGGER.info( - "Switching mytado.com to OFF for zone %s (%d) - HOT_WATER", - self.zone_name, - self.zone_id, - ) - self._store.set_zone_off( - self.zone_id, CONST_OVERLAY_MANUAL, "HOT_WATER" - ) - else: - _LOGGER.info( - "Switching mytado.com to OFF for zone %s (%d) - HEATING", - self.zone_name, - self.zone_id, - ) - self._store.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, "HEATING") + _LOGGER.debug( + "Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id + ) + self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, self.zone_type) self._overlay_mode = self._current_operation return - if self._ac_device: - _LOGGER.info( - "Switching mytado.com to %s mode for zone %s (%d). Temp (%s) - AIR_CONDITIONING", - self._current_operation, - self.zone_name, - self.zone_id, - self._target_temp, - ) - self._store.set_zone_overlay( - self.zone_id, - self._current_operation, - self._target_temp, - None, - "AIR_CONDITIONING", - "COOL", - ) - elif self._hot_water_device: - _LOGGER.info( - "Switching mytado.com to %s mode for zone %s (%d). Temp (%s) - HOT_WATER", - self._current_operation, - self.zone_name, - self.zone_id, - self._target_temp, - ) - self._store.set_zone_overlay( - self.zone_id, - self._current_operation, - self._target_temp, - None, - "HOT_WATER", - ) - else: - _LOGGER.info( - "Switching mytado.com to %s mode for zone %s (%d). Temp (%s) - HEATING", - self._current_operation, - self.zone_name, - self.zone_id, - self._target_temp, - ) - self._store.set_zone_overlay( - self.zone_id, - self._current_operation, - self._target_temp, - None, - "HEATING", - ) - + _LOGGER.debug( + "Switching to %s for zone %s (%d) with temperature %s °C", + self._current_operation, + self.zone_name, + self.zone_id, + self._target_temp, + ) + self._tado.set_zone_overlay( + self.zone_id, + self._current_operation, + self._target_temp, + None, + self.zone_type, + "COOL" if self._ac_device else None, + ) self._overlay_mode = self._current_operation - - @property - def is_aux_heat(self) -> Optional[bool]: - """Return true if aux heater. - - Requires SUPPORT_AUX_HEAT. - """ - return None - - def turn_aux_heat_on(self) -> None: - """Turn auxiliary heater on.""" - pass - - def turn_aux_heat_off(self) -> None: - """Turn auxiliary heater off.""" - pass - - @property - def swing_mode(self) -> Optional[str]: - """Return the swing setting. - - Requires SUPPORT_SWING_MODE. - """ - return None - - @property - def swing_modes(self) -> Optional[List[str]]: - """Return the list of available swing modes. - - Requires SUPPORT_SWING_MODE. - """ - return None - - def set_swing_mode(self, swing_mode: str) -> None: - """Set new target swing operation.""" - pass diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py new file mode 100644 index 00000000000..3c0232c8ba2 --- /dev/null +++ b/homeassistant/components/tado/const.py @@ -0,0 +1,18 @@ +"""Constant values for the Tado component.""" + +# Configuration +CONF_FALLBACK = "fallback" + +# Types +TYPE_AIR_CONDITIONING = "AIR_CONDITIONING" +TYPE_HEATING = "HEATING" +TYPE_HOT_WATER = "HOT_WATER" + +# Base modes +CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Use the schedule +CONST_MODE_OFF = "OFF" # Switch off heating in a zone + +# When we change the temperature setting, we need an overlay mode +CONST_OVERLAY_TADO_MODE = "TADO_MODE" # wait until tado changes the mode automatic +CONST_OVERLAY_MANUAL = "MANUAL" # the user has change the temperature or mode manually +CONST_OVERLAY_TIMER = "TIMER" # the temperature will be reset after a timespan diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 346b27bec26..68b470aced7 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -1,130 +1,109 @@ """Support for Tado sensors for each zone.""" import logging -from homeassistant.const import ATTR_ID, ATTR_NAME, TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import DATA_TADO +from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED +from .const import TYPE_AIR_CONDITIONING, TYPE_HEATING, TYPE_HOT_WATER _LOGGER = logging.getLogger(__name__) -ATTR_DATA_ID = "data_id" -ATTR_DEVICE = "device" -ATTR_ZONE = "zone" +ZONE_SENSORS = { + TYPE_HEATING: [ + "temperature", + "humidity", + "power", + "link", + "heating", + "tado mode", + "overlay", + "early start", + ], + TYPE_AIR_CONDITIONING: [ + "temperature", + "humidity", + "power", + "link", + "ac", + "tado mode", + "overlay", + ], + TYPE_HOT_WATER: ["power", "link", "tado mode", "overlay"], +} -CLIMATE_HEAT_SENSOR_TYPES = [ - "temperature", - "humidity", - "power", - "link", - "heating", - "tado mode", - "overlay", - "early start", -] - -CLIMATE_COOL_SENSOR_TYPES = [ - "temperature", - "humidity", - "power", - "link", - "ac", - "tado mode", - "overlay", -] - -HOT_WATER_SENSOR_TYPES = ["power", "link", "tado mode", "overlay"] +DEVICE_SENSORS = ["tado bridge status"] def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the sensor platform.""" - tado = hass.data[DATA_TADO] + tado = hass.data[DOMAIN] - try: - zones = tado.get_zones() - except RuntimeError: - _LOGGER.error("Unable to get zone info from mytado") - return - - sensor_items = [] - for zone in zones: - if zone["type"] == "HEATING": - for variable in CLIMATE_HEAT_SENSOR_TYPES: - sensor_items.append( - create_zone_sensor(tado, zone, zone["name"], zone["id"], variable) - ) - elif zone["type"] == "HOT_WATER": - for variable in HOT_WATER_SENSOR_TYPES: - sensor_items.append( - create_zone_sensor(tado, zone, zone["name"], zone["id"], variable) - ) - elif zone["type"] == "AIR_CONDITIONING": - for variable in CLIMATE_COOL_SENSOR_TYPES: - sensor_items.append( - create_zone_sensor(tado, zone, zone["name"], zone["id"], variable) - ) - - me_data = tado.get_me() - sensor_items.append( - create_device_sensor( - tado, - me_data, - me_data["homes"][0]["name"], - me_data["homes"][0]["id"], - "tado bridge status", + # Create zone sensors + entities = [] + for zone in tado.zones: + entities.extend( + [ + create_zone_sensor(tado, zone["name"], zone["id"], variable) + for variable in ZONE_SENSORS.get(zone["type"]) + ] ) - ) - if sensor_items: - add_entities(sensor_items, True) + # Create device sensors + for home in tado.devices: + entities.extend( + [ + create_device_sensor(tado, home["name"], home["id"], variable) + for variable in DEVICE_SENSORS + ] + ) + + add_entities(entities, True) -def create_zone_sensor(tado, zone, name, zone_id, variable): +def create_zone_sensor(tado, name, zone_id, variable): """Create a zone sensor.""" - data_id = f"zone {name} {zone_id}" - - tado.add_sensor( - data_id, - {ATTR_ZONE: zone, ATTR_NAME: name, ATTR_ID: zone_id, ATTR_DATA_ID: data_id}, - ) - - return TadoSensor(tado, name, zone_id, variable, data_id) + return TadoSensor(tado, name, "zone", zone_id, variable) -def create_device_sensor(tado, device, name, device_id, variable): +def create_device_sensor(tado, name, device_id, variable): """Create a device sensor.""" - data_id = f"device {name} {device_id}" - - tado.add_sensor( - data_id, - { - ATTR_DEVICE: device, - ATTR_NAME: name, - ATTR_ID: device_id, - ATTR_DATA_ID: data_id, - }, - ) - - return TadoSensor(tado, name, device_id, variable, data_id) + return TadoSensor(tado, name, "device", device_id, variable) class TadoSensor(Entity): """Representation of a tado Sensor.""" - def __init__(self, store, zone_name, zone_id, zone_variable, data_id): + def __init__(self, tado, zone_name, sensor_type, zone_id, zone_variable): """Initialize of the Tado Sensor.""" - self._store = store + self._tado = tado self.zone_name = zone_name self.zone_id = zone_id self.zone_variable = zone_variable + self.sensor_type = sensor_type self._unique_id = f"{zone_variable} {zone_id}" - self._data_id = data_id self._state = None self._state_attributes = None + async def async_added_to_hass(self): + """Register for sensor updates.""" + + @callback + def async_update_callback(): + """Schedule an entity update.""" + self.async_schedule_update_ha_state(True) + + async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format(self.sensor_type, self.zone_id), + async_update_callback, + ) + @property def unique_id(self): """Return the unique id.""" @@ -165,14 +144,16 @@ class TadoSensor(Entity): if self.zone_variable == "humidity": return "mdi:water-percent" + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False + def update(self): - """Update method called when should_poll is true.""" - self._store.update() - - data = self._store.get_data(self._data_id) - - if data is None: - _LOGGER.debug("Received no data for zone %s", self.zone_name) + """Handle update callbacks.""" + try: + data = self._tado.data[self.sensor_type][self.zone_id] + except KeyError: return unit = TEMP_CELSIUS