Home Assistant Unofficial Reference 2024.12.1
climate.py
Go to the documentation of this file.
1 """Support for Tado thermostats."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 import logging
7 from typing import Any
8 
9 import PyTado
10 import voluptuous as vol
11 
13  FAN_AUTO,
14  PRESET_AWAY,
15  PRESET_HOME,
16  SWING_BOTH,
17  SWING_HORIZONTAL,
18  SWING_OFF,
19  SWING_ON,
20  SWING_VERTICAL,
21  ClimateEntity,
22  ClimateEntityFeature,
23  HVACAction,
24  HVACMode,
25 )
26 from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, UnitOfTemperature
27 from homeassistant.core import HomeAssistant, callback
28 from homeassistant.helpers import config_validation as cv, entity_platform
29 from homeassistant.helpers.dispatcher import async_dispatcher_connect
30 from homeassistant.helpers.entity_platform import AddEntitiesCallback
31 from homeassistant.helpers.typing import VolDictType
32 
33 from . import TadoConfigEntry, TadoConnector
34 from .const import (
35  CONST_EXCLUSIVE_OVERLAY_GROUP,
36  CONST_FAN_AUTO,
37  CONST_FAN_OFF,
38  CONST_MODE_AUTO,
39  CONST_MODE_COOL,
40  CONST_MODE_HEAT,
41  CONST_MODE_OFF,
42  CONST_MODE_SMART_SCHEDULE,
43  CONST_OVERLAY_MANUAL,
44  CONST_OVERLAY_TADO_OPTIONS,
45  DOMAIN,
46  HA_TERMINATION_DURATION,
47  HA_TERMINATION_TYPE,
48  HA_TO_TADO_FAN_MODE_MAP,
49  HA_TO_TADO_FAN_MODE_MAP_LEGACY,
50  HA_TO_TADO_HVAC_MODE_MAP,
51  ORDERED_KNOWN_TADO_MODES,
52  PRESET_AUTO,
53  SIGNAL_TADO_UPDATE_RECEIVED,
54  SUPPORT_PRESET_AUTO,
55  SUPPORT_PRESET_MANUAL,
56  TADO_DEFAULT_MAX_TEMP,
57  TADO_DEFAULT_MIN_TEMP,
58  TADO_FANLEVEL_SETTING,
59  TADO_FANSPEED_SETTING,
60  TADO_HORIZONTAL_SWING_SETTING,
61  TADO_HVAC_ACTION_TO_HA_HVAC_ACTION,
62  TADO_MODES_WITH_NO_TEMP_SETTING,
63  TADO_SWING_OFF,
64  TADO_SWING_ON,
65  TADO_SWING_SETTING,
66  TADO_TO_HA_FAN_MODE_MAP,
67  TADO_TO_HA_FAN_MODE_MAP_LEGACY,
68  TADO_TO_HA_HVAC_MODE_MAP,
69  TADO_TO_HA_OFFSET_MAP,
70  TADO_TO_HA_SWING_MODE_MAP,
71  TADO_VERTICAL_SWING_SETTING,
72  TEMP_OFFSET,
73  TYPE_AIR_CONDITIONING,
74  TYPE_HEATING,
75 )
76 from .entity import TadoZoneEntity
77 from .helper import decide_duration, decide_overlay_mode, generate_supported_fanmodes
78 
79 _LOGGER = logging.getLogger(__name__)
80 
81 SERVICE_CLIMATE_TIMER = "set_climate_timer"
82 ATTR_TIME_PERIOD = "time_period"
83 ATTR_REQUESTED_OVERLAY = "requested_overlay"
84 
85 CLIMATE_TIMER_SCHEMA: VolDictType = {
86  vol.Required(ATTR_TEMPERATURE): vol.Coerce(float),
87  vol.Exclusive(ATTR_TIME_PERIOD, CONST_EXCLUSIVE_OVERLAY_GROUP): vol.All(
88  cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds()
89  ),
90  vol.Exclusive(ATTR_REQUESTED_OVERLAY, CONST_EXCLUSIVE_OVERLAY_GROUP): vol.In(
91  CONST_OVERLAY_TADO_OPTIONS
92  ),
93 }
94 
95 SERVICE_TEMP_OFFSET = "set_climate_temperature_offset"
96 ATTR_OFFSET = "offset"
97 
98 CLIMATE_TEMP_OFFSET_SCHEMA: VolDictType = {
99  vol.Required(ATTR_OFFSET, default=0): vol.Coerce(float),
100 }
101 
102 
104  hass: HomeAssistant, entry: TadoConfigEntry, async_add_entities: AddEntitiesCallback
105 ) -> None:
106  """Set up the Tado climate platform."""
107 
108  tado = entry.runtime_data
109  entities = await hass.async_add_executor_job(_generate_entities, tado)
110 
111  platform = entity_platform.async_get_current_platform()
112 
113  platform.async_register_entity_service(
114  SERVICE_CLIMATE_TIMER,
115  CLIMATE_TIMER_SCHEMA,
116  "set_timer",
117  )
118 
119  platform.async_register_entity_service(
120  SERVICE_TEMP_OFFSET,
121  CLIMATE_TEMP_OFFSET_SCHEMA,
122  "set_temp_offset",
123  )
124 
125  async_add_entities(entities, True)
126 
127 
128 def _generate_entities(tado: TadoConnector) -> list[TadoClimate]:
129  """Create all climate entities."""
130  entities = []
131  for zone in tado.zones:
132  if zone["type"] in [TYPE_HEATING, TYPE_AIR_CONDITIONING]:
133  entity = create_climate_entity(
134  tado, zone["name"], zone["id"], zone["devices"][0]
135  )
136  if entity:
137  entities.append(entity)
138  return entities
139 
140 
142  tado: TadoConnector, name: str, zone_id: int, device_info: dict
143 ) -> TadoClimate | None:
144  """Create a Tado climate entity."""
145  capabilities = tado.get_capabilities(zone_id)
146  _LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities)
147 
148  zone_type = capabilities["type"]
149  support_flags = (
150  ClimateEntityFeature.PRESET_MODE
151  | ClimateEntityFeature.TARGET_TEMPERATURE
152  | ClimateEntityFeature.TURN_OFF
153  | ClimateEntityFeature.TURN_ON
154  )
155  supported_hvac_modes = [
156  TADO_TO_HA_HVAC_MODE_MAP[CONST_MODE_OFF],
157  TADO_TO_HA_HVAC_MODE_MAP[CONST_MODE_SMART_SCHEDULE],
158  ]
159  supported_fan_modes = None
160  supported_swing_modes = None
161  heat_temperatures = None
162  cool_temperatures = None
163 
164  if zone_type == TYPE_AIR_CONDITIONING:
165  # Heat is preferred as it generally has a lower minimum temperature
166  for mode in ORDERED_KNOWN_TADO_MODES:
167  if mode not in capabilities:
168  continue
169 
170  supported_hvac_modes.append(TADO_TO_HA_HVAC_MODE_MAP[mode])
171  if (
172  TADO_SWING_SETTING in capabilities[mode]
173  or TADO_VERTICAL_SWING_SETTING in capabilities[mode]
174  or TADO_VERTICAL_SWING_SETTING in capabilities[mode]
175  ):
176  support_flags |= ClimateEntityFeature.SWING_MODE
177  supported_swing_modes = []
178  if TADO_SWING_SETTING in capabilities[mode]:
179  supported_swing_modes.append(
180  TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_ON]
181  )
182  if TADO_VERTICAL_SWING_SETTING in capabilities[mode]:
183  supported_swing_modes.append(SWING_VERTICAL)
184  if TADO_HORIZONTAL_SWING_SETTING in capabilities[mode]:
185  supported_swing_modes.append(SWING_HORIZONTAL)
186  if (
187  SWING_HORIZONTAL in supported_swing_modes
188  and SWING_VERTICAL in supported_swing_modes
189  ):
190  supported_swing_modes.append(SWING_BOTH)
191  supported_swing_modes.append(TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_OFF])
192 
193  if (
194  TADO_FANSPEED_SETTING not in capabilities[mode]
195  and TADO_FANLEVEL_SETTING not in capabilities[mode]
196  ):
197  continue
198 
199  support_flags |= ClimateEntityFeature.FAN_MODE
200 
201  if supported_fan_modes:
202  continue
203 
204  if TADO_FANSPEED_SETTING in capabilities[mode]:
205  supported_fan_modes = generate_supported_fanmodes(
206  TADO_TO_HA_FAN_MODE_MAP_LEGACY,
207  capabilities[mode][TADO_FANSPEED_SETTING],
208  )
209 
210  else:
211  supported_fan_modes = generate_supported_fanmodes(
212  TADO_TO_HA_FAN_MODE_MAP, capabilities[mode][TADO_FANLEVEL_SETTING]
213  )
214 
215  cool_temperatures = capabilities[CONST_MODE_COOL]["temperatures"]
216  else:
217  supported_hvac_modes.append(HVACMode.HEAT)
218 
219  if CONST_MODE_HEAT in capabilities:
220  heat_temperatures = capabilities[CONST_MODE_HEAT]["temperatures"]
221 
222  if heat_temperatures is None and "temperatures" in capabilities:
223  heat_temperatures = capabilities["temperatures"]
224 
225  if cool_temperatures is None and heat_temperatures is None:
226  _LOGGER.debug("Not adding zone %s since it has no temperatures", name)
227  return None
228 
229  heat_min_temp = None
230  heat_max_temp = None
231  heat_step = None
232  cool_min_temp = None
233  cool_max_temp = None
234  cool_step = None
235 
236  if heat_temperatures is not None:
237  heat_min_temp = float(heat_temperatures["celsius"]["min"])
238  heat_max_temp = float(heat_temperatures["celsius"]["max"])
239  heat_step = heat_temperatures["celsius"].get("step", PRECISION_TENTHS)
240 
241  if cool_temperatures is not None:
242  cool_min_temp = float(cool_temperatures["celsius"]["min"])
243  cool_max_temp = float(cool_temperatures["celsius"]["max"])
244  cool_step = cool_temperatures["celsius"].get("step", PRECISION_TENTHS)
245 
246  return TadoClimate(
247  tado,
248  name,
249  zone_id,
250  zone_type,
251  supported_hvac_modes,
252  support_flags,
253  device_info,
254  heat_min_temp,
255  heat_max_temp,
256  heat_step,
257  cool_min_temp,
258  cool_max_temp,
259  cool_step,
260  supported_fan_modes,
261  supported_swing_modes,
262  )
263 
264 
266  """Representation of a Tado climate entity."""
267 
268  _attr_temperature_unit = UnitOfTemperature.CELSIUS
269  _attr_name = None
270  _attr_translation_key = DOMAIN
271  _available = False
272  _enable_turn_on_off_backwards_compatibility = False
273 
274  def __init__(
275  self,
276  tado: TadoConnector,
277  zone_name: str,
278  zone_id: int,
279  zone_type: str,
280  supported_hvac_modes: list[HVACMode],
281  support_flags: ClimateEntityFeature,
282  device_info: dict[str, str],
283  heat_min_temp: float | None = None,
284  heat_max_temp: float | None = None,
285  heat_step: float | None = None,
286  cool_min_temp: float | None = None,
287  cool_max_temp: float | None = None,
288  cool_step: float | None = None,
289  supported_fan_modes: list[str] | None = None,
290  supported_swing_modes: list[str] | None = None,
291  ) -> None:
292  """Initialize of Tado climate entity."""
293  self._tado_tado = tado
294  super().__init__(zone_name, tado.home_id, zone_id)
295 
296  self.zone_idzone_idzone_id = zone_id
297  self.zone_typezone_type = zone_type
298 
299  self._attr_unique_id_attr_unique_id = f"{zone_type} {zone_id} {tado.home_id}"
300 
301  self._device_info_device_info = device_info
302  self._device_id_device_id = self._device_info_device_info["shortSerialNo"]
303 
304  self._ac_device_ac_device = zone_type == TYPE_AIR_CONDITIONING
305  self._attr_hvac_modes_attr_hvac_modes = supported_hvac_modes
306  self._attr_fan_modes_attr_fan_modes = supported_fan_modes
307  self._attr_supported_features_attr_supported_features = support_flags
308 
309  self._cur_temp_cur_temp = None
310  self._cur_humidity_cur_humidity = None
311  self._attr_swing_modes_attr_swing_modes = supported_swing_modes
312 
313  self._heat_min_temp_heat_min_temp = heat_min_temp
314  self._heat_max_temp_heat_max_temp = heat_max_temp
315  self._heat_step_heat_step = heat_step
316 
317  self._cool_min_temp_cool_min_temp = cool_min_temp
318  self._cool_max_temp_cool_max_temp = cool_max_temp
319  self._cool_step_cool_step = cool_step
320 
321  self._target_temp_target_temp: float | None = None
322 
323  self._current_tado_fan_speed_current_tado_fan_speed = CONST_FAN_OFF
324  self._current_tado_fan_level_current_tado_fan_level = CONST_FAN_OFF
325  self._current_tado_hvac_mode_current_tado_hvac_mode = CONST_MODE_OFF
326  self._current_tado_hvac_action_current_tado_hvac_action = HVACAction.OFF
327  self._current_tado_swing_mode_current_tado_swing_mode = TADO_SWING_OFF
328  self._current_tado_vertical_swing_current_tado_vertical_swing = TADO_SWING_OFF
329  self._current_tado_horizontal_swing_current_tado_horizontal_swing = TADO_SWING_OFF
330 
331  capabilities = tado.get_capabilities(zone_id)
332  self._current_tado_capabilities_current_tado_capabilities = capabilities
333 
334  self._tado_zone_data_tado_zone_data: PyTado.TadoZone = {}
335  self._tado_geofence_data_tado_geofence_data: dict[str, str] | None = None
336 
337  self._tado_zone_temp_offset: dict[str, Any] = {}
338 
339  self._async_update_home_data_async_update_home_data()
340  self._async_update_zone_data_async_update_zone_data()
341 
342  async def async_added_to_hass(self) -> None:
343  """Register for sensor updates."""
344  self.async_on_removeasync_on_remove(
346  self.hasshass,
347  SIGNAL_TADO_UPDATE_RECEIVED.format(self._tado_tado.home_id, "home", "data"),
348  self._async_update_home_callback_async_update_home_callback,
349  )
350  )
351 
352  self.async_on_removeasync_on_remove(
354  self.hasshass,
355  SIGNAL_TADO_UPDATE_RECEIVED.format(
356  self._tado_tado.home_id, "zone", self.zone_idzone_idzone_id
357  ),
358  self._async_update_zone_callback_async_update_zone_callback,
359  )
360  )
361 
362  @property
363  def current_humidity(self) -> int | None:
364  """Return the current humidity."""
365  return self._tado_zone_data_tado_zone_data.current_humidity
366 
367  @property
368  def current_temperature(self) -> float | None:
369  """Return the sensor temperature."""
370  return self._tado_zone_data_tado_zone_data.current_temp
371 
372  @property
373  def hvac_mode(self) -> HVACMode:
374  """Return hvac operation ie. heat, cool mode.
375 
376  Need to be one of HVAC_MODE_*.
377  """
378  return TADO_TO_HA_HVAC_MODE_MAP.get(self._current_tado_hvac_mode_current_tado_hvac_mode, HVACMode.OFF)
379 
380  @property
381  def hvac_action(self) -> HVACAction:
382  """Return the current running hvac operation if supported.
383 
384  Need to be one of CURRENT_HVAC_*.
385  """
386  return TADO_HVAC_ACTION_TO_HA_HVAC_ACTION.get(
387  self._tado_zone_data_tado_zone_data.current_hvac_action, HVACAction.OFF
388  )
389 
390  @property
391  def fan_mode(self) -> str | None:
392  """Return the fan setting."""
393  if self._ac_device_ac_device:
394  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING):
395  return TADO_TO_HA_FAN_MODE_MAP_LEGACY.get(
396  self._current_tado_fan_speed_current_tado_fan_speed, FAN_AUTO
397  )
398  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING):
399  return TADO_TO_HA_FAN_MODE_MAP.get(
400  self._current_tado_fan_level_current_tado_fan_level, FAN_AUTO
401  )
402  return FAN_AUTO
403  return None
404 
405  def set_fan_mode(self, fan_mode: str) -> None:
406  """Turn fan on/off."""
407  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING):
408  self._control_hvac_control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP_LEGACY[fan_mode])
409  elif self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING):
410  self._control_hvac_control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode])
411 
412  @property
413  def preset_mode(self) -> str:
414  """Return the current preset mode (home, away or auto)."""
415 
416  if (
417  self._tado_geofence_data_tado_geofence_data is not None
418  and "presenceLocked" in self._tado_geofence_data_tado_geofence_data
419  ):
420  if not self._tado_geofence_data_tado_geofence_data["presenceLocked"]:
421  return PRESET_AUTO
422  if self._tado_zone_data_tado_zone_data.is_away:
423  return PRESET_AWAY
424  return PRESET_HOME
425 
426  @property
427  def preset_modes(self) -> list[str]:
428  """Return a list of available preset modes."""
429  if self._tado_tado.get_auto_geofencing_supported():
430  return SUPPORT_PRESET_AUTO
431  return SUPPORT_PRESET_MANUAL
432 
433  def set_preset_mode(self, preset_mode: str) -> None:
434  """Set new preset mode."""
435  self._tado_tado.set_presence(preset_mode)
436 
437  @property
438  def target_temperature_step(self) -> float | None:
439  """Return the supported step of target temperature."""
440  if self._tado_zone_data_tado_zone_data.current_hvac_mode == CONST_MODE_COOL:
441  return self._cool_step_cool_step or self._heat_step_heat_step
442  return self._heat_step_heat_step or self._cool_step_cool_step
443 
444  @property
445  def target_temperature(self) -> float | None:
446  """Return the temperature we try to reach."""
447  # If the target temperature will be None
448  # if the device is performing an action
449  # that does not affect the temperature or
450  # the device is switching states
451  return self._tado_zone_data_tado_zone_data.target_temp or self._tado_zone_data_tado_zone_data.current_temp
452 
454  self,
455  temperature: float,
456  time_period: int | None = None,
457  requested_overlay: str | None = None,
458  ):
459  """Set the timer on the entity, and temperature if supported."""
460 
461  self._control_hvac_control_hvac(
462  hvac_mode=CONST_MODE_HEAT,
463  target_temp=temperature,
464  duration=time_period,
465  overlay_mode=requested_overlay,
466  )
467 
468  def set_temp_offset(self, offset: float) -> None:
469  """Set offset on the entity."""
470 
471  _LOGGER.debug(
472  "Setting temperature offset for device %s setting to (%.1f)",
473  self._device_id_device_id,
474  offset,
475  )
476 
477  self._tado_tado.set_temperature_offset(self._device_id_device_id, offset)
478 
479  def set_temperature(self, **kwargs: Any) -> None:
480  """Set new target temperature."""
481  if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
482  return
483 
484  if self._current_tado_hvac_mode_current_tado_hvac_mode not in (
485  CONST_MODE_OFF,
486  CONST_MODE_AUTO,
487  CONST_MODE_SMART_SCHEDULE,
488  ):
489  self._control_hvac_control_hvac(target_temp=temperature)
490  return
491 
492  new_hvac_mode = CONST_MODE_COOL if self._ac_device_ac_device else CONST_MODE_HEAT
493  self._control_hvac_control_hvac(target_temp=temperature, hvac_mode=new_hvac_mode)
494 
495  def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
496  """Set new target hvac mode."""
497  self._control_hvac_control_hvac(hvac_mode=HA_TO_TADO_HVAC_MODE_MAP[hvac_mode])
498 
499  @property
500  def available(self) -> bool:
501  """Return if the device is available."""
502  return self._tado_zone_data_tado_zone_data.available
503 
504  @property
505  def min_temp(self) -> float:
506  """Return the minimum temperature."""
507  if (
508  self._current_tado_hvac_mode_current_tado_hvac_mode == CONST_MODE_COOL
509  and self._cool_min_temp_cool_min_temp is not None
510  ):
511  return self._cool_min_temp_cool_min_temp
512  if self._heat_min_temp_heat_min_temp is not None:
513  return self._heat_min_temp_heat_min_temp
514 
515  return TADO_DEFAULT_MIN_TEMP
516 
517  @property
518  def max_temp(self) -> float:
519  """Return the maximum temperature."""
520  if (
521  self._current_tado_hvac_mode_current_tado_hvac_mode == CONST_MODE_HEAT
522  and self._heat_max_temp_heat_max_temp is not None
523  ):
524  return self._heat_max_temp_heat_max_temp
525  if self._heat_max_temp_heat_max_temp is not None:
526  return self._heat_max_temp_heat_max_temp
527 
528  return TADO_DEFAULT_MAX_TEMP
529 
530  @property
531  def swing_mode(self) -> str | None:
532  """Active swing mode for the device."""
533  swing_modes_tuple = (
534  self._current_tado_swing_mode_current_tado_swing_mode,
535  self._current_tado_vertical_swing_current_tado_vertical_swing,
536  self._current_tado_horizontal_swing_current_tado_horizontal_swing,
537  )
538  if swing_modes_tuple == (TADO_SWING_OFF, TADO_SWING_OFF, TADO_SWING_OFF):
539  return TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_OFF]
540  if swing_modes_tuple == (TADO_SWING_ON, TADO_SWING_OFF, TADO_SWING_OFF):
541  return TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_ON]
542  if swing_modes_tuple == (TADO_SWING_OFF, TADO_SWING_ON, TADO_SWING_OFF):
543  return SWING_VERTICAL
544  if swing_modes_tuple == (TADO_SWING_OFF, TADO_SWING_OFF, TADO_SWING_ON):
545  return SWING_HORIZONTAL
546  if swing_modes_tuple == (TADO_SWING_OFF, TADO_SWING_ON, TADO_SWING_ON):
547  return SWING_BOTH
548 
549  return TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_OFF]
550 
551  @property
552  def extra_state_attributes(self) -> Mapping[str, Any] | None:
553  """Return temperature offset."""
554  state_attr: dict[str, Any] = self._tado_zone_temp_offset
555  state_attr[HA_TERMINATION_TYPE] = (
556  self._tado_zone_data_tado_zone_data.default_overlay_termination_type
557  )
558  state_attr[HA_TERMINATION_DURATION] = (
559  self._tado_zone_data_tado_zone_data.default_overlay_termination_duration
560  )
561  return state_attr
562 
563  def set_swing_mode(self, swing_mode: str) -> None:
564  """Set swing modes for the device."""
565  vertical_swing = None
566  horizontal_swing = None
567  swing = None
568  if self._attr_swing_modes_attr_swing_modes is None:
569  return
570  if swing_mode == SWING_OFF:
571  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_SWING_SETTING):
572  swing = TADO_SWING_OFF
573  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
574  horizontal_swing = TADO_SWING_OFF
575  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
576  vertical_swing = TADO_SWING_OFF
577  if swing_mode == SWING_ON:
578  swing = TADO_SWING_ON
579  if swing_mode == SWING_VERTICAL:
580  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
581  vertical_swing = TADO_SWING_ON
582  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
583  horizontal_swing = TADO_SWING_OFF
584  if swing_mode == SWING_HORIZONTAL:
585  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
586  vertical_swing = TADO_SWING_OFF
587  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
588  horizontal_swing = TADO_SWING_ON
589  if swing_mode == SWING_BOTH:
590  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
591  vertical_swing = TADO_SWING_ON
592  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
593  horizontal_swing = TADO_SWING_ON
594 
595  self._control_hvac_control_hvac(
596  swing_mode=swing,
597  vertical_swing=vertical_swing,
598  horizontal_swing=horizontal_swing,
599  )
600 
601  @callback
602  def _async_update_zone_data(self) -> None:
603  """Load tado data into zone."""
604  self._tado_zone_data_tado_zone_data = self._tado_tado.data["zone"][self.zone_idzone_idzone_id]
605 
606  # Assign offset values to mapped attributes
607  for offset_key, attr in TADO_TO_HA_OFFSET_MAP.items():
608  if (
609  self._device_id_device_id in self._tado_tado.data["device"]
610  and offset_key
611  in self._tado_tado.data["device"][self._device_id_device_id][TEMP_OFFSET]
612  ):
613  self._tado_zone_temp_offset[attr] = self._tado_tado.data["device"][
614  self._device_id_device_id
615  ][TEMP_OFFSET][offset_key]
616 
617  self._current_tado_hvac_mode_current_tado_hvac_mode = self._tado_zone_data_tado_zone_data.current_hvac_mode
618  self._current_tado_hvac_action_current_tado_hvac_action = self._tado_zone_data_tado_zone_data.current_hvac_action
619 
620  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING):
621  self._current_tado_fan_level_current_tado_fan_level = self._tado_zone_data_tado_zone_data.current_fan_level
622  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING):
623  self._current_tado_fan_speed_current_tado_fan_speed = self._tado_zone_data_tado_zone_data.current_fan_speed
624  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_SWING_SETTING):
625  self._current_tado_swing_mode_current_tado_swing_mode = self._tado_zone_data_tado_zone_data.current_swing_mode
626  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
627  self._current_tado_vertical_swing_current_tado_vertical_swing = (
628  self._tado_zone_data_tado_zone_data.current_vertical_swing_mode
629  )
630  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
631  self._current_tado_horizontal_swing_current_tado_horizontal_swing = (
632  self._tado_zone_data_tado_zone_data.current_horizontal_swing_mode
633  )
634 
635  @callback
636  def _async_update_zone_callback(self) -> None:
637  """Load tado data and update state."""
638  self._async_update_zone_data_async_update_zone_data()
639  self.async_write_ha_stateasync_write_ha_state()
640 
641  @callback
642  def _async_update_home_data(self) -> None:
643  """Load tado geofencing data into zone."""
644  self._tado_geofence_data_tado_geofence_data = self._tado_tado.data["geofence"]
645 
646  @callback
647  def _async_update_home_callback(self) -> None:
648  """Load tado data and update state."""
649  self._async_update_home_data_async_update_home_data()
650  self.async_write_ha_stateasync_write_ha_state()
651 
653  def adjust_temp(min_temp, max_temp) -> float | None:
654  if max_temp is not None and self._target_temp_target_temp > max_temp:
655  return max_temp
656  if min_temp is not None and self._target_temp_target_temp < min_temp:
657  return min_temp
658  return self._target_temp_target_temp
659 
660  # Set a target temperature if we don't have any
661  # This can happen when we switch from Off to On
662  if self._target_temp_target_temp is None:
663  self._target_temp_target_temp = self._tado_zone_data_tado_zone_data.current_temp
664  elif self._current_tado_hvac_mode_current_tado_hvac_mode == CONST_MODE_COOL:
665  self._target_temp_target_temp = adjust_temp(self._cool_min_temp_cool_min_temp, self._cool_max_temp_cool_max_temp)
666  elif self._current_tado_hvac_mode_current_tado_hvac_mode == CONST_MODE_HEAT:
667  self._target_temp_target_temp = adjust_temp(self._heat_min_temp_heat_min_temp, self._heat_max_temp_heat_max_temp)
668 
670  self,
671  hvac_mode: str | None = None,
672  target_temp: float | None = None,
673  fan_mode: str | None = None,
674  swing_mode: str | None = None,
675  duration: int | None = None,
676  overlay_mode: str | None = None,
677  vertical_swing: str | None = None,
678  horizontal_swing: str | None = None,
679  ):
680  """Send new target temperature to Tado."""
681  if hvac_mode:
682  self._current_tado_hvac_mode_current_tado_hvac_mode = hvac_mode
683 
684  if target_temp:
685  self._target_temp_target_temp = target_temp
686 
687  if fan_mode:
688  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING):
689  self._current_tado_fan_speed_current_tado_fan_speed = fan_mode
690  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING):
691  self._current_tado_fan_level_current_tado_fan_level = fan_mode
692 
693  if swing_mode:
694  self._current_tado_swing_mode_current_tado_swing_mode = swing_mode
695 
696  if vertical_swing:
697  self._current_tado_vertical_swing_current_tado_vertical_swing = vertical_swing
698 
699  if horizontal_swing:
700  self._current_tado_horizontal_swing_current_tado_horizontal_swing = horizontal_swing
701 
702  self._normalize_target_temp_for_hvac_mode_normalize_target_temp_for_hvac_mode()
703 
704  # tado does not permit setting the fan speed to
705  # off, you must turn off the device
706  if (
707  self._current_tado_fan_speed_current_tado_fan_speed == CONST_FAN_OFF
708  and self._current_tado_hvac_mode_current_tado_hvac_mode != CONST_MODE_OFF
709  ):
710  self._current_tado_fan_speed_current_tado_fan_speed = CONST_FAN_AUTO
711 
712  if self._current_tado_hvac_mode_current_tado_hvac_mode == CONST_MODE_OFF:
713  _LOGGER.debug(
714  "Switching to OFF for zone %s (%d)", self.zone_namezone_name, self.zone_idzone_idzone_id
715  )
716  self._tado_tado.set_zone_off(self.zone_idzone_idzone_id, CONST_OVERLAY_MANUAL, self.zone_typezone_type)
717  return
718 
719  if self._current_tado_hvac_mode_current_tado_hvac_mode == CONST_MODE_SMART_SCHEDULE:
720  _LOGGER.debug(
721  "Switching to SMART_SCHEDULE for zone %s (%d)",
722  self.zone_namezone_name,
723  self.zone_idzone_idzone_id,
724  )
725  self._tado_tado.reset_zone_overlay(self.zone_idzone_idzone_id)
726  return
727 
728  overlay_mode = decide_overlay_mode(
729  tado=self._tado_tado,
730  duration=duration,
731  overlay_mode=overlay_mode,
732  zone_id=self.zone_idzone_idzone_id,
733  )
734  duration = decide_duration(
735  tado=self._tado_tado,
736  duration=duration,
737  zone_id=self.zone_idzone_idzone_id,
738  overlay_mode=overlay_mode,
739  )
740  _LOGGER.debug(
741  (
742  "Switching to %s for zone %s (%d) with temperature %s °C and duration"
743  " %s using overlay %s"
744  ),
745  self._current_tado_hvac_mode_current_tado_hvac_mode,
746  self.zone_namezone_name,
747  self.zone_idzone_idzone_id,
748  self._target_temp_target_temp,
749  duration,
750  overlay_mode,
751  )
752 
753  temperature_to_send = self._target_temp_target_temp
754  if self._current_tado_hvac_mode_current_tado_hvac_mode in TADO_MODES_WITH_NO_TEMP_SETTING:
755  # A temperature cannot be passed with these modes
756  temperature_to_send = None
757 
758  fan_speed = None
759  fan_level = None
760  if self.supported_featuressupported_featuressupported_features & ClimateEntityFeature.FAN_MODE:
761  if self._is_current_setting_supported_by_current_hvac_mode_is_current_setting_supported_by_current_hvac_mode(
762  TADO_FANSPEED_SETTING, self._current_tado_fan_speed_current_tado_fan_speed
763  ):
764  fan_speed = self._current_tado_fan_speed_current_tado_fan_speed
765  if self._is_current_setting_supported_by_current_hvac_mode_is_current_setting_supported_by_current_hvac_mode(
766  TADO_FANLEVEL_SETTING, self._current_tado_fan_level_current_tado_fan_level
767  ):
768  fan_level = self._current_tado_fan_level_current_tado_fan_level
769 
770  swing = None
771  vertical_swing = None
772  horizontal_swing = None
773  if (
774  self.supported_featuressupported_featuressupported_features & ClimateEntityFeature.SWING_MODE
775  ) and self._attr_swing_modes_attr_swing_modes is not None:
776  if self._is_current_setting_supported_by_current_hvac_mode_is_current_setting_supported_by_current_hvac_mode(
777  TADO_VERTICAL_SWING_SETTING, self._current_tado_vertical_swing_current_tado_vertical_swing
778  ):
779  vertical_swing = self._current_tado_vertical_swing_current_tado_vertical_swing
780  if self._is_current_setting_supported_by_current_hvac_mode_is_current_setting_supported_by_current_hvac_mode(
781  TADO_HORIZONTAL_SWING_SETTING, self._current_tado_horizontal_swing_current_tado_horizontal_swing
782  ):
783  horizontal_swing = self._current_tado_horizontal_swing_current_tado_horizontal_swing
784  if self._is_current_setting_supported_by_current_hvac_mode_is_current_setting_supported_by_current_hvac_mode(
785  TADO_SWING_SETTING, self._current_tado_swing_mode_current_tado_swing_mode
786  ):
787  swing = self._current_tado_swing_mode_current_tado_swing_mode
788 
789  self._tado_tado.set_zone_overlay(
790  zone_id=self.zone_idzone_idzone_id,
791  overlay_mode=overlay_mode, # What to do when the period ends
792  temperature=temperature_to_send,
793  duration=duration,
794  device_type=self.zone_typezone_type,
795  mode=self._current_tado_hvac_mode_current_tado_hvac_mode,
796  fan_speed=fan_speed, # api defaults to not sending fanSpeed if None specified
797  swing=swing, # api defaults to not sending swing if None specified
798  fan_level=fan_level, # api defaults to not sending fanLevel if fanSpeend not None
799  vertical_swing=vertical_swing, # api defaults to not sending verticalSwing if swing not None
800  horizontal_swing=horizontal_swing, # api defaults to not sending horizontalSwing if swing not None
801  )
802 
803  def _is_valid_setting_for_hvac_mode(self, setting: str) -> bool:
804  return (
805  self._current_tado_capabilities_current_tado_capabilities.get(self._current_tado_hvac_mode_current_tado_hvac_mode, {}).get(
806  setting
807  )
808  is not None
809  )
810 
812  self, setting: str, current_state: str | None
813  ) -> bool:
814  if self._is_valid_setting_for_hvac_mode_is_valid_setting_for_hvac_mode(setting):
815  return current_state in self._current_tado_capabilities_current_tado_capabilities[
816  self._current_tado_hvac_mode_current_tado_hvac_mode
817  ].get(setting, [])
818  return False
ClimateEntityFeature supported_features(self)
Definition: __init__.py:945
def _control_hvac(self, str|None hvac_mode=None, float|None target_temp=None, str|None fan_mode=None, str|None swing_mode=None, int|None duration=None, str|None overlay_mode=None, str|None vertical_swing=None, str|None horizontal_swing=None)
Definition: climate.py:679
bool _is_current_setting_supported_by_current_hvac_mode(self, str setting, str|None current_state)
Definition: climate.py:813
None __init__(self, TadoConnector tado, str zone_name, int zone_id, str zone_type, list[HVACMode] supported_hvac_modes, ClimateEntityFeature support_flags, dict[str, str] device_info, float|None heat_min_temp=None, float|None heat_max_temp=None, float|None heat_step=None, float|None cool_min_temp=None, float|None cool_max_temp=None, float|None cool_step=None, list[str]|None supported_fan_modes=None, list[str]|None supported_swing_modes=None)
Definition: climate.py:291
None set_hvac_mode(self, HVACMode hvac_mode)
Definition: climate.py:495
None set_swing_mode(self, str swing_mode)
Definition: climate.py:563
def set_timer(self, float temperature, int|None time_period=None, str|None requested_overlay=None)
Definition: climate.py:458
bool _is_valid_setting_for_hvac_mode(self, str setting)
Definition: climate.py:803
Mapping[str, Any]|None extra_state_attributes(self)
Definition: climate.py:552
None set_preset_mode(self, str preset_mode)
Definition: climate.py:433
int|None supported_features(self)
Definition: entity.py:861
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
list[TadoClimate] _generate_entities(TadoConnector tado)
Definition: climate.py:128
TadoClimate|None create_climate_entity(TadoConnector tado, str name, int zone_id, dict device_info)
Definition: climate.py:143
None async_setup_entry(HomeAssistant hass, TadoConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: climate.py:105
None|int decide_duration(TadoConnector tado, int|None duration, int zone_id, str|None overlay_mode=None)
Definition: helper.py:39
def generate_supported_fanmodes(dict[str, str] tado_to_ha_mapping, list[str] options)
Definition: helper.py:54
str decide_overlay_mode(TadoConnector tado, int|None duration, int zone_id, str|None overlay_mode=None)
Definition: helper.py:16
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103