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
This commit is contained in:
Michaël Arnauts 2019-12-20 13:24:43 +01:00 committed by GitHub
parent 92fd3e3ad5
commit 04b5d6c697
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 345 additions and 420 deletions

View file

@ -9,23 +9,29 @@ import voluptuous as vol
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.util import Throttle from homeassistant.util import Throttle
from .const import CONF_FALLBACK
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DATA_TADO = "tado_data"
DOMAIN = "tado" DOMAIN = "tado"
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}"
TADO_COMPONENTS = ["sensor", "climate"] TADO_COMPONENTS = ["sensor", "climate"]
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
SCAN_INTERVAL = timedelta(seconds=15)
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
{ {
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): 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] username = config[DOMAIN][CONF_USERNAME]
password = config[DOMAIN][CONF_PASSWORD] password = config[DOMAIN][CONF_PASSWORD]
try: tadoconnector = TadoConnector(hass, username, password)
tado = Tado(username, password) if not tadoconnector.setup():
tado.setDebugging(True)
except (RuntimeError, urllib.error.HTTPError):
_LOGGER.error("Unable to connect to mytado with username and password")
return False return False
hass.data[DATA_TADO] = TadoDataStore(tado) hass.data[DOMAIN] = tadoconnector
# Do first update
tadoconnector.update()
# Load components
for component in TADO_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 return True
class TadoDataStore: class TadoConnector:
"""An object to store the Tado data.""" """An object to store the Tado data."""
def __init__(self, tado): def __init__(self, hass, username, password):
"""Initialize Tado data store.""" """Initialize Tado Connector."""
self.tado = tado self.hass = hass
self._username = username
self._password = password
self.sensors = {} self.tado = None
self.data = {} 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) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
"""Update the internal data from mytado.com.""" """Update the registered zones."""
for data_id, sensor in list(self.sensors.items()): for zone in self.zones:
data = None self.update_sensor("zone", zone["id"])
for device in self.devices:
self.update_sensor("device", device["id"])
try: def update_sensor(self, sensor_type, sensor):
if "zone" in sensor: """Update the internal data from Tado."""
_LOGGER.debug( _LOGGER.debug("Updating %s %s", sensor_type, sensor)
"Querying mytado.com for zone %s %s", try:
sensor["id"], if sensor_type == "zone":
sensor["name"], data = self.tado.getState(sensor)
) elif sensor_type == "device":
data = self.tado.getState(sensor["id"]) 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: self.data[sensor_type][sensor] = data
_LOGGER.debug(
"Querying mytado.com for device %s %s",
sensor["id"],
sensor["name"],
)
data = self.tado.getDevices()[0]
except RuntimeError: _LOGGER.debug("Dispatching update to %s %s: %s", sensor_type, sensor, data)
_LOGGER.error( dispatcher_send(
"Unable to connect to myTado. %s %s", sensor["id"], sensor["id"] self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format(sensor_type, sensor)
) )
self.data[data_id] = data def get_capabilities(self, zone_id):
"""Return the capabilities of the devices."""
def add_sensor(self, data_id, sensor): return self.tado.getCapabilities(zone_id)
"""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 reset_zone_overlay(self, 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.tado.resetZoneOverlay(zone_id)
self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg self.update_sensor("zone", zone_id)
def set_zone_overlay( def set_zone_overlay(
self, self,
@ -133,13 +154,32 @@ class TadoDataStore:
device_type="HEATING", device_type="HEATING",
mode=None, mode=None,
): ):
"""Wrap for setZoneOverlay(..).""" """Set a zone overlay."""
self.tado.setZoneOverlay( _LOGGER.debug(
zone_id, overlay_mode, temperature, duration, device_type, "ON", mode "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"): def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"):
"""Set a zone to off.""" """Set a zone to off."""
self.tado.setZoneOverlay(zone_id, overlay_mode, None, None, device_type, "OFF") try:
self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg 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)

View file

@ -1,6 +1,5 @@
"""Support for Tado to create a climate device for each zone.""" """Support for Tado thermostats."""
import logging import logging
from typing import List, Optional
from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
@ -23,27 +22,20 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
) )
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS 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__) _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} FAN_MAP_TADO = {"HIGH": FAN_HIGH, "MIDDLE": FAN_MIDDLE, "LOW": FAN_LOW}
HVAC_MAP_TADO_HEAT = { HVAC_MAP_TADO_HEAT = {
@ -78,35 +70,29 @@ SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME]
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Tado climate platform.""" """Set up the Tado climate platform."""
tado = hass.data[DATA_TADO] tado = hass.data[DOMAIN]
try: entities = []
zones = tado.get_zones() for zone in tado.zones:
except RuntimeError: entity = create_climate_entity(
_LOGGER.error("Unable to get zone info from mytado") tado, zone["name"], zone["id"], discovery_info[CONF_FALLBACK]
return )
if entity:
entities.append(entity)
climate_devices = [] if entities:
for zone in zones: add_entities(entities, True)
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)
def create_climate_device(tado, hass, zone, name, zone_id): def create_climate_entity(tado, name: str, zone_id: int, fallback: bool):
"""Create a Tado climate device.""" """Create a Tado climate entity."""
capabilities = tado.get_capabilities(zone_id) 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 ac_support_heat = False
if zone_type == TYPE_AIR_CONDITIONING:
if ac_device:
# Only use heat if available # Only use heat if available
# (you don't have to setup a heat mode, but cool is required) # (you don't have to setup a heat mode, but cool is required)
# Heat is preferred as it generally has a lower minimum temperature # 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: elif "temperatures" in capabilities:
temperatures = capabilities["temperatures"] temperatures = capabilities["temperatures"]
else: else:
_LOGGER.debug("Received zone %s has no temperature; not adding", name) _LOGGER.debug("Not adding zone %s since it has no temperature", name)
return return None
min_temp = float(temperatures["celsius"]["min"]) min_temp = float(temperatures["celsius"]["min"])
max_temp = float(temperatures["celsius"]["max"]) max_temp = float(temperatures["celsius"]["max"])
step = temperatures["celsius"].get("step", PRECISION_TENTHS) step = temperatures["celsius"].get("step", PRECISION_TENTHS)
data_id = f"zone {name} {zone_id}" entity = TadoClimate(
device = TadoClimate(
tado, tado,
name, name,
zone_id, zone_id,
data_id, zone_type,
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,
min_temp, min_temp,
max_temp, max_temp,
step, step,
ac_device,
hot_water_device,
ac_support_heat, 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.""" """Initialize of Tado climate entity."""
self._store = store self._tado = tado
self._data_id = data_id
self.zone_name = zone_name self.zone_name = zone_name
self.zone_id = zone_id self.zone_id = zone_id
self.zone_type = zone_type
self._ac_device = ac_device self._ac_device = zone_type == TYPE_AIR_CONDITIONING
self._hot_water_device = hot_water_device
self._ac_support_heat = ac_support_heat self._ac_support_heat = ac_support_heat
self._cooling = False self._cooling = False
self._active = False self._active = False
self._device_is_active = False self._device_is_active = False
self._unit = TEMP_CELSIUS
self._cur_temp = None self._cur_temp = None
self._cur_humidity = None self._cur_humidity = None
self._is_away = False self._is_away = False
@ -186,12 +161,34 @@ class TadoClimate(ClimateDevice):
self._max_temp = max_temp self._max_temp = max_temp
self._step = step self._step = step
self._target_temp = None 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_fan = CONST_MODE_OFF
self._current_operation = CONST_MODE_SMART_SCHEDULE self._current_operation = CONST_MODE_SMART_SCHEDULE
self._overlay_mode = 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 @property
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""
@ -199,18 +196,19 @@ class TadoClimate(ClimateDevice):
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the entity."""
return self.zone_name return self.zone_name
@property
def should_poll(self) -> bool:
"""Do not poll."""
return False
@property @property
def current_humidity(self): def current_humidity(self):
"""Return the current humidity.""" """Return the current humidity."""
return self._cur_humidity return self._cur_humidity
def set_humidity(self, humidity: int) -> None:
"""Set new target humidity."""
pass
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the sensor temperature.""" """Return the sensor temperature."""
@ -234,9 +232,9 @@ class TadoClimate(ClimateDevice):
Need to be a subset of HVAC_MODES. Need to be a subset of HVAC_MODES.
""" """
if self._ac_device and self._ac_support_heat: if self._ac_device:
return SUPPORT_HVAC_HEAT_COOL if self._ac_support_heat:
if self._ac_device and not self._ac_support_heat: return SUPPORT_HVAC_HEAT_COOL
return SUPPORT_HVAC_COOL return SUPPORT_HVAC_COOL
return SUPPORT_HVAC_HEAT return SUPPORT_HVAC_HEAT
@ -248,16 +246,10 @@ class TadoClimate(ClimateDevice):
""" """
if not self._device_is_active: if not self._device_is_active:
return CURRENT_HVAC_OFF return CURRENT_HVAC_OFF
if self._ac_device and self._ac_support_heat and self._cooling: if self._ac_device:
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._active: if self._active:
if self._ac_support_heat and not self._cooling:
return CURRENT_HVAC_HEAT
return CURRENT_HVAC_COOL return CURRENT_HVAC_COOL
return CURRENT_HVAC_IDLE return CURRENT_HVAC_IDLE
if self._active: if self._active:
@ -284,7 +276,7 @@ class TadoClimate(ClimateDevice):
@property @property
def preset_mode(self): 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: if self._is_away:
return PRESET_AWAY return PRESET_AWAY
return PRESET_HOME return PRESET_HOME
@ -301,7 +293,7 @@ class TadoClimate(ClimateDevice):
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement used by the platform.""" """Return the unit of measurement used by the platform."""
return self._unit return TEMP_CELSIUS
@property @property
def target_temperature_step(self): def target_temperature_step(self):
@ -313,23 +305,13 @@ class TadoClimate(ClimateDevice):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self._target_temp 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): def set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None: if temperature is None:
return return
self._current_operation = CONST_OVERLAY_TADO_MODE self._current_operation = self._default_overlay
self._overlay_mode = None self._overlay_mode = None
self._target_temp = temperature self._target_temp = temperature
self._control_heating() self._control_heating()
@ -343,50 +325,51 @@ class TadoClimate(ClimateDevice):
elif hvac_mode == HVAC_MODE_AUTO: elif hvac_mode == HVAC_MODE_AUTO:
mode = CONST_MODE_SMART_SCHEDULE mode = CONST_MODE_SMART_SCHEDULE
elif hvac_mode == HVAC_MODE_HEAT: elif hvac_mode == HVAC_MODE_HEAT:
mode = CONST_OVERLAY_TADO_MODE mode = self._default_overlay
elif hvac_mode == HVAC_MODE_COOL: elif hvac_mode == HVAC_MODE_COOL:
mode = CONST_OVERLAY_TADO_MODE mode = self._default_overlay
elif hvac_mode == HVAC_MODE_HEAT_COOL: elif hvac_mode == HVAC_MODE_HEAT_COOL:
mode = CONST_OVERLAY_TADO_MODE mode = self._default_overlay
self._current_operation = mode self._current_operation = mode
self._overlay_mode = None 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() self._control_heating()
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
return convert_temperature( return self._min_temp
self._min_temp, self._unit, self.hass.config.units.temperature_unit
)
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
return convert_temperature( return self._max_temp
self._max_temp, self._unit, self.hass.config.units.temperature_unit
)
def update(self): def update(self):
"""Update the state of this climate device.""" """Handle update callbacks."""
self._store.update() _LOGGER.debug("Updating climate platform for zone %d", self.zone_id)
try:
data = self._store.get_data(self._data_id) data = self._tado.data["zone"][self.zone_id]
except KeyError:
if data is None: _LOGGER.debug("No data")
_LOGGER.debug("Received no data for zone %s", self.zone_name)
return return
if "sensorDataPoints" in data: if "sensorDataPoints" in data:
sensor_data = data["sensorDataPoints"] sensor_data = data["sensorDataPoints"]
unit = TEMP_CELSIUS
if "insideTemperature" in sensor_data: if "insideTemperature" in sensor_data:
temperature = float(sensor_data["insideTemperature"]["celsius"]) 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: if "humidity" in sensor_data:
humidity = float(sensor_data["humidity"]["percentage"]) humidity = float(sensor_data["humidity"]["percentage"])
@ -398,7 +381,7 @@ class TadoClimate(ClimateDevice):
and data["setting"]["temperature"] is not None and data["setting"]["temperature"] is not None
): ):
setting = float(data["setting"]["temperature"]["celsius"]) setting = float(data["setting"]["temperature"]["celsius"])
self._target_temp = self.hass.config.units.temperature(setting, unit) self._target_temp = setting
if "tadoMode" in data: if "tadoMode" in data:
mode = data["tadoMode"] mode = data["tadoMode"]
@ -468,135 +451,38 @@ class TadoClimate(ClimateDevice):
self._current_fan = fan_speed self._current_fan = fan_speed
def _control_heating(self): def _control_heating(self):
"""Send new target temperature to mytado.""" """Send new target temperature to Tado."""
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,
)
if self._current_operation == CONST_MODE_SMART_SCHEDULE: if self._current_operation == CONST_MODE_SMART_SCHEDULE:
_LOGGER.info( _LOGGER.debug(
"Switching mytado.com to SCHEDULE (default) for zone %s (%d)", "Switching to SMART_SCHEDULE for zone %s (%d)",
self.zone_name, self.zone_name,
self.zone_id, 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 self._overlay_mode = self._current_operation
return return
if self._current_operation == CONST_MODE_OFF: if self._current_operation == CONST_MODE_OFF:
if self._ac_device: _LOGGER.debug(
_LOGGER.info( "Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id
"Switching mytado.com to OFF for zone %s (%d) - AIR_CONDITIONING", )
self.zone_name, self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, self.zone_type)
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")
self._overlay_mode = self._current_operation self._overlay_mode = self._current_operation
return return
if self._ac_device: _LOGGER.debug(
_LOGGER.info( "Switching to %s for zone %s (%d) with temperature %s °C",
"Switching mytado.com to %s mode for zone %s (%d). Temp (%s) - AIR_CONDITIONING", self._current_operation,
self._current_operation, self.zone_name,
self.zone_name, self.zone_id,
self.zone_id, self._target_temp,
self._target_temp, )
) self._tado.set_zone_overlay(
self._store.set_zone_overlay( self.zone_id,
self.zone_id, self._current_operation,
self._current_operation, self._target_temp,
self._target_temp, None,
None, self.zone_type,
"AIR_CONDITIONING", "COOL" if self._ac_device else None,
"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",
)
self._overlay_mode = self._current_operation 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

View file

@ -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

View file

@ -1,130 +1,109 @@
"""Support for Tado sensors for each zone.""" """Support for Tado sensors for each zone."""
import logging 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 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__) _LOGGER = logging.getLogger(__name__)
ATTR_DATA_ID = "data_id" ZONE_SENSORS = {
ATTR_DEVICE = "device" TYPE_HEATING: [
ATTR_ZONE = "zone" "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 = [ DEVICE_SENSORS = ["tado bridge status"]
"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"]
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the sensor platform.""" """Set up the sensor platform."""
tado = hass.data[DATA_TADO] tado = hass.data[DOMAIN]
try: # Create zone sensors
zones = tado.get_zones() entities = []
except RuntimeError: for zone in tado.zones:
_LOGGER.error("Unable to get zone info from mytado") entities.extend(
return [
create_zone_sensor(tado, zone["name"], zone["id"], variable)
sensor_items = [] for variable in ZONE_SENSORS.get(zone["type"])
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",
) )
)
if sensor_items: # Create device sensors
add_entities(sensor_items, True) 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.""" """Create a zone sensor."""
data_id = f"zone {name} {zone_id}" return TadoSensor(tado, name, "zone", zone_id, variable)
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)
def create_device_sensor(tado, device, name, device_id, variable): def create_device_sensor(tado, name, device_id, variable):
"""Create a device sensor.""" """Create a device sensor."""
data_id = f"device {name} {device_id}" return TadoSensor(tado, name, "device", device_id, variable)
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)
class TadoSensor(Entity): class TadoSensor(Entity):
"""Representation of a tado Sensor.""" """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.""" """Initialize of the Tado Sensor."""
self._store = store self._tado = tado
self.zone_name = zone_name self.zone_name = zone_name
self.zone_id = zone_id self.zone_id = zone_id
self.zone_variable = zone_variable self.zone_variable = zone_variable
self.sensor_type = sensor_type
self._unique_id = f"{zone_variable} {zone_id}" self._unique_id = f"{zone_variable} {zone_id}"
self._data_id = data_id
self._state = None self._state = None
self._state_attributes = 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 @property
def unique_id(self): def unique_id(self):
"""Return the unique id.""" """Return the unique id."""
@ -165,14 +144,16 @@ class TadoSensor(Entity):
if self.zone_variable == "humidity": if self.zone_variable == "humidity":
return "mdi:water-percent" return "mdi:water-percent"
@property
def should_poll(self) -> bool:
"""Do not poll."""
return False
def update(self): def update(self):
"""Update method called when should_poll is true.""" """Handle update callbacks."""
self._store.update() try:
data = self._tado.data[self.sensor_type][self.zone_id]
data = self._store.get_data(self._data_id) except KeyError:
if data is None:
_LOGGER.debug("Received no data for zone %s", self.zone_name)
return return
unit = TEMP_CELSIUS unit = TEMP_CELSIUS