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
pull/33434/head
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/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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
}
]