Move Tado zone state handling into upstream python-tado library (#33195)

* Tado climate state moved to python-tado

* Resolve various incorrect states and add tests for known tado zone states

* Write state instead of calling for an update

This is a redux of pr #32564 with all of the zone state now moved into
python-tado and tests added for the various states.

* stale string

* add missing undos to dispachers

* remove unneeded hass

* naming

* rearrange

* fix water heater, add test

* fix water heater, add test

* switch hvac mode when changing temp if in auto/off/smart
This commit is contained in:
J. Nick Koston 2020-03-30 09:06:26 -05:00 committed by GitHub
parent eee0a6e9f4
commit f42804805c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 2348 additions and 442 deletions

View File

@ -369,7 +369,7 @@ homeassistant/components/switchmate/* @danielhiversen
homeassistant/components/syncthru/* @nielstron homeassistant/components/syncthru/* @nielstron
homeassistant/components/synology_srm/* @aerialls homeassistant/components/synology_srm/* @aerialls
homeassistant/components/syslog/* @fabaff homeassistant/components/syslog/* @fabaff
homeassistant/components/tado/* @michaelarnauts homeassistant/components/tado/* @michaelarnauts @bdraco
homeassistant/components/tahoma/* @philklei homeassistant/components/tahoma/* @philklei
homeassistant/components/tankerkoenig/* @guillempages homeassistant/components/tankerkoenig/* @guillempages
homeassistant/components/tautulli/* @ludeeus homeassistant/components/tautulli/* @ludeeus

View File

@ -1,9 +1,9 @@
"""Support for the (unofficial) Tado API.""" """Support for the (unofficial) Tado API."""
from datetime import timedelta from datetime import timedelta
import logging import logging
import urllib
from PyTado.interface import Tado from PyTado.interface import Tado
from requests import RequestException
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate.const import PRESET_AWAY, PRESET_HOME from homeassistant.components.climate.const import PRESET_AWAY, PRESET_HOME
@ -110,7 +110,7 @@ class TadoConnector:
"""Connect to Tado and fetch the zones.""" """Connect to Tado and fetch the zones."""
try: try:
self.tado = Tado(self._username, self._password) self.tado = Tado(self._username, self._password)
except (RuntimeError, urllib.error.HTTPError) as exc: except (RuntimeError, RequestException) as exc:
_LOGGER.error("Unable to connect: %s", exc) _LOGGER.error("Unable to connect: %s", exc)
return False return False
@ -135,9 +135,14 @@ class TadoConnector:
_LOGGER.debug("Updating %s %s", sensor_type, sensor) _LOGGER.debug("Updating %s %s", sensor_type, sensor)
try: try:
if sensor_type == "zone": if sensor_type == "zone":
data = self.tado.getState(sensor) data = self.tado.getZoneState(sensor)
elif sensor_type == "device": elif sensor_type == "device":
data = self.tado.getDevices()[0] devices_data = self.tado.getDevices()
if not devices_data:
_LOGGER.info("There are no devices to setup on this tado account.")
return
data = devices_data[0]
else: else:
_LOGGER.debug("Unknown sensor: %s", sensor_type) _LOGGER.debug("Unknown sensor: %s", sensor_type)
return return
@ -174,29 +179,40 @@ class TadoConnector:
def set_zone_overlay( def set_zone_overlay(
self, self,
zone_id, zone_id=None,
overlay_mode, overlay_mode=None,
temperature=None, temperature=None,
duration=None, duration=None,
device_type="HEATING", device_type="HEATING",
mode=None, mode=None,
fan_speed=None,
): ):
"""Set a zone overlay.""" """Set a zone overlay."""
_LOGGER.debug( _LOGGER.debug(
"Set overlay for zone %s: mode=%s, temp=%s, duration=%s, type=%s, mode=%s", "Set overlay for zone %s: overlay_mode=%s, temp=%s, duration=%s, type=%s, mode=%s fan_speed=%s",
zone_id, zone_id,
overlay_mode, overlay_mode,
temperature, temperature,
duration, duration,
device_type, device_type,
mode, mode,
fan_speed,
) )
try: try:
self.tado.setZoneOverlay( self.tado.setZoneOverlay(
zone_id, overlay_mode, temperature, duration, device_type, "ON", mode zone_id,
overlay_mode,
temperature,
duration,
device_type,
"ON",
mode,
fan_speed,
) )
except urllib.error.HTTPError as exc:
_LOGGER.error("Could not set zone overlay: %s", exc.read()) except RequestException as exc:
_LOGGER.error("Could not set zone overlay: %s", exc)
self.update_sensor("zone", zone_id) self.update_sensor("zone", zone_id)
@ -206,7 +222,7 @@ class TadoConnector:
self.tado.setZoneOverlay( self.tado.setZoneOverlay(
zone_id, overlay_mode, None, None, device_type, "OFF" zone_id, overlay_mode, None, None, device_type, "OFF"
) )
except urllib.error.HTTPError as exc: except RequestException as exc:
_LOGGER.error("Could not set zone overlay: %s", exc.read()) _LOGGER.error("Could not set zone overlay: %s", exc)
self.update_sensor("zone", zone_id) self.update_sensor("zone", zone_id)

View File

@ -3,21 +3,13 @@ import logging
from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
CURRENT_HVAC_COOL,
CURRENT_HVAC_HEAT,
CURRENT_HVAC_IDLE,
CURRENT_HVAC_OFF, CURRENT_HVAC_OFF,
FAN_HIGH, FAN_AUTO,
FAN_LOW,
FAN_MIDDLE,
FAN_OFF,
HVAC_MODE_AUTO,
HVAC_MODE_COOL,
HVAC_MODE_HEAT, HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF, HVAC_MODE_OFF,
PRESET_AWAY, PRESET_AWAY,
PRESET_HOME, PRESET_HOME,
SUPPORT_FAN_MODE,
SUPPORT_PRESET_MODE, SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
) )
@ -27,49 +19,30 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED
from .const import ( from .const import (
CONST_FAN_AUTO,
CONST_FAN_OFF,
CONST_MODE_AUTO,
CONST_MODE_COOL,
CONST_MODE_HEAT,
CONST_MODE_OFF, CONST_MODE_OFF,
CONST_MODE_SMART_SCHEDULE, CONST_MODE_SMART_SCHEDULE,
CONST_OVERLAY_MANUAL, CONST_OVERLAY_MANUAL,
CONST_OVERLAY_TADO_MODE, CONST_OVERLAY_TADO_MODE,
CONST_OVERLAY_TIMER,
DATA, DATA,
HA_TO_TADO_FAN_MODE_MAP,
HA_TO_TADO_HVAC_MODE_MAP,
ORDERED_KNOWN_TADO_MODES,
SUPPORT_PRESET,
TADO_HVAC_ACTION_TO_HA_HVAC_ACTION,
TADO_MODES_WITH_NO_TEMP_SETTING,
TADO_TO_HA_FAN_MODE_MAP,
TADO_TO_HA_HVAC_MODE_MAP,
TYPE_AIR_CONDITIONING, TYPE_AIR_CONDITIONING,
TYPE_HEATING, TYPE_HEATING,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
FAN_MAP_TADO = {"HIGH": FAN_HIGH, "MIDDLE": FAN_MIDDLE, "LOW": FAN_LOW}
HVAC_MAP_TADO_HEAT = {
CONST_OVERLAY_MANUAL: HVAC_MODE_HEAT,
CONST_OVERLAY_TIMER: HVAC_MODE_HEAT,
CONST_OVERLAY_TADO_MODE: HVAC_MODE_HEAT,
CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO,
CONST_MODE_OFF: HVAC_MODE_OFF,
}
HVAC_MAP_TADO_COOL = {
CONST_OVERLAY_MANUAL: HVAC_MODE_COOL,
CONST_OVERLAY_TIMER: HVAC_MODE_COOL,
CONST_OVERLAY_TADO_MODE: HVAC_MODE_COOL,
CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO,
CONST_MODE_OFF: HVAC_MODE_OFF,
}
HVAC_MAP_TADO_HEAT_COOL = {
CONST_OVERLAY_MANUAL: HVAC_MODE_HEAT_COOL,
CONST_OVERLAY_TIMER: HVAC_MODE_HEAT_COOL,
CONST_OVERLAY_TADO_MODE: HVAC_MODE_HEAT_COOL,
CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO,
CONST_MODE_OFF: HVAC_MODE_OFF,
}
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
SUPPORT_HVAC_HEAT = [HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF]
SUPPORT_HVAC_COOL = [HVAC_MODE_COOL, HVAC_MODE_AUTO, HVAC_MODE_OFF]
SUPPORT_HVAC_HEAT_COOL = [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO, HVAC_MODE_OFF]
SUPPORT_FAN = [FAN_HIGH, FAN_MIDDLE, FAN_LOW, FAN_OFF]
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."""
@ -96,29 +69,80 @@ def create_climate_entity(tado, name: str, zone_id: int):
_LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities) _LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities)
zone_type = capabilities["type"] zone_type = capabilities["type"]
support_flags = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE
supported_hvac_modes = [
TADO_TO_HA_HVAC_MODE_MAP[CONST_MODE_OFF],
TADO_TO_HA_HVAC_MODE_MAP[CONST_MODE_SMART_SCHEDULE],
]
supported_fan_modes = None
heat_temperatures = None
cool_temperatures = None
ac_support_heat = False
if zone_type == TYPE_AIR_CONDITIONING: 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 # Heat is preferred as it generally has a lower minimum temperature
if "HEAT" in capabilities: for mode in ORDERED_KNOWN_TADO_MODES:
temperatures = capabilities["HEAT"]["temperatures"] if mode not in capabilities:
ac_support_heat = True continue
else:
temperatures = capabilities["COOL"]["temperatures"] supported_hvac_modes.append(TADO_TO_HA_HVAC_MODE_MAP[mode])
elif "temperatures" in capabilities: if not capabilities[mode].get("fanSpeeds"):
temperatures = capabilities["temperatures"] continue
support_flags |= SUPPORT_FAN_MODE
if supported_fan_modes:
continue
supported_fan_modes = [
TADO_TO_HA_FAN_MODE_MAP[speed]
for speed in capabilities[mode]["fanSpeeds"]
]
cool_temperatures = capabilities[CONST_MODE_COOL]["temperatures"]
else: else:
_LOGGER.debug("Not adding zone %s since it has no temperature", name) supported_hvac_modes.append(HVAC_MODE_HEAT)
if CONST_MODE_HEAT in capabilities:
heat_temperatures = capabilities[CONST_MODE_HEAT]["temperatures"]
if heat_temperatures is None and "temperatures" in capabilities:
heat_temperatures = capabilities["temperatures"]
if cool_temperatures is None and heat_temperatures is None:
_LOGGER.debug("Not adding zone %s since it has no temperatures", name)
return None return None
min_temp = float(temperatures["celsius"]["min"]) heat_min_temp = None
max_temp = float(temperatures["celsius"]["max"]) heat_max_temp = None
step = temperatures["celsius"].get("step", PRECISION_TENTHS) heat_step = None
cool_min_temp = None
cool_max_temp = None
cool_step = None
if heat_temperatures is not None:
heat_min_temp = float(heat_temperatures["celsius"]["min"])
heat_max_temp = float(heat_temperatures["celsius"]["max"])
heat_step = heat_temperatures["celsius"].get("step", PRECISION_TENTHS)
if cool_temperatures is not None:
cool_min_temp = float(cool_temperatures["celsius"]["min"])
cool_max_temp = float(cool_temperatures["celsius"]["max"])
cool_step = cool_temperatures["celsius"].get("step", PRECISION_TENTHS)
entity = TadoClimate( entity = TadoClimate(
tado, name, zone_id, zone_type, min_temp, max_temp, step, ac_support_heat, tado,
name,
zone_id,
zone_type,
heat_min_temp,
heat_max_temp,
heat_step,
cool_min_temp,
cool_max_temp,
cool_step,
supported_hvac_modes,
supported_fan_modes,
support_flags,
) )
return entity return entity
@ -132,10 +156,15 @@ class TadoClimate(ClimateDevice):
zone_name, zone_name,
zone_id, zone_id,
zone_type, zone_type,
min_temp, heat_min_temp,
max_temp, heat_max_temp,
step, heat_step,
ac_support_heat, cool_min_temp,
cool_max_temp,
cool_step,
supported_hvac_modes,
supported_fan_modes,
support_flags,
): ):
"""Initialize of Tado climate entity.""" """Initialize of Tado climate entity."""
self._tado = tado self._tado = tado
@ -146,49 +175,51 @@ class TadoClimate(ClimateDevice):
self._unique_id = f"{zone_type} {zone_id} {tado.device_id}" self._unique_id = f"{zone_type} {zone_id} {tado.device_id}"
self._ac_device = zone_type == TYPE_AIR_CONDITIONING self._ac_device = zone_type == TYPE_AIR_CONDITIONING
self._ac_support_heat = ac_support_heat self._supported_hvac_modes = supported_hvac_modes
self._cooling = False self._supported_fan_modes = supported_fan_modes
self._support_flags = support_flags
self._active = False self._available = False
self._device_is_active = False
self._cur_temp = None self._cur_temp = None
self._cur_humidity = None self._cur_humidity = None
self._is_away = False
self._min_temp = min_temp self._heat_min_temp = heat_min_temp
self._max_temp = max_temp self._heat_max_temp = heat_max_temp
self._step = step self._heat_step = heat_step
self._cool_min_temp = cool_min_temp
self._cool_max_temp = cool_max_temp
self._cool_step = cool_step
self._target_temp = None self._target_temp = None
if tado.fallback: self._current_tado_fan_speed = CONST_FAN_OFF
# Fallback to Smart Schedule at next Schedule switch self._current_tado_hvac_mode = CONST_MODE_OFF
self._default_overlay = CONST_OVERLAY_TADO_MODE self._current_tado_hvac_action = CURRENT_HVAC_OFF
else:
# Don't fallback to Smart Schedule, but keep in manual mode
self._default_overlay = CONST_OVERLAY_MANUAL
self._current_fan = CONST_MODE_OFF self._undo_dispatcher = None
self._current_operation = CONST_MODE_SMART_SCHEDULE self._tado_zone_data = None
self._overlay_mode = CONST_MODE_SMART_SCHEDULE self._async_update_zone_data()
async def async_will_remove_from_hass(self):
"""When entity will be removed from hass."""
if self._undo_dispatcher:
self._undo_dispatcher()
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register for sensor updates.""" """Register for sensor updates."""
@callback self._undo_dispatcher = async_dispatcher_connect(
def async_update_callback():
"""Schedule an entity update."""
self.async_schedule_update_ha_state(True)
async_dispatcher_connect(
self.hass, self.hass,
SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id), SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id),
async_update_callback, self._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."""
return SUPPORT_FLAGS return self._support_flags
@property @property
def name(self): def name(self):
@ -208,12 +239,12 @@ class TadoClimate(ClimateDevice):
@property @property
def current_humidity(self): def current_humidity(self):
"""Return the current humidity.""" """Return the current humidity."""
return self._cur_humidity return self._tado_zone_data.current_humidity
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the sensor temperature.""" """Return the sensor temperature."""
return self._cur_temp return self._tado_zone_data.current_temp
@property @property
def hvac_mode(self): def hvac_mode(self):
@ -221,11 +252,7 @@ class TadoClimate(ClimateDevice):
Need to be one of HVAC_MODE_*. Need to be one of HVAC_MODE_*.
""" """
if self._ac_device and self._ac_support_heat: return TADO_TO_HA_HVAC_MODE_MAP.get(self._current_tado_hvac_mode, HVAC_MODE_OFF)
return HVAC_MAP_TADO_HEAT_COOL.get(self._current_operation)
if self._ac_device and not self._ac_support_heat:
return HVAC_MAP_TADO_COOL.get(self._current_operation)
return HVAC_MAP_TADO_HEAT.get(self._current_operation)
@property @property
def hvac_modes(self): def hvac_modes(self):
@ -233,11 +260,7 @@ class TadoClimate(ClimateDevice):
Need to be a subset of HVAC_MODES. Need to be a subset of HVAC_MODES.
""" """
if self._ac_device: return self._supported_hvac_modes
if self._ac_support_heat:
return SUPPORT_HVAC_HEAT_COOL
return SUPPORT_HVAC_COOL
return SUPPORT_HVAC_HEAT
@property @property
def hvac_action(self): def hvac_action(self):
@ -245,40 +268,30 @@ class TadoClimate(ClimateDevice):
Need to be one of CURRENT_HVAC_*. Need to be one of CURRENT_HVAC_*.
""" """
if not self._device_is_active: return TADO_HVAC_ACTION_TO_HA_HVAC_ACTION.get(
return CURRENT_HVAC_OFF self._tado_zone_data.current_hvac_action, CURRENT_HVAC_OFF
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:
return CURRENT_HVAC_HEAT
return CURRENT_HVAC_IDLE
@property @property
def fan_mode(self): def fan_mode(self):
"""Return the fan setting.""" """Return the fan setting."""
if self._ac_device: if self._ac_device:
return FAN_MAP_TADO.get(self._current_fan) return TADO_TO_HA_FAN_MODE_MAP.get(self._current_tado_fan_speed, FAN_AUTO)
return None return None
@property @property
def fan_modes(self): def fan_modes(self):
"""List of available fan modes.""" """List of available fan modes."""
if self._ac_device: return self._supported_fan_modes
return SUPPORT_FAN
return None
def set_fan_mode(self, fan_mode: str): def set_fan_mode(self, fan_mode: str):
"""Turn fan on/off.""" """Turn fan on/off."""
pass self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode])
@property @property
def preset_mode(self): def preset_mode(self):
"""Return the current preset mode (home, away).""" """Return the current preset mode (home, away)."""
if self._is_away: if self._tado_zone_data.is_away:
return PRESET_AWAY return PRESET_AWAY
return PRESET_HOME return PRESET_HOME
@ -299,12 +312,18 @@ class TadoClimate(ClimateDevice):
@property @property
def target_temperature_step(self): def target_temperature_step(self):
"""Return the supported step of target temperature.""" """Return the supported step of target temperature."""
return self._step if self._tado_zone_data.current_hvac_mode == CONST_MODE_COOL:
return self._cool_step or self._heat_step
return self._heat_step or self._cool_step
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self._target_temp # If the target temperature will be None
# if the device is performing an action
# that does not affect the temperature or
# the device is switching states
return self._tado_zone_data.target_temp or self._tado_zone_data.current_temp
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
@ -312,174 +331,149 @@ class TadoClimate(ClimateDevice):
if temperature is None: if temperature is None:
return return
self._current_operation = self._default_overlay if self._current_tado_hvac_mode not in (
self._overlay_mode = None CONST_MODE_OFF,
self._target_temp = temperature CONST_MODE_AUTO,
self._control_heating() CONST_MODE_SMART_SCHEDULE,
):
self._control_hvac(target_temp=temperature)
return
new_hvac_mode = CONST_MODE_COOL if self._ac_device else CONST_MODE_HEAT
self._control_hvac(target_temp=temperature, hvac_mode=new_hvac_mode)
def set_hvac_mode(self, hvac_mode): def set_hvac_mode(self, hvac_mode):
"""Set new target hvac mode.""" """Set new target hvac mode."""
mode = None
if hvac_mode == HVAC_MODE_OFF: self._control_hvac(hvac_mode=HA_TO_TADO_HVAC_MODE_MAP[hvac_mode])
mode = CONST_MODE_OFF
elif hvac_mode == HVAC_MODE_AUTO:
mode = CONST_MODE_SMART_SCHEDULE
elif hvac_mode == HVAC_MODE_HEAT:
mode = self._default_overlay
elif hvac_mode == HVAC_MODE_COOL:
mode = self._default_overlay
elif hvac_mode == HVAC_MODE_HEAT_COOL:
mode = self._default_overlay
self._current_operation = mode @property
self._overlay_mode = None def available(self):
"""Return if the device is available."""
# Set a target temperature if we don't have any return self._tado_zone_data.available
# 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 @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
return self._min_temp if (
self._current_tado_hvac_mode == CONST_MODE_COOL
and self._cool_min_temp is not None
):
return self._cool_min_temp
if self._heat_min_temp is not None:
return self._heat_min_temp
return self._cool_min_temp
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
return self._max_temp
def update(self):
"""Handle update callbacks."""
_LOGGER.debug("Updating climate platform for zone %d", self.zone_id)
data = self._tado.data["zone"][self.zone_id]
if "sensorDataPoints" in data:
sensor_data = data["sensorDataPoints"]
if "insideTemperature" in sensor_data:
temperature = float(sensor_data["insideTemperature"]["celsius"])
self._cur_temp = temperature
if "humidity" in sensor_data:
humidity = float(sensor_data["humidity"]["percentage"])
self._cur_humidity = humidity
# temperature setting will not exist when device is off
if ( if (
"temperature" in data["setting"] self._current_tado_hvac_mode == CONST_MODE_HEAT
and data["setting"]["temperature"] is not None and self._heat_max_temp is not None
): ):
setting = float(data["setting"]["temperature"]["celsius"]) return self._heat_max_temp
self._target_temp = setting if self._heat_max_temp is not None:
return self._heat_max_temp
if "tadoMode" in data: return self._heat_max_temp
mode = data["tadoMode"]
self._is_away = mode == "AWAY"
if "setting" in data: @callback
power = data["setting"]["power"] def _async_update_zone_data(self):
if power == "OFF": """Load tado data into zone."""
self._current_operation = CONST_MODE_OFF self._tado_zone_data = self._tado.data["zone"][self.zone_id]
self._current_fan = CONST_MODE_OFF self._current_tado_fan_speed = self._tado_zone_data.current_fan_speed
# There is no overlay, the mode will always be self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode
# "SMART_SCHEDULE" self._current_tado_hvac_action = self._tado_zone_data.current_hvac_action
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
self._device_is_active = False
else:
self._device_is_active = True
active = False @callback
if "activityDataPoints" in data: def _async_update_callback(self):
activity_data = data["activityDataPoints"] """Load tado data and update state."""
if self._ac_device: self._async_update_zone_data()
if "acPower" in activity_data and activity_data["acPower"] is not None: self.async_write_ha_state()
if not activity_data["acPower"]["value"] == "OFF":
active = True
else:
if (
"heatingPower" in activity_data
and activity_data["heatingPower"] is not None
):
if float(activity_data["heatingPower"]["percentage"]) > 0.0:
active = True
self._active = active
overlay = False def _normalize_target_temp_for_hvac_mode(self):
overlay_data = None # Set a target temperature if we don't have any
termination = CONST_MODE_SMART_SCHEDULE # This can happen when we switch from Off to On
cooling = False if self._target_temp is None:
fan_speed = CONST_MODE_OFF self._target_temp = self._tado_zone_data.current_temp
elif self._current_tado_hvac_mode == CONST_MODE_COOL:
if self._target_temp > self._cool_max_temp:
self._target_temp = self._cool_max_temp
elif self._target_temp < self._cool_min_temp:
self._target_temp = self._cool_min_temp
elif self._current_tado_hvac_mode == CONST_MODE_HEAT:
if self._target_temp > self._heat_max_temp:
self._target_temp = self._heat_max_temp
elif self._target_temp < self._heat_min_temp:
self._target_temp = self._heat_min_temp
if "overlay" in data: def _control_hvac(self, hvac_mode=None, target_temp=None, fan_mode=None):
overlay_data = data["overlay"]
overlay = overlay_data is not None
if overlay:
termination = overlay_data["termination"]["type"]
setting = False
setting_data = None
if "setting" in overlay_data:
setting_data = overlay_data["setting"]
setting = setting_data is not None
if setting:
if "mode" in setting_data:
cooling = setting_data["mode"] == "COOL"
if "fanSpeed" in setting_data:
fan_speed = setting_data["fanSpeed"]
if self._device_is_active:
# If you set mode manually to off, there will be an overlay
# and a termination, but we want to see the mode "OFF"
self._overlay_mode = termination
self._current_operation = termination
self._cooling = cooling
self._current_fan = fan_speed
def _control_heating(self):
"""Send new target temperature to Tado.""" """Send new target temperature to Tado."""
if self._current_operation == CONST_MODE_SMART_SCHEDULE:
if hvac_mode:
self._current_tado_hvac_mode = hvac_mode
if target_temp:
self._target_temp = target_temp
if fan_mode:
self._current_tado_fan_speed = fan_mode
self._normalize_target_temp_for_hvac_mode()
# tado does not permit setting the fan speed to
# off, you must turn off the device
if (
self._current_tado_fan_speed == CONST_FAN_OFF
and self._current_tado_hvac_mode != CONST_MODE_OFF
):
self._current_tado_fan_speed = CONST_FAN_AUTO
if self._current_tado_hvac_mode == CONST_MODE_OFF:
_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)
return
if self._current_tado_hvac_mode == CONST_MODE_SMART_SCHEDULE:
_LOGGER.debug( _LOGGER.debug(
"Switching to SMART_SCHEDULE 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._tado.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:
_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 return
_LOGGER.debug( _LOGGER.debug(
"Switching to %s for zone %s (%d) with temperature %s °C", "Switching to %s for zone %s (%d) with temperature %s °C",
self._current_operation, self._current_tado_hvac_mode,
self.zone_name, self.zone_name,
self.zone_id, self.zone_id,
self._target_temp, self._target_temp,
) )
self._tado.set_zone_overlay(
self.zone_id, # Fallback to Smart Schedule at next Schedule switch if we have fallback enabled
self._current_operation, overlay_mode = (
self._target_temp, CONST_OVERLAY_TADO_MODE if self._tado.fallback else CONST_OVERLAY_MANUAL
None, )
self.zone_type,
"COOL" if self._ac_device else None, temperature_to_send = self._target_temp
if self._current_tado_hvac_mode in TADO_MODES_WITH_NO_TEMP_SETTING:
# A temperature cannot be passed with these modes
temperature_to_send = None
self._tado.set_zone_overlay(
zone_id=self.zone_id,
overlay_mode=overlay_mode, # What to do when the period ends
temperature=temperature_to_send,
duration=None,
device_type=self.zone_type,
mode=self._current_tado_hvac_mode,
fan_speed=(
self._current_tado_fan_speed
if (self._support_flags & SUPPORT_FAN_MODE)
else None
), # api defaults to not sending fanSpeed if not specified
) )
self._overlay_mode = self._current_operation

View File

@ -1,5 +1,48 @@
"""Constant values for the Tado component.""" """Constant values for the Tado component."""
from PyTado.const import (
CONST_HVAC_COOL,
CONST_HVAC_DRY,
CONST_HVAC_FAN,
CONST_HVAC_HEAT,
CONST_HVAC_HOT_WATER,
CONST_HVAC_IDLE,
CONST_HVAC_OFF,
)
from homeassistant.components.climate.const import (
CURRENT_HVAC_COOL,
CURRENT_HVAC_DRY,
CURRENT_HVAC_FAN,
CURRENT_HVAC_HEAT,
CURRENT_HVAC_IDLE,
CURRENT_HVAC_OFF,
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
FAN_OFF,
HVAC_MODE_AUTO,
HVAC_MODE_COOL,
HVAC_MODE_DRY,
HVAC_MODE_FAN_ONLY,
HVAC_MODE_HEAT,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_OFF,
PRESET_AWAY,
PRESET_HOME,
)
TADO_HVAC_ACTION_TO_HA_HVAC_ACTION = {
CONST_HVAC_HEAT: CURRENT_HVAC_HEAT,
CONST_HVAC_DRY: CURRENT_HVAC_DRY,
CONST_HVAC_FAN: CURRENT_HVAC_FAN,
CONST_HVAC_COOL: CURRENT_HVAC_COOL,
CONST_HVAC_IDLE: CURRENT_HVAC_IDLE,
CONST_HVAC_OFF: CURRENT_HVAC_OFF,
CONST_HVAC_HOT_WATER: CURRENT_HVAC_HEAT,
}
# Configuration # Configuration
CONF_FALLBACK = "fallback" CONF_FALLBACK = "fallback"
DATA = "data" DATA = "data"
@ -10,10 +53,81 @@ TYPE_HEATING = "HEATING"
TYPE_HOT_WATER = "HOT_WATER" TYPE_HOT_WATER = "HOT_WATER"
# Base modes # Base modes
CONST_MODE_OFF = "OFF"
CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Use the schedule CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Use the schedule
CONST_MODE_OFF = "OFF" # Switch off heating in a zone CONST_MODE_AUTO = "AUTO"
CONST_MODE_COOL = "COOL"
CONST_MODE_HEAT = "HEAT"
CONST_MODE_DRY = "DRY"
CONST_MODE_FAN = "FAN"
CONST_LINK_OFFLINE = "OFFLINE"
CONST_FAN_OFF = "OFF"
CONST_FAN_AUTO = "AUTO"
CONST_FAN_LOW = "LOW"
CONST_FAN_MIDDLE = "MIDDLE"
CONST_FAN_HIGH = "HIGH"
# When we change the temperature setting, we need an overlay mode # 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_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_MANUAL = "MANUAL" # the user has change the temperature or mode manually
CONST_OVERLAY_TIMER = "TIMER" # the temperature will be reset after a timespan CONST_OVERLAY_TIMER = "TIMER" # the temperature will be reset after a timespan
# Heat always comes first since we get the
# min and max tempatures for the zone from
# it.
# Heat is preferred as it generally has a lower minimum temperature
ORDERED_KNOWN_TADO_MODES = [
CONST_MODE_HEAT,
CONST_MODE_COOL,
CONST_MODE_AUTO,
CONST_MODE_DRY,
CONST_MODE_FAN,
]
TADO_MODES_TO_HA_CURRENT_HVAC_ACTION = {
CONST_MODE_HEAT: CURRENT_HVAC_HEAT,
CONST_MODE_DRY: CURRENT_HVAC_DRY,
CONST_MODE_FAN: CURRENT_HVAC_FAN,
CONST_MODE_COOL: CURRENT_HVAC_COOL,
}
# These modes will not allow a temp to be set
TADO_MODES_WITH_NO_TEMP_SETTING = [CONST_MODE_AUTO, CONST_MODE_DRY, CONST_MODE_FAN]
#
# HVAC_MODE_HEAT_COOL is mapped to CONST_MODE_AUTO
# This lets tado decide on a temp
#
# HVAC_MODE_AUTO is mapped to CONST_MODE_SMART_SCHEDULE
# This runs the smart schedule
#
HA_TO_TADO_HVAC_MODE_MAP = {
HVAC_MODE_OFF: CONST_MODE_OFF,
HVAC_MODE_HEAT_COOL: CONST_MODE_AUTO,
HVAC_MODE_AUTO: CONST_MODE_SMART_SCHEDULE,
HVAC_MODE_HEAT: CONST_MODE_HEAT,
HVAC_MODE_COOL: CONST_MODE_COOL,
HVAC_MODE_DRY: CONST_MODE_DRY,
HVAC_MODE_FAN_ONLY: CONST_MODE_FAN,
}
HA_TO_TADO_FAN_MODE_MAP = {
FAN_AUTO: CONST_FAN_AUTO,
FAN_OFF: CONST_FAN_OFF,
FAN_LOW: CONST_FAN_LOW,
FAN_MEDIUM: CONST_FAN_MIDDLE,
FAN_HIGH: CONST_FAN_HIGH,
}
TADO_TO_HA_HVAC_MODE_MAP = {
value: key for key, value in HA_TO_TADO_HVAC_MODE_MAP.items()
}
TADO_TO_HA_FAN_MODE_MAP = {value: key for key, value in HA_TO_TADO_FAN_MODE_MAP.items()}
DEFAULT_TADO_PRECISION = 0.1
SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME]

View File

@ -7,6 +7,6 @@
], ],
"dependencies": [], "dependencies": [],
"codeowners": [ "codeowners": [
"@michaelarnauts" "@michaelarnauts", "@bdraco"
] ]
} }

View File

@ -31,6 +31,7 @@ ZONE_SENSORS = {
"ac", "ac",
"tado mode", "tado mode",
"overlay", "overlay",
"open window",
], ],
TYPE_HOT_WATER: ["power", "link", "tado mode", "overlay"], TYPE_HOT_WATER: ["power", "link", "tado mode", "overlay"],
} }
@ -46,20 +47,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for tado in api_list: for tado in api_list:
# Create zone sensors # Create zone sensors
zones = tado.zones
devices = tado.devices
for zone in zones:
zone_type = zone["type"]
if zone_type not in ZONE_SENSORS:
_LOGGER.warning("Unknown zone type skipped: %s", zone_type)
continue
for zone in tado.zones:
entities.extend( entities.extend(
[ [
create_zone_sensor(tado, zone["name"], zone["id"], variable) TadoZoneSensor(tado, zone["name"], zone["id"], variable)
for variable in ZONE_SENSORS.get(zone["type"]) for variable in ZONE_SENSORS[zone_type]
] ]
) )
# Create device sensors # Create device sensors
for home in tado.devices: for device in devices:
entities.extend( entities.extend(
[ [
create_device_sensor(tado, home["name"], home["id"], variable) TadoDeviceSensor(tado, device["name"], device["id"], variable)
for variable in DEVICE_SENSORS for variable in DEVICE_SENSORS
] ]
) )
@ -67,46 +75,38 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(entities, True) add_entities(entities, True)
def create_zone_sensor(tado, name, zone_id, variable): class TadoZoneSensor(Entity):
"""Create a zone sensor."""
return TadoSensor(tado, name, "zone", zone_id, variable)
def create_device_sensor(tado, name, device_id, variable):
"""Create a device sensor."""
return TadoSensor(tado, name, "device", device_id, variable)
class TadoSensor(Entity):
"""Representation of a tado Sensor.""" """Representation of a tado Sensor."""
def __init__(self, tado, zone_name, sensor_type, zone_id, zone_variable): def __init__(self, tado, zone_name, zone_id, zone_variable):
"""Initialize of the Tado Sensor.""" """Initialize of the Tado Sensor."""
self._tado = tado 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} {tado.device_id}" self._unique_id = f"{zone_variable} {zone_id} {tado.device_id}"
self._state = None self._state = None
self._state_attributes = None self._state_attributes = None
self._tado_zone_data = None
self._undo_dispatcher = None
async def async_will_remove_from_hass(self):
"""When entity will be removed from hass."""
if self._undo_dispatcher:
self._undo_dispatcher()
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register for sensor updates.""" """Register for sensor updates."""
@callback self._undo_dispatcher = async_dispatcher_connect(
def async_update_callback():
"""Schedule an entity update."""
self.async_schedule_update_ha_state(True)
async_dispatcher_connect(
self.hass, self.hass,
SIGNAL_TADO_UPDATE_RECEIVED.format(self.sensor_type, self.zone_id), SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id),
async_update_callback, self._async_update_callback,
) )
self._async_update_zone_data()
@property @property
def unique_id(self): def unique_id(self):
@ -138,7 +138,7 @@ class TadoSensor(Entity):
if self.zone_variable == "heating": if self.zone_variable == "heating":
return UNIT_PERCENTAGE return UNIT_PERCENTAGE
if self.zone_variable == "ac": if self.zone_variable == "ac":
return "" return None
@property @property
def icon(self): def icon(self):
@ -149,97 +149,143 @@ class TadoSensor(Entity):
return "mdi:water-percent" return "mdi:water-percent"
@property @property
def should_poll(self) -> bool: def should_poll(self):
"""Do not poll.""" """Do not poll."""
return False return False
def update(self): @callback
def _async_update_callback(self):
"""Update and write state."""
self._async_update_zone_data()
self.async_write_ha_state()
@callback
def _async_update_zone_data(self):
"""Handle update callbacks.""" """Handle update callbacks."""
try: try:
data = self._tado.data[self.sensor_type][self.zone_id] self._tado_zone_data = self._tado.data["zone"][self.zone_id]
except KeyError: except KeyError:
return return
unit = TEMP_CELSIUS
if self.zone_variable == "temperature": if self.zone_variable == "temperature":
if "sensorDataPoints" in data: self._state = self.hass.config.units.temperature(
sensor_data = data["sensorDataPoints"] self._tado_zone_data.current_temp, TEMP_CELSIUS
temperature = float(sensor_data["insideTemperature"]["celsius"]) )
self._state_attributes = {
self._state = self.hass.config.units.temperature(temperature, unit) "time": self._tado_zone_data.current_temp_timestamp,
self._state_attributes = { "setting": 0, # setting is used in climate device
"time": sensor_data["insideTemperature"]["timestamp"], }
"setting": 0, # setting is used in climate device
}
# temperature setting will not exist when device is off
if (
"temperature" in data["setting"]
and data["setting"]["temperature"] is not None
):
temperature = float(data["setting"]["temperature"]["celsius"])
self._state_attributes[
"setting"
] = self.hass.config.units.temperature(temperature, unit)
elif self.zone_variable == "humidity": elif self.zone_variable == "humidity":
if "sensorDataPoints" in data: self._state = self._tado_zone_data.current_humidity
sensor_data = data["sensorDataPoints"] self._state_attributes = {
self._state = float(sensor_data["humidity"]["percentage"]) "time": self._tado_zone_data.current_humidity_timestamp
self._state_attributes = {"time": sensor_data["humidity"]["timestamp"]} }
elif self.zone_variable == "power": elif self.zone_variable == "power":
if "setting" in data: self._state = self._tado_zone_data.power
self._state = data["setting"]["power"]
elif self.zone_variable == "link": elif self.zone_variable == "link":
if "link" in data: self._state = self._tado_zone_data.link
self._state = data["link"]["state"]
elif self.zone_variable == "heating": elif self.zone_variable == "heating":
if "activityDataPoints" in data: self._state = self._tado_zone_data.heating_power_percentage
activity_data = data["activityDataPoints"] self._state_attributes = {
"time": self._tado_zone_data.heating_power_timestamp
if ( }
"heatingPower" in activity_data
and activity_data["heatingPower"] is not None
):
self._state = float(activity_data["heatingPower"]["percentage"])
self._state_attributes = {
"time": activity_data["heatingPower"]["timestamp"]
}
elif self.zone_variable == "ac": elif self.zone_variable == "ac":
if "activityDataPoints" in data: self._state = self._tado_zone_data.ac_power
activity_data = data["activityDataPoints"] self._state_attributes = {"time": self._tado_zone_data.ac_power_timestamp}
if "acPower" in activity_data and activity_data["acPower"] is not None:
self._state = activity_data["acPower"]["value"]
self._state_attributes = {
"time": activity_data["acPower"]["timestamp"]
}
elif self.zone_variable == "tado bridge status": elif self.zone_variable == "tado bridge status":
if "connectionState" in data: self._state = self._tado_zone_data.connection
self._state = data["connectionState"]["value"]
elif self.zone_variable == "tado mode": elif self.zone_variable == "tado mode":
if "tadoMode" in data: self._state = self._tado_zone_data.tado_mode
self._state = data["tadoMode"]
elif self.zone_variable == "overlay": elif self.zone_variable == "overlay":
self._state = "overlay" in data and data["overlay"] is not None self._state = self._tado_zone_data.overlay_active
self._state_attributes = ( self._state_attributes = (
{"termination": data["overlay"]["termination"]["type"]} {"termination": self._tado_zone_data.overlay_termination_type}
if self._state if self._tado_zone_data.overlay_active
else {} else {}
) )
elif self.zone_variable == "early start": elif self.zone_variable == "early start":
self._state = "preparation" in data and data["preparation"] is not None self._state = self._tado_zone_data.preparation
elif self.zone_variable == "open window": elif self.zone_variable == "open window":
self._state = "openWindow" in data and data["openWindow"] is not None self._state = self._tado_zone_data.open_window
self._state_attributes = data["openWindow"] if self._state else {} self._state_attributes = self._tado_zone_data.open_window_attr
class TadoDeviceSensor(Entity):
"""Representation of a tado Sensor."""
def __init__(self, tado, device_name, device_id, device_variable):
"""Initialize of the Tado Sensor."""
self._tado = tado
self.device_name = device_name
self.device_id = device_id
self.device_variable = device_variable
self._unique_id = f"{device_variable} {device_id} {tado.device_id}"
self._state = None
self._state_attributes = None
self._tado_device_data = None
self._undo_dispatcher = None
async def async_will_remove_from_hass(self):
"""When entity will be removed from hass."""
if self._undo_dispatcher:
self._undo_dispatcher()
async def async_added_to_hass(self):
"""Register for sensor updates."""
self._undo_dispatcher = async_dispatcher_connect(
self.hass,
SIGNAL_TADO_UPDATE_RECEIVED.format("device", self.device_id),
self._async_update_callback,
)
self._async_update_device_data()
@property
def unique_id(self):
"""Return the unique id."""
return self._unique_id
@property
def name(self):
"""Return the name of the sensor."""
return f"{self.device_name} {self.device_variable}"
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def should_poll(self):
"""Do not poll."""
return False
@callback
def _async_update_callback(self):
"""Update and write state."""
self._async_update_device_data()
self.async_write_ha_state()
@callback
def _async_update_device_data(self):
"""Handle update callbacks."""
try:
data = self._tado.data["device"][self.device_id]
except KeyError:
return
if self.device_variable == "tado bridge status":
self._state = data.get("connectionState", {}).get("value", False)

View File

@ -12,6 +12,9 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED
from .const import ( from .const import (
CONST_HVAC_HEAT,
CONST_MODE_AUTO,
CONST_MODE_HEAT,
CONST_MODE_OFF, CONST_MODE_OFF,
CONST_MODE_SMART_SCHEDULE, CONST_MODE_SMART_SCHEDULE,
CONST_OVERLAY_MANUAL, CONST_OVERLAY_MANUAL,
@ -33,6 +36,7 @@ WATER_HEATER_MAP_TADO = {
CONST_OVERLAY_MANUAL: MODE_HEAT, CONST_OVERLAY_MANUAL: MODE_HEAT,
CONST_OVERLAY_TIMER: MODE_HEAT, CONST_OVERLAY_TIMER: MODE_HEAT,
CONST_OVERLAY_TADO_MODE: MODE_HEAT, CONST_OVERLAY_TADO_MODE: MODE_HEAT,
CONST_HVAC_HEAT: MODE_HEAT,
CONST_MODE_SMART_SCHEDULE: MODE_AUTO, CONST_MODE_SMART_SCHEDULE: MODE_AUTO,
CONST_MODE_OFF: MODE_OFF, CONST_MODE_OFF: MODE_OFF,
} }
@ -50,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for tado in api_list: for tado in api_list:
for zone in tado.zones: for zone in tado.zones:
if zone["type"] in [TYPE_HOT_WATER]: if zone["type"] == TYPE_HOT_WATER:
entity = create_water_heater_entity(tado, zone["name"], zone["id"]) entity = create_water_heater_entity(tado, zone["name"], zone["id"])
entities.append(entity) entities.append(entity)
@ -61,6 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
def create_water_heater_entity(tado, name: str, zone_id: int): def create_water_heater_entity(tado, name: str, zone_id: int):
"""Create a Tado water heater device.""" """Create a Tado water heater device."""
capabilities = tado.get_capabilities(zone_id) capabilities = tado.get_capabilities(zone_id)
supports_temperature_control = capabilities["canSetTemperature"] supports_temperature_control = capabilities["canSetTemperature"]
if supports_temperature_control and "temperatures" in capabilities: if supports_temperature_control and "temperatures" in capabilities:
@ -98,7 +103,6 @@ class TadoWaterHeater(WaterHeaterDevice):
self._unique_id = f"{zone_id} {tado.device_id}" self._unique_id = f"{zone_id} {tado.device_id}"
self._device_is_active = False self._device_is_active = False
self._is_away = False
self._supports_temperature_control = supports_temperature_control self._supports_temperature_control = supports_temperature_control
self._min_temperature = min_temp self._min_temperature = min_temp
@ -110,29 +114,25 @@ class TadoWaterHeater(WaterHeaterDevice):
if self._supports_temperature_control: if self._supports_temperature_control:
self._supported_features |= SUPPORT_TARGET_TEMPERATURE self._supported_features |= SUPPORT_TARGET_TEMPERATURE
if tado.fallback: self._current_tado_hvac_mode = CONST_MODE_SMART_SCHEDULE
# Fallback to Smart Schedule at next Schedule switch
self._default_overlay = CONST_OVERLAY_TADO_MODE
else:
# Don't fallback to Smart Schedule, but keep in manual mode
self._default_overlay = CONST_OVERLAY_MANUAL
self._current_operation = CONST_MODE_SMART_SCHEDULE
self._overlay_mode = CONST_MODE_SMART_SCHEDULE self._overlay_mode = CONST_MODE_SMART_SCHEDULE
self._tado_zone_data = None
self._undo_dispatcher = None
async def async_will_remove_from_hass(self):
"""When entity will be removed from hass."""
if self._undo_dispatcher:
self._undo_dispatcher()
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register for sensor updates.""" """Register for sensor updates."""
@callback self._undo_dispatcher = async_dispatcher_connect(
def async_update_callback():
"""Schedule an entity update."""
self.async_schedule_update_ha_state(True)
async_dispatcher_connect(
self.hass, self.hass,
SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id), SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id),
async_update_callback, self._async_update_callback,
) )
self._async_update_data()
@property @property
def supported_features(self): def supported_features(self):
@ -157,17 +157,17 @@ class TadoWaterHeater(WaterHeaterDevice):
@property @property
def current_operation(self): def current_operation(self):
"""Return current readable operation mode.""" """Return current readable operation mode."""
return WATER_HEATER_MAP_TADO.get(self._current_operation) return WATER_HEATER_MAP_TADO.get(self._current_tado_hvac_mode)
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self._target_temp return self._tado_zone_data.target_temp
@property @property
def is_away_mode_on(self): def is_away_mode_on(self):
"""Return true if away mode is on.""" """Return true if away mode is on."""
return self._is_away return self._tado_zone_data.is_away
@property @property
def operation_list(self): def operation_list(self):
@ -198,16 +198,9 @@ class TadoWaterHeater(WaterHeaterDevice):
elif operation_mode == MODE_AUTO: elif operation_mode == MODE_AUTO:
mode = CONST_MODE_SMART_SCHEDULE mode = CONST_MODE_SMART_SCHEDULE
elif operation_mode == MODE_HEAT: elif operation_mode == MODE_HEAT:
mode = self._default_overlay mode = CONST_MODE_HEAT
self._current_operation = mode self._control_heater(hvac_mode=mode)
self._overlay_mode = None
# Set a target temperature if we don't have any
if mode == CONST_OVERLAY_TADO_MODE and self._target_temp is None:
self._target_temp = self.min_temp
self._control_heater()
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
@ -215,88 +208,75 @@ class TadoWaterHeater(WaterHeaterDevice):
if not self._supports_temperature_control or temperature is None: if not self._supports_temperature_control or temperature is None:
return return
self._current_operation = self._default_overlay if self._current_tado_hvac_mode not in (
self._overlay_mode = None CONST_MODE_OFF,
self._target_temp = temperature CONST_MODE_AUTO,
self._control_heater() CONST_MODE_SMART_SCHEDULE,
def update(self):
"""Handle update callbacks."""
_LOGGER.debug("Updating water_heater platform for zone %d", self.zone_id)
data = self._tado.data["zone"][self.zone_id]
if "tadoMode" in data:
mode = data["tadoMode"]
self._is_away = mode == "AWAY"
if "setting" in data:
power = data["setting"]["power"]
if power == "OFF":
self._current_operation = CONST_MODE_OFF
# There is no overlay, the mode will always be
# "SMART_SCHEDULE"
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
self._device_is_active = False
else:
self._device_is_active = True
# temperature setting will not exist when device is off
if (
"temperature" in data["setting"]
and data["setting"]["temperature"] is not None
): ):
setting = float(data["setting"]["temperature"]["celsius"]) self._control_heater(target_temp=temperature)
self._target_temp = setting return
overlay = False self._control_heater(target_temp=temperature, hvac_mode=CONST_MODE_HEAT)
overlay_data = None
termination = CONST_MODE_SMART_SCHEDULE
if "overlay" in data: @callback
overlay_data = data["overlay"] def _async_update_callback(self):
overlay = overlay_data is not None """Load tado data and update state."""
self._async_update_data()
self.async_write_ha_state()
if overlay: @callback
termination = overlay_data["termination"]["type"] def _async_update_data(self):
"""Load tado data."""
_LOGGER.debug("Updating water_heater platform for zone %d", self.zone_id)
self._tado_zone_data = self._tado.data["zone"][self.zone_id]
self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode
if self._device_is_active: def _control_heater(self, hvac_mode=None, target_temp=None):
# If you set mode manually to off, there will be an overlay
# and a termination, but we want to see the mode "OFF"
self._overlay_mode = termination
self._current_operation = termination
def _control_heater(self):
"""Send new target temperature.""" """Send new target temperature."""
if self._current_operation == CONST_MODE_SMART_SCHEDULE:
if hvac_mode:
self._current_tado_hvac_mode = hvac_mode
if target_temp:
self._target_temp = target_temp
# Set a target temperature if we don't have any
if self._target_temp is None:
self._target_temp = self.min_temp
if self._current_tado_hvac_mode == CONST_MODE_SMART_SCHEDULE:
_LOGGER.debug( _LOGGER.debug(
"Switching to SMART_SCHEDULE 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._tado.reset_zone_overlay(self.zone_id) self._tado.reset_zone_overlay(self.zone_id)
self._overlay_mode = self._current_operation
return return
if self._current_operation == CONST_MODE_OFF: if self._current_tado_hvac_mode == CONST_MODE_OFF:
_LOGGER.debug( _LOGGER.debug(
"Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id "Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id
) )
self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, TYPE_HOT_WATER) self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, TYPE_HOT_WATER)
self._overlay_mode = self._current_operation
return return
# Fallback to Smart Schedule at next Schedule switch if we have fallback enabled
overlay_mode = (
CONST_OVERLAY_TADO_MODE if self._tado.fallback else CONST_OVERLAY_MANUAL
)
_LOGGER.debug( _LOGGER.debug(
"Switching to %s for zone %s (%d) with temperature %s", "Switching to %s for zone %s (%d) with temperature %s",
self._current_operation, self._current_tado_hvac_mode,
self.zone_name, self.zone_name,
self.zone_id, self.zone_id,
self._target_temp, self._target_temp,
) )
self._tado.set_zone_overlay( self._tado.set_zone_overlay(
self.zone_id, zone_id=self.zone_id,
self._current_operation, overlay_mode=overlay_mode,
self._target_temp, temperature=self._target_temp,
None, duration=None,
TYPE_HOT_WATER, device_type=TYPE_HOT_WATER,
) )
self._overlay_mode = self._current_operation self._overlay_mode = self._current_tado_hvac_mode

View File

@ -616,6 +616,9 @@ python-miio==0.4.8
# homeassistant.components.nest # homeassistant.components.nest
python-nest==4.1.0 python-nest==4.1.0
# homeassistant.components.tado
python-tado==0.5.0
# homeassistant.components.twitch # homeassistant.components.twitch
python-twitch-client==0.6.0 python-twitch-client==0.6.0

View File

@ -0,0 +1 @@
"""Tests for the tado integration."""

View File

@ -0,0 +1,59 @@
"""The sensor tests for the tado platform."""
from .util import async_init_integration
async def test_air_con(hass):
"""Test creation of aircon climate."""
await async_init_integration(hass)
state = hass.states.get("climate.air_conditioning")
assert state.state == "cool"
expected_attributes = {
"current_humidity": 60.9,
"current_temperature": 24.8,
"fan_mode": "auto",
"fan_modes": ["auto", "high", "medium", "low"],
"friendly_name": "Air Conditioning",
"hvac_action": "cooling",
"hvac_modes": ["off", "auto", "heat", "cool", "heat_cool", "dry", "fan_only"],
"max_temp": 31.0,
"min_temp": 16.0,
"preset_mode": "home",
"preset_modes": ["away", "home"],
"supported_features": 25,
"target_temp_step": 1,
"temperature": 17.8,
}
# Only test for a subset of attributes in case
# HA changes the implementation and a new one appears
assert all(item in state.attributes.items() for item in expected_attributes.items())
async def test_heater(hass):
"""Test creation of heater climate."""
await async_init_integration(hass)
state = hass.states.get("climate.baseboard_heater")
assert state.state == "heat"
expected_attributes = {
"current_humidity": 45.2,
"current_temperature": 20.6,
"friendly_name": "Baseboard Heater",
"hvac_action": "idle",
"hvac_modes": ["off", "auto", "heat"],
"max_temp": 31.0,
"min_temp": 16.0,
"preset_mode": "home",
"preset_modes": ["away", "home"],
"supported_features": 17,
"target_temp_step": 1,
"temperature": 20.5,
}
# Only test for a subset of attributes in case
# HA changes the implementation and a new one appears
assert all(item in state.attributes.items() for item in expected_attributes.items())

View File

@ -0,0 +1,96 @@
"""The sensor tests for the tado platform."""
from .util import async_init_integration
async def test_air_con_create_sensors(hass):
"""Test creation of aircon sensors."""
await async_init_integration(hass)
state = hass.states.get("sensor.air_conditioning_power")
assert state.state == "ON"
state = hass.states.get("sensor.air_conditioning_link")
assert state.state == "ONLINE"
state = hass.states.get("sensor.air_conditioning_link")
assert state.state == "ONLINE"
state = hass.states.get("sensor.air_conditioning_tado_mode")
assert state.state == "HOME"
state = hass.states.get("sensor.air_conditioning_temperature")
assert state.state == "24.76"
state = hass.states.get("sensor.air_conditioning_ac")
assert state.state == "ON"
state = hass.states.get("sensor.air_conditioning_overlay")
assert state.state == "True"
state = hass.states.get("sensor.air_conditioning_humidity")
assert state.state == "60.9"
state = hass.states.get("sensor.air_conditioning_open_window")
assert state.state == "False"
async def test_heater_create_sensors(hass):
"""Test creation of heater sensors."""
await async_init_integration(hass)
state = hass.states.get("sensor.baseboard_heater_power")
assert state.state == "ON"
state = hass.states.get("sensor.baseboard_heater_link")
assert state.state == "ONLINE"
state = hass.states.get("sensor.baseboard_heater_link")
assert state.state == "ONLINE"
state = hass.states.get("sensor.baseboard_heater_tado_mode")
assert state.state == "HOME"
state = hass.states.get("sensor.baseboard_heater_temperature")
assert state.state == "20.65"
state = hass.states.get("sensor.baseboard_heater_early_start")
assert state.state == "False"
state = hass.states.get("sensor.baseboard_heater_overlay")
assert state.state == "True"
state = hass.states.get("sensor.baseboard_heater_humidity")
assert state.state == "45.2"
state = hass.states.get("sensor.baseboard_heater_open_window")
assert state.state == "False"
async def test_water_heater_create_sensors(hass):
"""Test creation of water heater sensors."""
await async_init_integration(hass)
state = hass.states.get("sensor.water_heater_tado_mode")
assert state.state == "HOME"
state = hass.states.get("sensor.water_heater_link")
assert state.state == "ONLINE"
state = hass.states.get("sensor.water_heater_overlay")
assert state.state == "False"
state = hass.states.get("sensor.water_heater_power")
assert state.state == "ON"
async def test_home_create_sensors(hass):
"""Test creation of home sensors."""
await async_init_integration(hass)
state = hass.states.get("sensor.home_name_tado_bridge_status")
assert state.state == "True"

View File

@ -0,0 +1,47 @@
"""The sensor tests for the tado platform."""
from .util import async_init_integration
async def test_water_heater_create_sensors(hass):
"""Test creation of water heater."""
await async_init_integration(hass)
state = hass.states.get("water_heater.water_heater")
assert state.state == "auto"
expected_attributes = {
"current_temperature": None,
"friendly_name": "Water Heater",
"max_temp": 31.0,
"min_temp": 16.0,
"operation_list": ["auto", "heat", "off"],
"operation_mode": "auto",
"supported_features": 3,
"target_temp_high": None,
"target_temp_low": None,
"temperature": 65.0,
}
# Only test for a subset of attributes in case
# HA changes the implementation and a new one appears
assert all(item in state.attributes.items() for item in expected_attributes.items())
state = hass.states.get("water_heater.second_water_heater")
assert state.state == "heat"
expected_attributes = {
"current_temperature": None,
"friendly_name": "Second Water Heater",
"max_temp": 31.0,
"min_temp": 16.0,
"operation_list": ["auto", "heat", "off"],
"operation_mode": "heat",
"supported_features": 3,
"target_temp_high": None,
"target_temp_low": None,
"temperature": 30.0,
}
# Only test for a subset of attributes in case
# HA changes the implementation and a new one appears
assert all(item in state.attributes.items() for item in expected_attributes.items())

View File

@ -0,0 +1,86 @@
"""Tests for the tado integration."""
import requests_mock
from homeassistant.components.tado import DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import load_fixture
async def async_init_integration(
hass: HomeAssistant, skip_setup: bool = False,
):
"""Set up the tado integration in Home Assistant."""
token_fixture = "tado/token.json"
devices_fixture = "tado/devices.json"
me_fixture = "tado/me.json"
zones_fixture = "tado/zones.json"
# Water Heater 2
zone_4_state_fixture = "tado/tadov2.water_heater.heating.json"
zone_4_capabilities_fixture = "tado/water_heater_zone_capabilities.json"
# Smart AC
zone_3_state_fixture = "tado/smartac3.cool_mode.json"
zone_3_capabilities_fixture = "tado/zone_capabilities.json"
# Water Heater
zone_2_state_fixture = "tado/tadov2.water_heater.auto_mode.json"
zone_2_capabilities_fixture = "tado/water_heater_zone_capabilities.json"
zone_1_state_fixture = "tado/tadov2.heating.manual_mode.json"
zone_1_capabilities_fixture = "tado/tadov2.zone_capabilities.json"
with requests_mock.mock() as m:
m.post("https://auth.tado.com/oauth/token", text=load_fixture(token_fixture))
m.get(
"https://my.tado.com/api/v2/me", text=load_fixture(me_fixture),
)
m.get(
"https://my.tado.com/api/v2/homes/1/devices",
text=load_fixture(devices_fixture),
)
m.get(
"https://my.tado.com/api/v2/homes/1/zones",
text=load_fixture(zones_fixture),
)
m.get(
"https://my.tado.com/api/v2/homes/1/zones/4/capabilities",
text=load_fixture(zone_4_capabilities_fixture),
)
m.get(
"https://my.tado.com/api/v2/homes/1/zones/3/capabilities",
text=load_fixture(zone_3_capabilities_fixture),
)
m.get(
"https://my.tado.com/api/v2/homes/1/zones/2/capabilities",
text=load_fixture(zone_2_capabilities_fixture),
)
m.get(
"https://my.tado.com/api/v2/homes/1/zones/1/capabilities",
text=load_fixture(zone_1_capabilities_fixture),
)
m.get(
"https://my.tado.com/api/v2/homes/1/zones/4/state",
text=load_fixture(zone_4_state_fixture),
)
m.get(
"https://my.tado.com/api/v2/homes/1/zones/3/state",
text=load_fixture(zone_3_state_fixture),
)
m.get(
"https://my.tado.com/api/v2/homes/1/zones/2/state",
text=load_fixture(zone_2_state_fixture),
)
m.get(
"https://my.tado.com/api/v2/homes/1/zones/1/state",
text=load_fixture(zone_1_state_fixture),
)
if not skip_setup:
assert await async_setup_component(
hass, DOMAIN, {DOMAIN: {CONF_USERNAME: "mock", CONF_PASSWORD: "mock"}}
)
await hass.async_block_till_done()

View File

@ -0,0 +1,60 @@
{
"tadoMode": "HOME",
"sensorDataPoints": {
"insideTemperature": {
"fahrenheit": 71.28,
"timestamp": "2020-02-29T22:51:05.016Z",
"celsius": 21.82,
"type": "TEMPERATURE",
"precision": {
"fahrenheit": 0.1,
"celsius": 0.1
}
},
"humidity": {
"timestamp": "2020-02-29T22:51:05.016Z",
"percentage": 40.4,
"type": "PERCENTAGE"
}
},
"link": {
"state": "ONLINE"
},
"openWindow": null,
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"overlay": null,
"activityDataPoints": {
"acPower": {
"timestamp": "2020-02-29T22:50:34.850Z",
"type": "POWER",
"value": "ON"
}
},
"nextTimeBlock": {
"start": "2020-03-01T00:00:00.000Z"
},
"preparation": null,
"overlayType": null,
"nextScheduleChange": {
"start": "2020-03-01T00:00:00Z",
"setting": {
"type": "AIR_CONDITIONING",
"mode": "HEAT",
"power": "ON",
"temperature": {
"fahrenheit": 59.0,
"celsius": 15.0
}
}
},
"setting": {
"type": "AIR_CONDITIONING",
"mode": "HEAT",
"power": "ON",
"temperature": {
"fahrenheit": 77.0,
"celsius": 25.0
}
}
}

22
tests/fixtures/tado/devices.json vendored Normal file
View File

@ -0,0 +1,22 @@
[
{
"deviceType" : "WR02",
"currentFwVersion" : "59.4",
"accessPointWiFi" : {
"ssid" : "tado8480"
},
"characteristics" : {
"capabilities" : [
"INSIDE_TEMPERATURE_MEASUREMENT",
"IDENTIFY"
]
},
"serialNo" : "WR1",
"commandTableUploadState" : "FINISHED",
"connectionState" : {
"timestamp" : "2020-03-23T18:30:07.377Z",
"value" : true
},
"shortSerialNo" : "WR1"
}
]

View File

@ -0,0 +1,67 @@
{
"tadoMode": "HOME",
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"preparation": null,
"setting": {
"type": "AIR_CONDITIONING",
"power": "ON",
"mode": "HEAT",
"temperature": {
"celsius": 16.11,
"fahrenheit": 61.00
},
"fanSpeed": "AUTO"
},
"overlayType": "MANUAL",
"overlay": {
"type": "MANUAL",
"setting": {
"type": "AIR_CONDITIONING",
"power": "ON",
"mode": "HEAT",
"temperature": {
"celsius": 16.11,
"fahrenheit": 61.00
},
"fanSpeed": "AUTO"
},
"termination": {
"type": "TADO_MODE",
"typeSkillBasedApp": "TADO_MODE",
"projectedExpiry": null
}
},
"openWindow": null,
"nextScheduleChange": null,
"nextTimeBlock": {
"start": "2020-03-07T04:00:00.000Z"
},
"link": {
"state": "ONLINE"
},
"activityDataPoints": {
"acPower": {
"timestamp": "2020-03-06T17:38:30.302Z",
"type": "POWER",
"value": "OFF"
}
},
"sensorDataPoints": {
"insideTemperature": {
"celsius": 21.40,
"fahrenheit": 70.52,
"timestamp": "2020-03-06T18:06:09.546Z",
"type": "TEMPERATURE",
"precision": {
"celsius": 0.1,
"fahrenheit": 0.1
}
},
"humidity": {
"type": "PERCENTAGE",
"percentage": 50.40,
"timestamp": "2020-03-06T18:06:09.546Z"
}
}
}

28
tests/fixtures/tado/me.json vendored Normal file
View File

@ -0,0 +1,28 @@
{
"id" : "5",
"mobileDevices" : [
{
"name" : "nick Android",
"deviceMetadata" : {
"platform" : "Android",
"locale" : "en",
"osVersion" : "10",
"model" : "OnePlus_GM1917"
},
"settings" : {
"geoTrackingEnabled" : false
},
"id" : 1
}
],
"homes" : [
{
"name" : "home name",
"id" : 1
}
],
"name" : "name",
"locale" : "en_US",
"email" : "user@domain.tld",
"username" : "user@domain.tld"
}

View File

@ -0,0 +1,57 @@
{
"tadoMode": "HOME",
"sensorDataPoints": {
"insideTemperature": {
"fahrenheit": 76.64,
"timestamp": "2020-03-05T03:55:38.160Z",
"celsius": 24.8,
"type": "TEMPERATURE",
"precision": {
"fahrenheit": 0.1,
"celsius": 0.1
}
},
"humidity": {
"timestamp": "2020-03-05T03:55:38.160Z",
"percentage": 62.5,
"type": "PERCENTAGE"
}
},
"link": {
"state": "ONLINE"
},
"openWindow": null,
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"overlay": {
"termination": {
"typeSkillBasedApp": "TADO_MODE",
"projectedExpiry": null,
"type": "TADO_MODE"
},
"setting": {
"type": "AIR_CONDITIONING",
"mode": "AUTO",
"power": "ON"
},
"type": "MANUAL"
},
"activityDataPoints": {
"acPower": {
"timestamp": "2020-03-05T03:56:38.627Z",
"type": "POWER",
"value": "ON"
}
},
"nextTimeBlock": {
"start": "2020-03-05T08:00:00.000Z"
},
"preparation": null,
"overlayType": "MANUAL",
"nextScheduleChange": null,
"setting": {
"type": "AIR_CONDITIONING",
"mode": "AUTO",
"power": "ON"
}
}

View File

@ -0,0 +1,67 @@
{
"tadoMode": "HOME",
"sensorDataPoints": {
"insideTemperature": {
"fahrenheit": 76.57,
"timestamp": "2020-03-05T03:57:38.850Z",
"celsius": 24.76,
"type": "TEMPERATURE",
"precision": {
"fahrenheit": 0.1,
"celsius": 0.1
}
},
"humidity": {
"timestamp": "2020-03-05T03:57:38.850Z",
"percentage": 60.9,
"type": "PERCENTAGE"
}
},
"link": {
"state": "ONLINE"
},
"openWindow": null,
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"overlay": {
"termination": {
"typeSkillBasedApp": "TADO_MODE",
"projectedExpiry": null,
"type": "TADO_MODE"
},
"setting": {
"fanSpeed": "AUTO",
"type": "AIR_CONDITIONING",
"mode": "COOL",
"power": "ON",
"temperature": {
"fahrenheit": 64.0,
"celsius": 17.78
}
},
"type": "MANUAL"
},
"activityDataPoints": {
"acPower": {
"timestamp": "2020-03-05T04:01:07.162Z",
"type": "POWER",
"value": "ON"
}
},
"nextTimeBlock": {
"start": "2020-03-05T08:00:00.000Z"
},
"preparation": null,
"overlayType": "MANUAL",
"nextScheduleChange": null,
"setting": {
"fanSpeed": "AUTO",
"type": "AIR_CONDITIONING",
"mode": "COOL",
"power": "ON",
"temperature": {
"fahrenheit": 64.0,
"celsius": 17.78
}
}
}

View File

@ -0,0 +1,57 @@
{
"tadoMode": "HOME",
"sensorDataPoints": {
"insideTemperature": {
"fahrenheit": 77.02,
"timestamp": "2020-03-05T04:02:07.396Z",
"celsius": 25.01,
"type": "TEMPERATURE",
"precision": {
"fahrenheit": 0.1,
"celsius": 0.1
}
},
"humidity": {
"timestamp": "2020-03-05T04:02:07.396Z",
"percentage": 62.0,
"type": "PERCENTAGE"
}
},
"link": {
"state": "ONLINE"
},
"openWindow": null,
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"overlay": {
"termination": {
"typeSkillBasedApp": "TADO_MODE",
"projectedExpiry": null,
"type": "TADO_MODE"
},
"setting": {
"type": "AIR_CONDITIONING",
"mode": "DRY",
"power": "ON"
},
"type": "MANUAL"
},
"activityDataPoints": {
"acPower": {
"timestamp": "2020-03-05T04:02:40.867Z",
"type": "POWER",
"value": "ON"
}
},
"nextTimeBlock": {
"start": "2020-03-05T08:00:00.000Z"
},
"preparation": null,
"overlayType": "MANUAL",
"nextScheduleChange": null,
"setting": {
"type": "AIR_CONDITIONING",
"mode": "DRY",
"power": "ON"
}
}

View File

@ -0,0 +1,57 @@
{
"tadoMode": "HOME",
"sensorDataPoints": {
"insideTemperature": {
"fahrenheit": 77.02,
"timestamp": "2020-03-05T04:02:07.396Z",
"celsius": 25.01,
"type": "TEMPERATURE",
"precision": {
"fahrenheit": 0.1,
"celsius": 0.1
}
},
"humidity": {
"timestamp": "2020-03-05T04:02:07.396Z",
"percentage": 62.0,
"type": "PERCENTAGE"
}
},
"link": {
"state": "ONLINE"
},
"openWindow": null,
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"overlay": {
"termination": {
"typeSkillBasedApp": "TADO_MODE",
"projectedExpiry": null,
"type": "TADO_MODE"
},
"setting": {
"type": "AIR_CONDITIONING",
"mode": "FAN",
"power": "ON"
},
"type": "MANUAL"
},
"activityDataPoints": {
"acPower": {
"timestamp": "2020-03-05T04:03:44.328Z",
"type": "POWER",
"value": "ON"
}
},
"nextTimeBlock": {
"start": "2020-03-05T08:00:00.000Z"
},
"preparation": null,
"overlayType": "MANUAL",
"nextScheduleChange": null,
"setting": {
"type": "AIR_CONDITIONING",
"mode": "FAN",
"power": "ON"
}
}

View File

@ -0,0 +1,67 @@
{
"tadoMode": "HOME",
"sensorDataPoints": {
"insideTemperature": {
"fahrenheit": 76.57,
"timestamp": "2020-03-05T03:57:38.850Z",
"celsius": 24.76,
"type": "TEMPERATURE",
"precision": {
"fahrenheit": 0.1,
"celsius": 0.1
}
},
"humidity": {
"timestamp": "2020-03-05T03:57:38.850Z",
"percentage": 60.9,
"type": "PERCENTAGE"
}
},
"link": {
"state": "ONLINE"
},
"openWindow": null,
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"overlay": {
"termination": {
"typeSkillBasedApp": "TADO_MODE",
"projectedExpiry": null,
"type": "TADO_MODE"
},
"setting": {
"fanSpeed": "AUTO",
"type": "AIR_CONDITIONING",
"mode": "HEAT",
"power": "ON",
"temperature": {
"fahrenheit": 61.0,
"celsius": 16.11
}
},
"type": "MANUAL"
},
"activityDataPoints": {
"acPower": {
"timestamp": "2020-03-05T03:59:36.390Z",
"type": "POWER",
"value": "ON"
}
},
"nextTimeBlock": {
"start": "2020-03-05T08:00:00.000Z"
},
"preparation": null,
"overlayType": "MANUAL",
"nextScheduleChange": null,
"setting": {
"fanSpeed": "AUTO",
"type": "AIR_CONDITIONING",
"mode": "HEAT",
"power": "ON",
"temperature": {
"fahrenheit": 61.0,
"celsius": 16.11
}
}
}

View File

@ -0,0 +1,55 @@
{
"tadoMode": "AWAY",
"sensorDataPoints": {
"insideTemperature": {
"fahrenheit": 70.59,
"timestamp": "2020-03-05T01:21:44.089Z",
"celsius": 21.44,
"type": "TEMPERATURE",
"precision": {
"fahrenheit": 0.1,
"celsius": 0.1
}
},
"humidity": {
"timestamp": "2020-03-05T01:21:44.089Z",
"percentage": 48.2,
"type": "PERCENTAGE"
}
},
"link": {
"state": "ONLINE"
},
"openWindow": null,
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"overlay": {
"termination": {
"typeSkillBasedApp": "MANUAL",
"projectedExpiry": null,
"type": "MANUAL"
},
"setting": {
"type": "AIR_CONDITIONING",
"power": "OFF"
},
"type": "MANUAL"
},
"activityDataPoints": {
"acPower": {
"timestamp": "2020-02-29T05:34:10.318Z",
"type": "POWER",
"value": "OFF"
}
},
"nextTimeBlock": {
"start": "2020-03-05T04:00:00.000Z"
},
"preparation": null,
"overlayType": "MANUAL",
"nextScheduleChange": null,
"setting": {
"type": "AIR_CONDITIONING",
"power": "OFF"
}
}

View File

@ -0,0 +1,55 @@
{
"tadoMode": "HOME",
"sensorDataPoints": {
"insideTemperature": {
"fahrenheit": 77.02,
"timestamp": "2020-03-05T04:02:07.396Z",
"celsius": 25.01,
"type": "TEMPERATURE",
"precision": {
"fahrenheit": 0.1,
"celsius": 0.1
}
},
"humidity": {
"timestamp": "2020-03-05T04:02:07.396Z",
"percentage": 62.0,
"type": "PERCENTAGE"
}
},
"link": {
"state": "ONLINE"
},
"openWindow": null,
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"overlay": {
"termination": {
"typeSkillBasedApp": "MANUAL",
"projectedExpiry": null,
"type": "MANUAL"
},
"setting": {
"type": "AIR_CONDITIONING",
"power": "OFF"
},
"type": "MANUAL"
},
"activityDataPoints": {
"acPower": {
"timestamp": "2020-03-05T04:05:08.804Z",
"type": "POWER",
"value": "OFF"
}
},
"nextTimeBlock": {
"start": "2020-03-05T08:00:00.000Z"
},
"preparation": null,
"overlayType": "MANUAL",
"nextScheduleChange": null,
"setting": {
"type": "AIR_CONDITIONING",
"power": "OFF"
}
}

View File

@ -0,0 +1,71 @@
{
"tadoMode": "HOME",
"sensorDataPoints": {
"insideTemperature": {
"fahrenheit": 77.09,
"timestamp": "2020-03-03T21:23:57.846Z",
"celsius": 25.05,
"type": "TEMPERATURE",
"precision": {
"fahrenheit": 0.1,
"celsius": 0.1
}
},
"humidity": {
"timestamp": "2020-03-03T21:23:57.846Z",
"percentage": 61.6,
"type": "PERCENTAGE"
}
},
"link": {
"state": "OFFLINE",
"reason": {
"code": "disconnectedDevice",
"title": "There is a disconnected device."
}
},
"openWindow": null,
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"overlay": {
"termination": {
"typeSkillBasedApp": "TADO_MODE",
"projectedExpiry": null,
"type": "TADO_MODE"
},
"setting": {
"fanSpeed": "AUTO",
"type": "AIR_CONDITIONING",
"mode": "COOL",
"power": "ON",
"temperature": {
"fahrenheit": 64.0,
"celsius": 17.78
}
},
"type": "MANUAL"
},
"activityDataPoints": {
"acPower": {
"timestamp": "2020-02-29T18:42:26.683Z",
"type": "POWER",
"value": "OFF"
}
},
"nextTimeBlock": {
"start": "2020-03-05T08:00:00.000Z"
},
"preparation": null,
"overlayType": "MANUAL",
"nextScheduleChange": null,
"setting": {
"fanSpeed": "AUTO",
"type": "AIR_CONDITIONING",
"mode": "COOL",
"power": "ON",
"temperature": {
"fahrenheit": 64.0,
"celsius": 17.78
}
}
}

View File

@ -0,0 +1,50 @@
{
"tadoMode": "HOME",
"sensorDataPoints": {
"insideTemperature": {
"fahrenheit": 75.97,
"timestamp": "2020-03-05T03:50:24.769Z",
"celsius": 24.43,
"type": "TEMPERATURE",
"precision": {
"fahrenheit": 0.1,
"celsius": 0.1
}
},
"humidity": {
"timestamp": "2020-03-05T03:50:24.769Z",
"percentage": 60.0,
"type": "PERCENTAGE"
}
},
"link": {
"state": "ONLINE"
},
"openWindow": null,
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"overlay": null,
"activityDataPoints": {
"acPower": {
"timestamp": "2020-03-05T03:52:22.253Z",
"type": "POWER",
"value": "OFF"
}
},
"nextTimeBlock": {
"start": "2020-03-05T08:00:00.000Z"
},
"preparation": null,
"overlayType": null,
"nextScheduleChange": null,
"setting": {
"fanSpeed": "MIDDLE",
"type": "AIR_CONDITIONING",
"mode": "COOL",
"power": "ON",
"temperature": {
"fahrenheit": 68.0,
"celsius": 20.0
}
}
}

View File

@ -0,0 +1,55 @@
{
"tadoMode": "HOME",
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"preparation": null,
"setting": {
"type": "AIR_CONDITIONING",
"power": "OFF"
},
"overlayType": "MANUAL",
"overlay": {
"type": "MANUAL",
"setting": {
"type": "AIR_CONDITIONING",
"power": "OFF"
},
"termination": {
"type": "MANUAL",
"typeSkillBasedApp": "MANUAL",
"projectedExpiry": null
}
},
"openWindow": null,
"nextScheduleChange": null,
"nextTimeBlock": {
"start": "2020-03-07T04:00:00.000Z"
},
"link": {
"state": "ONLINE"
},
"activityDataPoints": {
"acPower": {
"timestamp": "2020-03-06T19:05:21.835Z",
"type": "POWER",
"value": "ON"
}
},
"sensorDataPoints": {
"insideTemperature": {
"celsius": 21.40,
"fahrenheit": 70.52,
"timestamp": "2020-03-06T19:06:13.185Z",
"type": "TEMPERATURE",
"precision": {
"celsius": 0.1,
"fahrenheit": 0.1
}
},
"humidity": {
"type": "PERCENTAGE",
"percentage": 49.20,
"timestamp": "2020-03-06T19:06:13.185Z"
}
}
}

View File

@ -0,0 +1,58 @@
{
"tadoMode": "HOME",
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"preparation": null,
"setting": {
"type": "HEATING",
"power": "ON",
"temperature": {
"celsius": 20.00,
"fahrenheit": 68.00
}
},
"overlayType": null,
"overlay": null,
"openWindow": null,
"nextScheduleChange": {
"start": "2020-03-10T17:00:00Z",
"setting": {
"type": "HEATING",
"power": "ON",
"temperature": {
"celsius": 21.00,
"fahrenheit": 69.80
}
}
},
"nextTimeBlock": {
"start": "2020-03-10T17:00:00.000Z"
},
"link": {
"state": "ONLINE"
},
"activityDataPoints": {
"heatingPower": {
"type": "PERCENTAGE",
"percentage": 0.00,
"timestamp": "2020-03-10T07:47:45.978Z"
}
},
"sensorDataPoints": {
"insideTemperature": {
"celsius": 20.65,
"fahrenheit": 69.17,
"timestamp": "2020-03-10T07:44:11.947Z",
"type": "TEMPERATURE",
"precision": {
"celsius": 0.1,
"fahrenheit": 0.1
}
},
"humidity": {
"type": "PERCENTAGE",
"percentage": 45.20,
"timestamp": "2020-03-10T07:44:11.947Z"
}
}
}

View File

@ -0,0 +1,73 @@
{
"tadoMode": "HOME",
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"preparation": null,
"setting": {
"type": "HEATING",
"power": "ON",
"temperature": {
"celsius": 20.50,
"fahrenheit": 68.90
}
},
"overlayType": "MANUAL",
"overlay": {
"type": "MANUAL",
"setting": {
"type": "HEATING",
"power": "ON",
"temperature": {
"celsius": 20.50,
"fahrenheit": 68.90
}
},
"termination": {
"type": "MANUAL",
"typeSkillBasedApp": "MANUAL",
"projectedExpiry": null
}
},
"openWindow": null,
"nextScheduleChange": {
"start": "2020-03-10T17:00:00Z",
"setting": {
"type": "HEATING",
"power": "ON",
"temperature": {
"celsius": 21.00,
"fahrenheit": 69.80
}
}
},
"nextTimeBlock": {
"start": "2020-03-10T17:00:00.000Z"
},
"link": {
"state": "ONLINE"
},
"activityDataPoints": {
"heatingPower": {
"type": "PERCENTAGE",
"percentage": 0.00,
"timestamp": "2020-03-10T07:47:45.978Z"
}
},
"sensorDataPoints": {
"insideTemperature": {
"celsius": 20.65,
"fahrenheit": 69.17,
"timestamp": "2020-03-10T07:44:11.947Z",
"type": "TEMPERATURE",
"precision": {
"celsius": 0.1,
"fahrenheit": 0.1
}
},
"humidity": {
"type": "PERCENTAGE",
"percentage": 45.20,
"timestamp": "2020-03-10T07:44:11.947Z"
}
}
}

View File

@ -0,0 +1,67 @@
{
"tadoMode": "HOME",
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"preparation": null,
"setting": {
"type": "HEATING",
"power": "OFF",
"temperature": null
},
"overlayType": "MANUAL",
"overlay": {
"type": "MANUAL",
"setting": {
"type": "HEATING",
"power": "OFF",
"temperature": null
},
"termination": {
"type": "MANUAL",
"typeSkillBasedApp": "MANUAL",
"projectedExpiry": null
}
},
"openWindow": null,
"nextScheduleChange": {
"start": "2020-03-10T17:00:00Z",
"setting": {
"type": "HEATING",
"power": "ON",
"temperature": {
"celsius": 21.00,
"fahrenheit": 69.80
}
}
},
"nextTimeBlock": {
"start": "2020-03-10T17:00:00.000Z"
},
"link": {
"state": "ONLINE"
},
"activityDataPoints": {
"heatingPower": {
"type": "PERCENTAGE",
"percentage": 0.00,
"timestamp": "2020-03-10T07:47:45.978Z"
}
},
"sensorDataPoints": {
"insideTemperature": {
"celsius": 20.65,
"fahrenheit": 69.17,
"timestamp": "2020-03-10T07:44:11.947Z",
"type": "TEMPERATURE",
"precision": {
"celsius": 0.1,
"fahrenheit": 0.1
}
},
"humidity": {
"type": "PERCENTAGE",
"percentage": 45.20,
"timestamp": "2020-03-10T07:44:11.947Z"
}
}
}

View File

@ -0,0 +1,33 @@
{
"tadoMode": "HOME",
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"preparation": null,
"setting": {
"type": "HOT_WATER",
"power": "ON",
"temperature": {
"celsius": 65.00,
"fahrenheit": 149.00
}
},
"overlayType": null,
"overlay": null,
"openWindow": null,
"nextScheduleChange": {
"start": "2020-03-10T22:00:00Z",
"setting": {
"type": "HOT_WATER",
"power": "OFF",
"temperature": null
}
},
"nextTimeBlock": {
"start": "2020-03-10T22:00:00.000Z"
},
"link": {
"state": "ONLINE"
},
"activityDataPoints": {},
"sensorDataPoints": {}
}

View File

@ -0,0 +1,51 @@
{
"activityDataPoints" : {},
"preparation" : null,
"openWindow" : null,
"tadoMode" : "HOME",
"nextScheduleChange" : {
"setting" : {
"temperature" : {
"fahrenheit" : 149,
"celsius" : 65
},
"type" : "HOT_WATER",
"power" : "ON"
},
"start" : "2020-03-26T05:00:00Z"
},
"nextTimeBlock" : {
"start" : "2020-03-26T05:00:00.000Z"
},
"overlay" : {
"setting" : {
"temperature" : {
"celsius" : 30,
"fahrenheit" : 86
},
"type" : "HOT_WATER",
"power" : "ON"
},
"termination" : {
"type" : "TADO_MODE",
"projectedExpiry" : "2020-03-26T05:00:00Z",
"typeSkillBasedApp" : "TADO_MODE"
},
"type" : "MANUAL"
},
"geolocationOverride" : false,
"geolocationOverrideDisableTime" : null,
"sensorDataPoints" : {},
"overlayType" : "MANUAL",
"link" : {
"state" : "ONLINE"
},
"setting" : {
"type" : "HOT_WATER",
"temperature" : {
"fahrenheit" : 86,
"celsius" : 30
},
"power" : "ON"
}
}

View File

@ -0,0 +1,48 @@
{
"tadoMode": "HOME",
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"preparation": null,
"setting": {
"type": "HOT_WATER",
"power": "ON",
"temperature": {
"celsius": 55.00,
"fahrenheit": 131.00
}
},
"overlayType": "MANUAL",
"overlay": {
"type": "MANUAL",
"setting": {
"type": "HOT_WATER",
"power": "ON",
"temperature": {
"celsius": 55.00,
"fahrenheit": 131.00
}
},
"termination": {
"type": "MANUAL",
"typeSkillBasedApp": "MANUAL",
"projectedExpiry": null
}
},
"openWindow": null,
"nextScheduleChange": {
"start": "2020-03-10T22:00:00Z",
"setting": {
"type": "HOT_WATER",
"power": "OFF",
"temperature": null
}
},
"nextTimeBlock": {
"start": "2020-03-10T22:00:00.000Z"
},
"link": {
"state": "ONLINE"
},
"activityDataPoints": {},
"sensorDataPoints": {}
}

View File

@ -0,0 +1,42 @@
{
"tadoMode": "HOME",
"geolocationOverride": false,
"geolocationOverrideDisableTime": null,
"preparation": null,
"setting": {
"type": "HOT_WATER",
"power": "OFF",
"temperature": null
},
"overlayType": "MANUAL",
"overlay": {
"type": "MANUAL",
"setting": {
"type": "HOT_WATER",
"power": "OFF",
"temperature": null
},
"termination": {
"type": "MANUAL",
"typeSkillBasedApp": "MANUAL",
"projectedExpiry": null
}
},
"openWindow": null,
"nextScheduleChange": {
"start": "2020-03-10T22:00:00Z",
"setting": {
"type": "HOT_WATER",
"power": "OFF",
"temperature": null
}
},
"nextTimeBlock": {
"start": "2020-03-10T22:00:00.000Z"
},
"link": {
"state": "ONLINE"
},
"activityDataPoints": {},
"sensorDataPoints": {}
}

View File

@ -0,0 +1,19 @@
{
"type" : "HEATING",
"HEAT" : {
"temperatures" : {
"celsius" : {
"max" : 31,
"step" : 1,
"min" : 16
},
"fahrenheit" : {
"step" : 1,
"max" : 88,
"min" : 61
}
}
},
"AUTO" : {},
"FAN" : {}
}

8
tests/fixtures/tado/token.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"expires_in" : 599,
"scope" : "home.user",
"token_type" : "bearer",
"refresh_token" : "refresh",
"access_token" : "access",
"jti" : "jti"
}

View File

@ -0,0 +1,17 @@
{
"canSetTemperature" : true,
"DRY" : {},
"type" : "HOT_WATER",
"temperatures" : {
"celsius" : {
"min" : 16,
"max" : 31,
"step" : 1
},
"fahrenheit" : {
"step" : 1,
"max" : 88,
"min" : 61
}
}
}

View File

@ -0,0 +1,46 @@
{
"type" : "AIR_CONDITIONING",
"HEAT" : {
"fanSpeeds" : [
"AUTO",
"HIGH",
"MIDDLE",
"LOW"
],
"temperatures" : {
"celsius" : {
"max" : 31,
"step" : 1,
"min" : 16
},
"fahrenheit" : {
"step" : 1,
"max" : 88,
"min" : 61
}
}
},
"AUTO" : {},
"DRY" : {},
"FAN" : {},
"COOL" : {
"temperatures" : {
"celsius" : {
"min" : 16,
"step" : 1,
"max" : 31
},
"fahrenheit" : {
"min" : 61,
"max" : 88,
"step" : 1
}
},
"fanSpeeds" : [
"AUTO",
"HIGH",
"MIDDLE",
"LOW"
]
}
}

55
tests/fixtures/tado/zone_state.json vendored Normal file
View File

@ -0,0 +1,55 @@
{
"openWindow" : null,
"nextScheduleChange" : null,
"geolocationOverrideDisableTime" : null,
"sensorDataPoints" : {
"insideTemperature" : {
"celsius" : 22.43,
"type" : "TEMPERATURE",
"precision" : {
"fahrenheit" : 0.1,
"celsius" : 0.1
},
"timestamp" : "2020-03-23T18:30:07.377Z",
"fahrenheit" : 72.37
},
"humidity" : {
"timestamp" : "2020-03-23T18:30:07.377Z",
"percentage" : 60.2,
"type" : "PERCENTAGE"
}
},
"overlay" : {
"type" : "MANUAL",
"termination" : {
"projectedExpiry" : null,
"typeSkillBasedApp" : "MANUAL",
"type" : "MANUAL"
},
"setting" : {
"power" : "OFF",
"type" : "AIR_CONDITIONING"
}
},
"geolocationOverride" : false,
"overlayType" : "MANUAL",
"activityDataPoints" : {
"acPower" : {
"type" : "POWER",
"timestamp" : "2020-03-11T15:08:23.604Z",
"value" : "OFF"
}
},
"tadoMode" : "HOME",
"link" : {
"state" : "ONLINE"
},
"setting" : {
"power" : "OFF",
"type" : "AIR_CONDITIONING"
},
"nextTimeBlock" : {
"start" : "2020-03-24T03:00:00.000Z"
},
"preparation" : null
}

179
tests/fixtures/tado/zones.json vendored Normal file
View File

@ -0,0 +1,179 @@
[
{
"deviceTypes" : [
"WR02"
],
"type" : "HEATING",
"reportAvailable" : false,
"dazzleMode" : {
"enabled" : true,
"supported" : true
},
"name" : "Baseboard Heater",
"supportsDazzle" : true,
"id" : 1,
"devices" : [
{
"duties" : [
"ZONE_UI",
"ZONE_DRIVER",
"ZONE_LEADER"
],
"currentFwVersion" : "59.4",
"deviceType" : "WR02",
"serialNo" : "WR4",
"shortSerialNo" : "WR4",
"commandTableUploadState" : "FINISHED",
"connectionState" : {
"value" : true,
"timestamp" : "2020-03-23T18:30:07.377Z"
},
"accessPointWiFi" : {
"ssid" : "tado8480"
},
"characteristics" : {
"capabilities" : [
"INSIDE_TEMPERATURE_MEASUREMENT",
"IDENTIFY"
]
}
}
],
"dateCreated" : "2019-11-28T15:58:48.968Z",
"dazzleEnabled" : true
},
{
"type" : "HOT_WATER",
"reportAvailable" : false,
"deviceTypes" : [
"WR02"
],
"devices" : [
{
"connectionState" : {
"value" : true,
"timestamp" : "2020-03-23T18:30:07.377Z"
},
"accessPointWiFi" : {
"ssid" : "tado8480"
},
"characteristics" : {
"capabilities" : [
"INSIDE_TEMPERATURE_MEASUREMENT",
"IDENTIFY"
]
},
"duties" : [
"ZONE_UI",
"ZONE_DRIVER",
"ZONE_LEADER"
],
"currentFwVersion" : "59.4",
"deviceType" : "WR02",
"serialNo" : "WR4",
"shortSerialNo" : "WR4",
"commandTableUploadState" : "FINISHED"
}
],
"dazzleEnabled" : true,
"dateCreated" : "2019-11-28T15:58:48.968Z",
"name" : "Water Heater",
"dazzleMode" : {
"enabled" : true,
"supported" : true
},
"id" : 2,
"supportsDazzle" : true
},
{
"dazzleMode" : {
"supported" : true,
"enabled" : true
},
"name" : "Air Conditioning",
"id" : 3,
"supportsDazzle" : true,
"devices" : [
{
"deviceType" : "WR02",
"shortSerialNo" : "WR4",
"serialNo" : "WR4",
"commandTableUploadState" : "FINISHED",
"duties" : [
"ZONE_UI",
"ZONE_DRIVER",
"ZONE_LEADER"
],
"currentFwVersion" : "59.4",
"characteristics" : {
"capabilities" : [
"INSIDE_TEMPERATURE_MEASUREMENT",
"IDENTIFY"
]
},
"accessPointWiFi" : {
"ssid" : "tado8480"
},
"connectionState" : {
"timestamp" : "2020-03-23T18:30:07.377Z",
"value" : true
}
}
],
"dazzleEnabled" : true,
"dateCreated" : "2019-11-28T15:58:48.968Z",
"openWindowDetection" : {
"timeoutInSeconds" : 900,
"enabled" : true,
"supported" : true
},
"deviceTypes" : [
"WR02"
],
"reportAvailable" : false,
"type" : "AIR_CONDITIONING"
},
{
"type" : "HOT_WATER",
"reportAvailable" : false,
"deviceTypes" : [
"WR02"
],
"devices" : [
{
"connectionState" : {
"value" : true,
"timestamp" : "2020-03-23T18:30:07.377Z"
},
"accessPointWiFi" : {
"ssid" : "tado8480"
},
"characteristics" : {
"capabilities" : [
"INSIDE_TEMPERATURE_MEASUREMENT",
"IDENTIFY"
]
},
"duties" : [
"ZONE_UI",
"ZONE_DRIVER",
"ZONE_LEADER"
],
"currentFwVersion" : "59.4",
"deviceType" : "WR02",
"serialNo" : "WR4",
"shortSerialNo" : "WR4",
"commandTableUploadState" : "FINISHED"
}
],
"dazzleEnabled" : true,
"dateCreated" : "2019-11-28T15:58:48.968Z",
"name" : "Second Water Heater",
"dazzleMode" : {
"enabled" : true,
"supported" : true
},
"id" : 4,
"supportsDazzle" : true
}
]