diff --git a/CODEOWNERS b/CODEOWNERS index 5ea7c376329..8f335bfcc4d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -369,7 +369,7 @@ homeassistant/components/switchmate/* @danielhiversen homeassistant/components/syncthru/* @nielstron homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff -homeassistant/components/tado/* @michaelarnauts +homeassistant/components/tado/* @michaelarnauts @bdraco homeassistant/components/tahoma/* @philklei homeassistant/components/tankerkoenig/* @guillempages homeassistant/components/tautulli/* @ludeeus diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index e5e3d1d409c..46dba04a77e 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -1,9 +1,9 @@ """Support for the (unofficial) Tado API.""" from datetime import timedelta import logging -import urllib from PyTado.interface import Tado +from requests import RequestException import voluptuous as vol from homeassistant.components.climate.const import PRESET_AWAY, PRESET_HOME @@ -110,7 +110,7 @@ class TadoConnector: """Connect to Tado and fetch the zones.""" try: 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) return False @@ -135,9 +135,14 @@ class TadoConnector: _LOGGER.debug("Updating %s %s", sensor_type, sensor) try: if sensor_type == "zone": - data = self.tado.getState(sensor) + data = self.tado.getZoneState(sensor) 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: _LOGGER.debug("Unknown sensor: %s", sensor_type) return @@ -174,29 +179,40 @@ class TadoConnector: def set_zone_overlay( self, - zone_id, - overlay_mode, + zone_id=None, + overlay_mode=None, temperature=None, duration=None, device_type="HEATING", mode=None, + fan_speed=None, ): """Set a zone overlay.""" _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, overlay_mode, temperature, duration, device_type, mode, + fan_speed, ) + try: 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) @@ -206,7 +222,7 @@ class TadoConnector: 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()) + except RequestException as exc: + _LOGGER.error("Could not set zone overlay: %s", exc) self.update_sensor("zone", zone_id) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index e202cc49da4..224960ea3eb 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -3,21 +3,13 @@ import logging from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - CURRENT_HVAC_COOL, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, - FAN_HIGH, - FAN_LOW, - FAN_MIDDLE, - FAN_OFF, - HVAC_MODE_AUTO, - HVAC_MODE_COOL, + FAN_AUTO, HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_AWAY, PRESET_HOME, + SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -27,49 +19,30 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED from .const import ( + CONST_FAN_AUTO, + CONST_FAN_OFF, + CONST_MODE_AUTO, + CONST_MODE_COOL, + CONST_MODE_HEAT, CONST_MODE_OFF, CONST_MODE_SMART_SCHEDULE, CONST_OVERLAY_MANUAL, CONST_OVERLAY_TADO_MODE, - CONST_OVERLAY_TIMER, 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_HEATING, ) _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): """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) 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: - # 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 - if "HEAT" in capabilities: - temperatures = capabilities["HEAT"]["temperatures"] - ac_support_heat = True - else: - temperatures = capabilities["COOL"]["temperatures"] - elif "temperatures" in capabilities: - temperatures = capabilities["temperatures"] + for mode in ORDERED_KNOWN_TADO_MODES: + if mode not in capabilities: + continue + + supported_hvac_modes.append(TADO_TO_HA_HVAC_MODE_MAP[mode]) + if not capabilities[mode].get("fanSpeeds"): + 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: - _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 - min_temp = float(temperatures["celsius"]["min"]) - max_temp = float(temperatures["celsius"]["max"]) - step = temperatures["celsius"].get("step", PRECISION_TENTHS) + heat_min_temp = None + heat_max_temp = None + 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( - 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 @@ -132,10 +156,15 @@ class TadoClimate(ClimateDevice): zone_name, zone_id, zone_type, - min_temp, - max_temp, - step, - ac_support_heat, + heat_min_temp, + heat_max_temp, + heat_step, + cool_min_temp, + cool_max_temp, + cool_step, + supported_hvac_modes, + supported_fan_modes, + support_flags, ): """Initialize of Tado climate entity.""" self._tado = tado @@ -146,49 +175,51 @@ class TadoClimate(ClimateDevice): self._unique_id = f"{zone_type} {zone_id} {tado.device_id}" self._ac_device = zone_type == TYPE_AIR_CONDITIONING - self._ac_support_heat = ac_support_heat - self._cooling = False + self._supported_hvac_modes = supported_hvac_modes + self._supported_fan_modes = supported_fan_modes + self._support_flags = support_flags - self._active = False - self._device_is_active = False + self._available = False self._cur_temp = None self._cur_humidity = None - self._is_away = False - self._min_temp = min_temp - self._max_temp = max_temp - self._step = step + + self._heat_min_temp = heat_min_temp + self._heat_max_temp = heat_max_temp + 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 - if tado.fallback: - # 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_tado_fan_speed = CONST_FAN_OFF + self._current_tado_hvac_mode = CONST_MODE_OFF + self._current_tado_hvac_action = CURRENT_HVAC_OFF - self._current_fan = CONST_MODE_OFF - self._current_operation = CONST_MODE_SMART_SCHEDULE - self._overlay_mode = CONST_MODE_SMART_SCHEDULE + self._undo_dispatcher = None + self._tado_zone_data = None + 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): """Register for sensor updates.""" - @callback - def async_update_callback(): - """Schedule an entity update.""" - self.async_schedule_update_ha_state(True) - - async_dispatcher_connect( + self._undo_dispatcher = async_dispatcher_connect( self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id), - async_update_callback, + self._async_update_callback, ) @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_FLAGS + return self._support_flags @property def name(self): @@ -208,12 +239,12 @@ class TadoClimate(ClimateDevice): @property def current_humidity(self): """Return the current humidity.""" - return self._cur_humidity + return self._tado_zone_data.current_humidity @property def current_temperature(self): """Return the sensor temperature.""" - return self._cur_temp + return self._tado_zone_data.current_temp @property def hvac_mode(self): @@ -221,11 +252,7 @@ class TadoClimate(ClimateDevice): Need to be one of HVAC_MODE_*. """ - if self._ac_device and self._ac_support_heat: - 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) + return TADO_TO_HA_HVAC_MODE_MAP.get(self._current_tado_hvac_mode, HVAC_MODE_OFF) @property def hvac_modes(self): @@ -233,11 +260,7 @@ class TadoClimate(ClimateDevice): Need to be a subset of HVAC_MODES. """ - if self._ac_device: - if self._ac_support_heat: - return SUPPORT_HVAC_HEAT_COOL - return SUPPORT_HVAC_COOL - return SUPPORT_HVAC_HEAT + return self._supported_hvac_modes @property def hvac_action(self): @@ -245,40 +268,30 @@ class TadoClimate(ClimateDevice): Need to be one of CURRENT_HVAC_*. """ - if not self._device_is_active: - return 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 + return TADO_HVAC_ACTION_TO_HA_HVAC_ACTION.get( + self._tado_zone_data.current_hvac_action, CURRENT_HVAC_OFF + ) @property def fan_mode(self): """Return the fan setting.""" 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 @property def fan_modes(self): """List of available fan modes.""" - if self._ac_device: - return SUPPORT_FAN - return None + return self._supported_fan_modes def set_fan_mode(self, fan_mode: str): """Turn fan on/off.""" - pass + self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode]) @property def preset_mode(self): """Return the current preset mode (home, away).""" - if self._is_away: + if self._tado_zone_data.is_away: return PRESET_AWAY return PRESET_HOME @@ -299,12 +312,18 @@ class TadoClimate(ClimateDevice): @property def target_temperature_step(self): """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 def target_temperature(self): """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): """Set new target temperature.""" @@ -312,174 +331,149 @@ class TadoClimate(ClimateDevice): if temperature is None: return - self._current_operation = self._default_overlay - self._overlay_mode = None - self._target_temp = temperature - self._control_heating() + if self._current_tado_hvac_mode not in ( + CONST_MODE_OFF, + CONST_MODE_AUTO, + 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): """Set new target hvac mode.""" - mode = None - if hvac_mode == HVAC_MODE_OFF: - 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._control_hvac(hvac_mode=HA_TO_TADO_HVAC_MODE_MAP[hvac_mode]) - self._current_operation = mode - self._overlay_mode = None - - # Set a target temperature if we don't have any - # This can happen when we switch from Off to On - if self._target_temp is None: - if self._ac_device: - self._target_temp = self.max_temp - else: - self._target_temp = self.min_temp - self.schedule_update_ha_state() - - self._control_heating() + @property + def available(self): + """Return if the device is available.""" + return self._tado_zone_data.available @property def min_temp(self): """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 def max_temp(self): """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 ( - "temperature" in data["setting"] - and data["setting"]["temperature"] is not None + self._current_tado_hvac_mode == CONST_MODE_HEAT + and self._heat_max_temp is not None ): - setting = float(data["setting"]["temperature"]["celsius"]) - self._target_temp = setting + return self._heat_max_temp + if self._heat_max_temp is not None: + return self._heat_max_temp - if "tadoMode" in data: - mode = data["tadoMode"] - self._is_away = mode == "AWAY" + return self._heat_max_temp - if "setting" in data: - power = data["setting"]["power"] - if power == "OFF": - self._current_operation = CONST_MODE_OFF - self._current_fan = 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 + @callback + def _async_update_zone_data(self): + """Load tado data into zone.""" + self._tado_zone_data = self._tado.data["zone"][self.zone_id] + self._current_tado_fan_speed = self._tado_zone_data.current_fan_speed + self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode + self._current_tado_hvac_action = self._tado_zone_data.current_hvac_action - active = False - if "activityDataPoints" in data: - activity_data = data["activityDataPoints"] - if self._ac_device: - if "acPower" in activity_data and activity_data["acPower"] is not None: - 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 + @callback + def _async_update_callback(self): + """Load tado data and update state.""" + self._async_update_zone_data() + self.async_write_ha_state() - overlay = False - overlay_data = None - termination = CONST_MODE_SMART_SCHEDULE - cooling = False - fan_speed = CONST_MODE_OFF + def _normalize_target_temp_for_hvac_mode(self): + # 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: + 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: - 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): + def _control_hvac(self, hvac_mode=None, target_temp=None, fan_mode=None): """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( "Switching to SMART_SCHEDULE for zone %s (%d)", self.zone_name, 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 _LOGGER.debug( "Switching to %s for zone %s (%d) with temperature %s °C", - self._current_operation, + self._current_tado_hvac_mode, self.zone_name, self.zone_id, self._target_temp, ) - self._tado.set_zone_overlay( - self.zone_id, - self._current_operation, - self._target_temp, - None, - self.zone_type, - "COOL" if self._ac_device else None, + + # 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 + ) + + 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 diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index 8d67e3bf9f8..542437d0af0 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -1,5 +1,48 @@ """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 CONF_FALLBACK = "fallback" DATA = "data" @@ -10,10 +53,81 @@ TYPE_HEATING = "HEATING" TYPE_HOT_WATER = "HOT_WATER" # Base modes +CONST_MODE_OFF = "OFF" 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 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 + + +# 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] diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index ab0be2d4346..e84072b5985 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -7,6 +7,6 @@ ], "dependencies": [], "codeowners": [ - "@michaelarnauts" + "@michaelarnauts", "@bdraco" ] } diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 2cd40bee3fa..fea81dcb586 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -31,6 +31,7 @@ ZONE_SENSORS = { "ac", "tado mode", "overlay", + "open window", ], 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: # 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( [ - create_zone_sensor(tado, zone["name"], zone["id"], variable) - for variable in ZONE_SENSORS.get(zone["type"]) + TadoZoneSensor(tado, zone["name"], zone["id"], variable) + for variable in ZONE_SENSORS[zone_type] ] ) # Create device sensors - for home in tado.devices: + for device in devices: entities.extend( [ - create_device_sensor(tado, home["name"], home["id"], variable) + TadoDeviceSensor(tado, device["name"], device["id"], variable) for variable in DEVICE_SENSORS ] ) @@ -67,46 +75,38 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(entities, True) -def create_zone_sensor(tado, name, zone_id, variable): - """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): +class TadoZoneSensor(Entity): """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.""" self._tado = tado self.zone_name = zone_name self.zone_id = zone_id self.zone_variable = zone_variable - self.sensor_type = sensor_type self._unique_id = f"{zone_variable} {zone_id} {tado.device_id}" self._state = 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): """Register for sensor updates.""" - @callback - def async_update_callback(): - """Schedule an entity update.""" - self.async_schedule_update_ha_state(True) - - async_dispatcher_connect( + self._undo_dispatcher = async_dispatcher_connect( self.hass, - SIGNAL_TADO_UPDATE_RECEIVED.format(self.sensor_type, self.zone_id), - async_update_callback, + SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id), + self._async_update_callback, ) + self._async_update_zone_data() @property def unique_id(self): @@ -138,7 +138,7 @@ class TadoSensor(Entity): if self.zone_variable == "heating": return UNIT_PERCENTAGE if self.zone_variable == "ac": - return "" + return None @property def icon(self): @@ -149,97 +149,143 @@ class TadoSensor(Entity): return "mdi:water-percent" @property - def should_poll(self) -> bool: + def should_poll(self): """Do not poll.""" 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.""" try: - data = self._tado.data[self.sensor_type][self.zone_id] + self._tado_zone_data = self._tado.data["zone"][self.zone_id] except KeyError: return - unit = TEMP_CELSIUS - if self.zone_variable == "temperature": - if "sensorDataPoints" in data: - sensor_data = data["sensorDataPoints"] - temperature = float(sensor_data["insideTemperature"]["celsius"]) - - self._state = self.hass.config.units.temperature(temperature, unit) - self._state_attributes = { - "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) + self._state = self.hass.config.units.temperature( + self._tado_zone_data.current_temp, TEMP_CELSIUS + ) + self._state_attributes = { + "time": self._tado_zone_data.current_temp_timestamp, + "setting": 0, # setting is used in climate device + } elif self.zone_variable == "humidity": - if "sensorDataPoints" in data: - sensor_data = data["sensorDataPoints"] - self._state = float(sensor_data["humidity"]["percentage"]) - self._state_attributes = {"time": sensor_data["humidity"]["timestamp"]} + self._state = self._tado_zone_data.current_humidity + self._state_attributes = { + "time": self._tado_zone_data.current_humidity_timestamp + } elif self.zone_variable == "power": - if "setting" in data: - self._state = data["setting"]["power"] + self._state = self._tado_zone_data.power elif self.zone_variable == "link": - if "link" in data: - self._state = data["link"]["state"] + self._state = self._tado_zone_data.link elif self.zone_variable == "heating": - if "activityDataPoints" in data: - activity_data = data["activityDataPoints"] - - 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"] - } + self._state = self._tado_zone_data.heating_power_percentage + self._state_attributes = { + "time": self._tado_zone_data.heating_power_timestamp + } elif self.zone_variable == "ac": - if "activityDataPoints" in data: - activity_data = data["activityDataPoints"] - - 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"] - } + self._state = self._tado_zone_data.ac_power + self._state_attributes = {"time": self._tado_zone_data.ac_power_timestamp} elif self.zone_variable == "tado bridge status": - if "connectionState" in data: - self._state = data["connectionState"]["value"] + self._state = self._tado_zone_data.connection elif self.zone_variable == "tado mode": - if "tadoMode" in data: - self._state = data["tadoMode"] + self._state = self._tado_zone_data.tado_mode 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 = ( - {"termination": data["overlay"]["termination"]["type"]} - if self._state + {"termination": self._tado_zone_data.overlay_termination_type} + if self._tado_zone_data.overlay_active else {} ) 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": - self._state = "openWindow" in data and data["openWindow"] is not None - self._state_attributes = data["openWindow"] if self._state else {} + self._state = self._tado_zone_data.open_window + 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) diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index fc3a9ce9cf4..51ff2ede57d 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -12,6 +12,9 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED from .const import ( + CONST_HVAC_HEAT, + CONST_MODE_AUTO, + CONST_MODE_HEAT, CONST_MODE_OFF, CONST_MODE_SMART_SCHEDULE, CONST_OVERLAY_MANUAL, @@ -33,6 +36,7 @@ WATER_HEATER_MAP_TADO = { CONST_OVERLAY_MANUAL: MODE_HEAT, CONST_OVERLAY_TIMER: MODE_HEAT, CONST_OVERLAY_TADO_MODE: MODE_HEAT, + CONST_HVAC_HEAT: MODE_HEAT, CONST_MODE_SMART_SCHEDULE: MODE_AUTO, 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 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"]) 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): """Create a Tado water heater device.""" capabilities = tado.get_capabilities(zone_id) + supports_temperature_control = capabilities["canSetTemperature"] 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._device_is_active = False - self._is_away = False self._supports_temperature_control = supports_temperature_control self._min_temperature = min_temp @@ -110,29 +114,25 @@ class TadoWaterHeater(WaterHeaterDevice): if self._supports_temperature_control: self._supported_features |= SUPPORT_TARGET_TEMPERATURE - if tado.fallback: - # 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._current_tado_hvac_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): """Register for sensor updates.""" - @callback - def async_update_callback(): - """Schedule an entity update.""" - self.async_schedule_update_ha_state(True) - - async_dispatcher_connect( + self._undo_dispatcher = async_dispatcher_connect( self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id), - async_update_callback, + self._async_update_callback, ) + self._async_update_data() @property def supported_features(self): @@ -157,17 +157,17 @@ class TadoWaterHeater(WaterHeaterDevice): @property def current_operation(self): """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 def target_temperature(self): """Return the temperature we try to reach.""" - return self._target_temp + return self._tado_zone_data.target_temp @property def is_away_mode_on(self): """Return true if away mode is on.""" - return self._is_away + return self._tado_zone_data.is_away @property def operation_list(self): @@ -198,16 +198,9 @@ class TadoWaterHeater(WaterHeaterDevice): elif operation_mode == MODE_AUTO: mode = CONST_MODE_SMART_SCHEDULE elif operation_mode == MODE_HEAT: - mode = self._default_overlay + mode = CONST_MODE_HEAT - self._current_operation = 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() + self._control_heater(hvac_mode=mode) def set_temperature(self, **kwargs): """Set new target temperature.""" @@ -215,88 +208,75 @@ class TadoWaterHeater(WaterHeaterDevice): if not self._supports_temperature_control or temperature is None: return - self._current_operation = self._default_overlay - self._overlay_mode = None - self._target_temp = temperature - self._control_heater() - - 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 + if self._current_tado_hvac_mode not in ( + CONST_MODE_OFF, + CONST_MODE_AUTO, + CONST_MODE_SMART_SCHEDULE, ): - setting = float(data["setting"]["temperature"]["celsius"]) - self._target_temp = setting + self._control_heater(target_temp=temperature) + return - overlay = False - overlay_data = None - termination = CONST_MODE_SMART_SCHEDULE + self._control_heater(target_temp=temperature, hvac_mode=CONST_MODE_HEAT) - if "overlay" in data: - overlay_data = data["overlay"] - overlay = overlay_data is not None + @callback + def _async_update_callback(self): + """Load tado data and update state.""" + self._async_update_data() + self.async_write_ha_state() - if overlay: - termination = overlay_data["termination"]["type"] + @callback + 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: - # 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): + def _control_heater(self, hvac_mode=None, target_temp=None): """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( "Switching to SMART_SCHEDULE for zone %s (%d)", self.zone_name, self.zone_id, ) self._tado.reset_zone_overlay(self.zone_id) - self._overlay_mode = self._current_operation return - if self._current_operation == CONST_MODE_OFF: + if self._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, TYPE_HOT_WATER) - self._overlay_mode = self._current_operation 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( "Switching to %s for zone %s (%d) with temperature %s", - self._current_operation, + self._current_tado_hvac_mode, self.zone_name, self.zone_id, self._target_temp, ) self._tado.set_zone_overlay( - self.zone_id, - self._current_operation, - self._target_temp, - None, - TYPE_HOT_WATER, + zone_id=self.zone_id, + overlay_mode=overlay_mode, + temperature=self._target_temp, + duration=None, + device_type=TYPE_HOT_WATER, ) - self._overlay_mode = self._current_operation + self._overlay_mode = self._current_tado_hvac_mode diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8ff29221ac..0f0580f8c09 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,6 +616,9 @@ python-miio==0.4.8 # homeassistant.components.nest python-nest==4.1.0 +# homeassistant.components.tado +python-tado==0.5.0 + # homeassistant.components.twitch python-twitch-client==0.6.0 diff --git a/tests/components/tado/__init__.py b/tests/components/tado/__init__.py new file mode 100644 index 00000000000..11d199f01a1 --- /dev/null +++ b/tests/components/tado/__init__.py @@ -0,0 +1 @@ +"""Tests for the tado integration.""" diff --git a/tests/components/tado/test_climate.py b/tests/components/tado/test_climate.py new file mode 100644 index 00000000000..602f4d8424f --- /dev/null +++ b/tests/components/tado/test_climate.py @@ -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()) diff --git a/tests/components/tado/test_sensor.py b/tests/components/tado/test_sensor.py new file mode 100644 index 00000000000..2ea2c0508ee --- /dev/null +++ b/tests/components/tado/test_sensor.py @@ -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" diff --git a/tests/components/tado/test_water_heater.py b/tests/components/tado/test_water_heater.py new file mode 100644 index 00000000000..03dfaaef0df --- /dev/null +++ b/tests/components/tado/test_water_heater.py @@ -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()) diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py new file mode 100644 index 00000000000..7ee4c17058d --- /dev/null +++ b/tests/components/tado/util.py @@ -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() diff --git a/tests/fixtures/tado/ac_issue_32294.heat_mode.json b/tests/fixtures/tado/ac_issue_32294.heat_mode.json new file mode 100644 index 00000000000..098afd018aa --- /dev/null +++ b/tests/fixtures/tado/ac_issue_32294.heat_mode.json @@ -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 + } + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/devices.json b/tests/fixtures/tado/devices.json new file mode 100644 index 00000000000..5fc43adc903 --- /dev/null +++ b/tests/fixtures/tado/devices.json @@ -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" + } +] diff --git a/tests/fixtures/tado/hvac_action_heat.json b/tests/fixtures/tado/hvac_action_heat.json new file mode 100644 index 00000000000..9cbf1fd5f82 --- /dev/null +++ b/tests/fixtures/tado/hvac_action_heat.json @@ -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" + } + } +} diff --git a/tests/fixtures/tado/me.json b/tests/fixtures/tado/me.json new file mode 100644 index 00000000000..4707b3f04d4 --- /dev/null +++ b/tests/fixtures/tado/me.json @@ -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" +} diff --git a/tests/fixtures/tado/smartac3.auto_mode.json b/tests/fixtures/tado/smartac3.auto_mode.json new file mode 100644 index 00000000000..254b409ddd9 --- /dev/null +++ b/tests/fixtures/tado/smartac3.auto_mode.json @@ -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" + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/smartac3.cool_mode.json b/tests/fixtures/tado/smartac3.cool_mode.json new file mode 100644 index 00000000000..a7db2cc75bc --- /dev/null +++ b/tests/fixtures/tado/smartac3.cool_mode.json @@ -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 + } + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/smartac3.dry_mode.json b/tests/fixtures/tado/smartac3.dry_mode.json new file mode 100644 index 00000000000..d04612d1105 --- /dev/null +++ b/tests/fixtures/tado/smartac3.dry_mode.json @@ -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" + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/smartac3.fan_mode.json b/tests/fixtures/tado/smartac3.fan_mode.json new file mode 100644 index 00000000000..6907c31c517 --- /dev/null +++ b/tests/fixtures/tado/smartac3.fan_mode.json @@ -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" + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/smartac3.heat_mode.json b/tests/fixtures/tado/smartac3.heat_mode.json new file mode 100644 index 00000000000..06b5a350d83 --- /dev/null +++ b/tests/fixtures/tado/smartac3.heat_mode.json @@ -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 + } + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/smartac3.hvac_off.json b/tests/fixtures/tado/smartac3.hvac_off.json new file mode 100644 index 00000000000..83e9d1a83d5 --- /dev/null +++ b/tests/fixtures/tado/smartac3.hvac_off.json @@ -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" + } +} diff --git a/tests/fixtures/tado/smartac3.manual_off.json b/tests/fixtures/tado/smartac3.manual_off.json new file mode 100644 index 00000000000..a9538f30dbe --- /dev/null +++ b/tests/fixtures/tado/smartac3.manual_off.json @@ -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" + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/smartac3.offline.json b/tests/fixtures/tado/smartac3.offline.json new file mode 100644 index 00000000000..fda1e6468eb --- /dev/null +++ b/tests/fixtures/tado/smartac3.offline.json @@ -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 + } + } +} diff --git a/tests/fixtures/tado/smartac3.smart_mode.json b/tests/fixtures/tado/smartac3.smart_mode.json new file mode 100644 index 00000000000..357a1a96658 --- /dev/null +++ b/tests/fixtures/tado/smartac3.smart_mode.json @@ -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 + } + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/smartac3.turning_off.json b/tests/fixtures/tado/smartac3.turning_off.json new file mode 100644 index 00000000000..0c16f85811a --- /dev/null +++ b/tests/fixtures/tado/smartac3.turning_off.json @@ -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" + } + } +} diff --git a/tests/fixtures/tado/tadov2.heating.auto_mode.json b/tests/fixtures/tado/tadov2.heating.auto_mode.json new file mode 100644 index 00000000000..34464051f1e --- /dev/null +++ b/tests/fixtures/tado/tadov2.heating.auto_mode.json @@ -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" + } + } +} diff --git a/tests/fixtures/tado/tadov2.heating.manual_mode.json b/tests/fixtures/tado/tadov2.heating.manual_mode.json new file mode 100644 index 00000000000..a62499d7dd4 --- /dev/null +++ b/tests/fixtures/tado/tadov2.heating.manual_mode.json @@ -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" + } + } +} diff --git a/tests/fixtures/tado/tadov2.heating.off_mode.json b/tests/fixtures/tado/tadov2.heating.off_mode.json new file mode 100644 index 00000000000..e22805abc73 --- /dev/null +++ b/tests/fixtures/tado/tadov2.heating.off_mode.json @@ -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" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/tadov2.water_heater.auto_mode.json b/tests/fixtures/tado/tadov2.water_heater.auto_mode.json new file mode 100644 index 00000000000..7df4e3f5ea6 --- /dev/null +++ b/tests/fixtures/tado/tadov2.water_heater.auto_mode.json @@ -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": {} +} diff --git a/tests/fixtures/tado/tadov2.water_heater.heating.json b/tests/fixtures/tado/tadov2.water_heater.heating.json new file mode 100644 index 00000000000..8eecc79d63c --- /dev/null +++ b/tests/fixtures/tado/tadov2.water_heater.heating.json @@ -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" + } +} diff --git a/tests/fixtures/tado/tadov2.water_heater.manual_mode.json b/tests/fixtures/tado/tadov2.water_heater.manual_mode.json new file mode 100644 index 00000000000..21972a55d6d --- /dev/null +++ b/tests/fixtures/tado/tadov2.water_heater.manual_mode.json @@ -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": {} +} diff --git a/tests/fixtures/tado/tadov2.water_heater.off_mode.json b/tests/fixtures/tado/tadov2.water_heater.off_mode.json new file mode 100644 index 00000000000..12698db601b --- /dev/null +++ b/tests/fixtures/tado/tadov2.water_heater.off_mode.json @@ -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": {} +} diff --git a/tests/fixtures/tado/tadov2.zone_capabilities.json b/tests/fixtures/tado/tadov2.zone_capabilities.json new file mode 100644 index 00000000000..a908b699e64 --- /dev/null +++ b/tests/fixtures/tado/tadov2.zone_capabilities.json @@ -0,0 +1,19 @@ +{ + "type" : "HEATING", + "HEAT" : { + "temperatures" : { + "celsius" : { + "max" : 31, + "step" : 1, + "min" : 16 + }, + "fahrenheit" : { + "step" : 1, + "max" : 88, + "min" : 61 + } + } + }, + "AUTO" : {}, + "FAN" : {} +} diff --git a/tests/fixtures/tado/token.json b/tests/fixtures/tado/token.json new file mode 100644 index 00000000000..1e0089a1c9a --- /dev/null +++ b/tests/fixtures/tado/token.json @@ -0,0 +1,8 @@ +{ + "expires_in" : 599, + "scope" : "home.user", + "token_type" : "bearer", + "refresh_token" : "refresh", + "access_token" : "access", + "jti" : "jti" +} diff --git a/tests/fixtures/tado/water_heater_zone_capabilities.json b/tests/fixtures/tado/water_heater_zone_capabilities.json new file mode 100644 index 00000000000..f3f0daa6c09 --- /dev/null +++ b/tests/fixtures/tado/water_heater_zone_capabilities.json @@ -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 + } + } +} diff --git a/tests/fixtures/tado/zone_capabilities.json b/tests/fixtures/tado/zone_capabilities.json new file mode 100644 index 00000000000..8435094ecca --- /dev/null +++ b/tests/fixtures/tado/zone_capabilities.json @@ -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" + ] + } +} diff --git a/tests/fixtures/tado/zone_state.json b/tests/fixtures/tado/zone_state.json new file mode 100644 index 00000000000..c206dc9d081 --- /dev/null +++ b/tests/fixtures/tado/zone_state.json @@ -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 +} diff --git a/tests/fixtures/tado/zones.json b/tests/fixtures/tado/zones.json new file mode 100644 index 00000000000..8d7265ade50 --- /dev/null +++ b/tests/fixtures/tado/zones.json @@ -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 + } +]