* Fix tado overlay end state Previously, when tado ended an overlay state itself, say because a timer expired or a scheduled temperature change ocurred, the tado climate component would not return to Smart Schedule mode. This change fixes that issue * Correct tado state after multiple rapid updates Previosuly, making two changes to tado climate within 10 seconds, for example setting operation mode to Tado mode, then changing the temperature, would leave the entity showing the incorrect state for up to a minute. This change forces an unthrottled update after setting the climate state, which fixes the issue * Fix comment formatting
358 lines
11 KiB
Python
358 lines
11 KiB
Python
"""
|
|
Tado component to create a climate device for each zone.
|
|
|
|
For more details about this platform, please refer to the documentation at
|
|
https://home-assistant.io/components/climate.tado/
|
|
"""
|
|
import logging
|
|
|
|
from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS)
|
|
from homeassistant.components.climate import (
|
|
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
|
|
from homeassistant.const import ATTR_TEMPERATURE
|
|
from homeassistant.components.tado import DATA_TADO
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CONST_MODE_SMART_SCHEDULE = 'SMART_SCHEDULE' # Default mytado mode
|
|
CONST_MODE_OFF = 'OFF' # Switch off heating in a zone
|
|
|
|
# When we change the temperature setting, we need an overlay mode
|
|
# wait until tado changes the mode automatic
|
|
CONST_OVERLAY_TADO_MODE = 'TADO_MODE'
|
|
# the user has change the temperature or mode manually
|
|
CONST_OVERLAY_MANUAL = 'MANUAL'
|
|
# the temperature will be reset after a timespan
|
|
CONST_OVERLAY_TIMER = 'TIMER'
|
|
|
|
CONST_MODE_FAN_HIGH = 'HIGH'
|
|
CONST_MODE_FAN_MIDDLE = 'MIDDLE'
|
|
CONST_MODE_FAN_LOW = 'LOW'
|
|
|
|
FAN_MODES_LIST = {
|
|
CONST_MODE_FAN_HIGH: 'High',
|
|
CONST_MODE_FAN_MIDDLE: 'Middle',
|
|
CONST_MODE_FAN_LOW: 'Low',
|
|
CONST_MODE_OFF: 'Off',
|
|
}
|
|
|
|
OPERATION_LIST = {
|
|
CONST_OVERLAY_MANUAL: 'Manual',
|
|
CONST_OVERLAY_TIMER: 'Timer',
|
|
CONST_OVERLAY_TADO_MODE: 'Tado mode',
|
|
CONST_MODE_SMART_SCHEDULE: 'Smart schedule',
|
|
CONST_MODE_OFF: 'Off',
|
|
}
|
|
|
|
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
|
|
|
|
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
|
"""Set up the Tado climate platform."""
|
|
tado = hass.data[DATA_TADO]
|
|
|
|
try:
|
|
zones = tado.get_zones()
|
|
except RuntimeError:
|
|
_LOGGER.error("Unable to get zone info from mytado")
|
|
return
|
|
|
|
climate_devices = []
|
|
for zone in zones:
|
|
device = create_climate_device(
|
|
tado, hass, zone, zone['name'], zone['id'])
|
|
if not device:
|
|
continue
|
|
climate_devices.append(device)
|
|
|
|
if climate_devices:
|
|
add_devices(climate_devices, True)
|
|
|
|
|
|
def create_climate_device(tado, hass, zone, name, zone_id):
|
|
"""Create a Tado climate device."""
|
|
capabilities = tado.get_capabilities(zone_id)
|
|
|
|
unit = TEMP_CELSIUS
|
|
ac_mode = capabilities['type'] == 'AIR_CONDITIONING'
|
|
|
|
if ac_mode:
|
|
temperatures = capabilities['HEAT']['temperatures']
|
|
elif 'temperatures' in capabilities:
|
|
temperatures = capabilities['temperatures']
|
|
else:
|
|
_LOGGER.debug("Received zone %s has no temperature; not adding", name)
|
|
return
|
|
|
|
min_temp = float(temperatures['celsius']['min'])
|
|
max_temp = float(temperatures['celsius']['max'])
|
|
|
|
data_id = 'zone {} {}'.format(name, zone_id)
|
|
device = TadoClimate(tado,
|
|
name, zone_id, data_id,
|
|
hass.config.units.temperature(min_temp, unit),
|
|
hass.config.units.temperature(max_temp, unit),
|
|
ac_mode)
|
|
|
|
tado.add_sensor(data_id, {
|
|
'id': zone_id,
|
|
'zone': zone,
|
|
'name': name,
|
|
'climate': device
|
|
})
|
|
|
|
return device
|
|
|
|
|
|
class TadoClimate(ClimateDevice):
|
|
"""Representation of a tado climate device."""
|
|
|
|
def __init__(self, store, zone_name, zone_id, data_id,
|
|
min_temp, max_temp, ac_mode,
|
|
tolerance=0.3):
|
|
"""Initialize of Tado climate device."""
|
|
self._store = store
|
|
self._data_id = data_id
|
|
|
|
self.zone_name = zone_name
|
|
self.zone_id = zone_id
|
|
|
|
self.ac_mode = ac_mode
|
|
|
|
self._active = False
|
|
self._device_is_active = False
|
|
|
|
self._unit = TEMP_CELSIUS
|
|
self._cur_temp = None
|
|
self._cur_humidity = None
|
|
self._is_away = False
|
|
self._min_temp = min_temp
|
|
self._max_temp = max_temp
|
|
self._target_temp = None
|
|
self._tolerance = tolerance
|
|
self._cooling = False
|
|
|
|
self._current_fan = CONST_MODE_OFF
|
|
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
|
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
|
|
|
@property
|
|
def supported_features(self):
|
|
"""Return the list of supported features."""
|
|
return SUPPORT_FLAGS
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the device."""
|
|
return self.zone_name
|
|
|
|
@property
|
|
def current_humidity(self):
|
|
"""Return the current humidity."""
|
|
return self._cur_humidity
|
|
|
|
@property
|
|
def current_temperature(self):
|
|
"""Return the sensor temperature."""
|
|
return self._cur_temp
|
|
|
|
@property
|
|
def current_operation(self):
|
|
"""Return current readable operation mode."""
|
|
if self._cooling:
|
|
return "Cooling"
|
|
return OPERATION_LIST.get(self._current_operation)
|
|
|
|
@property
|
|
def operation_list(self):
|
|
"""Return the list of available operation modes (readable)."""
|
|
return list(OPERATION_LIST.values())
|
|
|
|
@property
|
|
def current_fan_mode(self):
|
|
"""Return the fan setting."""
|
|
if self.ac_mode:
|
|
return FAN_MODES_LIST.get(self._current_fan)
|
|
return None
|
|
|
|
@property
|
|
def fan_list(self):
|
|
"""List of available fan modes."""
|
|
if self.ac_mode:
|
|
return list(FAN_MODES_LIST.values())
|
|
return None
|
|
|
|
@property
|
|
def temperature_unit(self):
|
|
"""Return the unit of measurement used by the platform."""
|
|
return self._unit
|
|
|
|
@property
|
|
def is_away_mode_on(self):
|
|
"""Return true if away mode is on."""
|
|
return self._is_away
|
|
|
|
@property
|
|
def target_temperature_step(self):
|
|
"""Return the supported step of target temperature."""
|
|
return PRECISION_TENTHS
|
|
|
|
@property
|
|
def target_temperature(self):
|
|
"""Return the temperature we try to reach."""
|
|
return self._target_temp
|
|
|
|
def set_temperature(self, **kwargs):
|
|
"""Set new target temperature."""
|
|
temperature = kwargs.get(ATTR_TEMPERATURE)
|
|
if temperature is None:
|
|
return
|
|
|
|
self._current_operation = CONST_OVERLAY_TADO_MODE
|
|
self._overlay_mode = None
|
|
self._target_temp = temperature
|
|
self._control_heating()
|
|
|
|
def set_operation_mode(self, readable_operation_mode):
|
|
"""Set new operation mode."""
|
|
operation_mode = CONST_MODE_SMART_SCHEDULE
|
|
|
|
for mode, readable in OPERATION_LIST.items():
|
|
if readable == readable_operation_mode:
|
|
operation_mode = mode
|
|
break
|
|
|
|
self._current_operation = operation_mode
|
|
self._overlay_mode = None
|
|
self._control_heating()
|
|
|
|
@property
|
|
def min_temp(self):
|
|
"""Return the minimum temperature."""
|
|
if self._min_temp:
|
|
return self._min_temp
|
|
# get default temp from super class
|
|
return super().min_temp
|
|
|
|
@property
|
|
def max_temp(self):
|
|
"""Return the maximum temperature."""
|
|
if self._max_temp:
|
|
return self._max_temp
|
|
# Get default temp from super class
|
|
return super().max_temp
|
|
|
|
def update(self):
|
|
"""Update the state of this climate device."""
|
|
self._store.update()
|
|
|
|
data = self._store.get_data(self._data_id)
|
|
|
|
if data is None:
|
|
_LOGGER.debug("Received no data for zone %s", self.zone_name)
|
|
return
|
|
|
|
if 'sensorDataPoints' in data:
|
|
sensor_data = data['sensorDataPoints']
|
|
|
|
unit = TEMP_CELSIUS
|
|
|
|
if 'insideTemperature' in sensor_data:
|
|
temperature = float(
|
|
sensor_data['insideTemperature']['celsius'])
|
|
self._cur_temp = self.hass.config.units.temperature(
|
|
temperature, unit)
|
|
|
|
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:
|
|
setting = float(
|
|
data['setting']['temperature']['celsius'])
|
|
self._target_temp = self.hass.config.units.temperature(
|
|
setting, unit)
|
|
|
|
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
|
|
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
|
|
|
|
overlay = False
|
|
overlay_data = None
|
|
termination = CONST_MODE_SMART_SCHEDULE
|
|
cooling = False
|
|
fan_speed = CONST_MODE_OFF
|
|
|
|
if 'overlay' in data:
|
|
overlay_data = data['overlay']
|
|
overlay = overlay_data is not None
|
|
|
|
if overlay:
|
|
termination = overlay_data['termination']['type']
|
|
|
|
if 'setting' in overlay_data:
|
|
setting_data = overlay_data['setting']
|
|
setting = setting_data is not None
|
|
|
|
if setting:
|
|
if 'mode' in setting_data:
|
|
cooling = setting_data['mode'] == 'COOL'
|
|
|
|
if 'fanSpeed' in setting_data:
|
|
fan_speed = setting_data['fanSpeed']
|
|
|
|
if self._device_is_active:
|
|
# If you set mode manually to off, there will be an overlay
|
|
# and a termination, but we want to see the mode "OFF"
|
|
self._overlay_mode = termination
|
|
self._current_operation = termination
|
|
|
|
self._cooling = cooling
|
|
self._current_fan = fan_speed
|
|
|
|
def _control_heating(self):
|
|
"""Send new target temperature to mytado."""
|
|
if not self._active and None not in (
|
|
self._cur_temp, self._target_temp):
|
|
self._active = True
|
|
_LOGGER.info("Obtained current and target temperature. "
|
|
"Tado thermostat active")
|
|
|
|
if not self._active or self._current_operation == self._overlay_mode:
|
|
return
|
|
|
|
if self._current_operation == CONST_MODE_SMART_SCHEDULE:
|
|
_LOGGER.info("Switching mytado.com to SCHEDULE (default) "
|
|
"for zone %s", self.zone_name)
|
|
self._store.reset_zone_overlay(self.zone_id)
|
|
self._overlay_mode = self._current_operation
|
|
return
|
|
|
|
if self._current_operation == CONST_MODE_OFF:
|
|
_LOGGER.info("Switching mytado.com to OFF for zone %s",
|
|
self.zone_name)
|
|
self._store.set_zone_overlay(self.zone_id, CONST_OVERLAY_MANUAL)
|
|
self._overlay_mode = self._current_operation
|
|
return
|
|
|
|
_LOGGER.info("Switching mytado.com to %s mode for zone %s",
|
|
self._current_operation, self.zone_name)
|
|
self._store.set_zone_overlay(
|
|
self.zone_id, self._current_operation, self._target_temp)
|
|
|
|
self._overlay_mode = self._current_operation
|