hass-core/homeassistant/components/climate/tado.py
Nigel Rook c204a7c787 Tado fixes (#11294)
* 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
2018-02-02 17:28:54 -08:00

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