Home Assistant Unofficial Reference 2024.12.1
water_heater.py
Go to the documentation of this file.
1 """Support for Tado hot water zones."""
2 
3 import logging
4 from typing import Any
5 
6 import voluptuous as vol
7 
9  WaterHeaterEntity,
10  WaterHeaterEntityFeature,
11 )
12 from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
13 from homeassistant.core import HomeAssistant, callback
14 from homeassistant.helpers import config_validation as cv, entity_platform
15 from homeassistant.helpers.dispatcher import async_dispatcher_connect
16 from homeassistant.helpers.entity_platform import AddEntitiesCallback
17 from homeassistant.helpers.typing import VolDictType
18 
19 from . import TadoConfigEntry
20 from .const import (
21  CONST_HVAC_HEAT,
22  CONST_MODE_AUTO,
23  CONST_MODE_HEAT,
24  CONST_MODE_OFF,
25  CONST_MODE_SMART_SCHEDULE,
26  CONST_OVERLAY_MANUAL,
27  CONST_OVERLAY_TADO_MODE,
28  CONST_OVERLAY_TIMER,
29  SIGNAL_TADO_UPDATE_RECEIVED,
30  TYPE_HOT_WATER,
31 )
32 from .entity import TadoZoneEntity
33 from .helper import decide_duration, decide_overlay_mode
34 from .repairs import manage_water_heater_fallback_issue
35 from .tado_connector import TadoConnector
36 
37 _LOGGER = logging.getLogger(__name__)
38 
39 MODE_AUTO = "auto"
40 MODE_HEAT = "heat"
41 MODE_OFF = "off"
42 
43 OPERATION_MODES = [MODE_AUTO, MODE_HEAT, MODE_OFF]
44 
45 WATER_HEATER_MAP_TADO = {
46  CONST_OVERLAY_MANUAL: MODE_HEAT,
47  CONST_OVERLAY_TIMER: MODE_HEAT,
48  CONST_OVERLAY_TADO_MODE: MODE_HEAT,
49  CONST_HVAC_HEAT: MODE_HEAT,
50  CONST_MODE_SMART_SCHEDULE: MODE_AUTO,
51  CONST_MODE_OFF: MODE_OFF,
52 }
53 
54 SERVICE_WATER_HEATER_TIMER = "set_water_heater_timer"
55 ATTR_TIME_PERIOD = "time_period"
56 
57 WATER_HEATER_TIMER_SCHEMA: VolDictType = {
58  vol.Required(ATTR_TIME_PERIOD, default="01:00:00"): vol.All(
59  cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds()
60  ),
61  vol.Optional(ATTR_TEMPERATURE): vol.Coerce(float),
62 }
63 
64 
66  hass: HomeAssistant, entry: TadoConfigEntry, async_add_entities: AddEntitiesCallback
67 ) -> None:
68  """Set up the Tado water heater platform."""
69 
70  tado = entry.runtime_data
71  entities = await hass.async_add_executor_job(_generate_entities, tado)
72 
73  platform = entity_platform.async_get_current_platform()
74 
75  platform.async_register_entity_service(
76  SERVICE_WATER_HEATER_TIMER,
77  WATER_HEATER_TIMER_SCHEMA,
78  "set_timer",
79  )
80 
81  async_add_entities(entities, True)
82 
84  hass=hass,
85  water_heater_names=[e.zone_name for e in entities],
86  integration_overlay_fallback=tado.fallback,
87  )
88 
89 
90 def _generate_entities(tado: TadoConnector) -> list:
91  """Create all water heater entities."""
92  entities = []
93 
94  for zone in tado.zones:
95  if zone["type"] == TYPE_HOT_WATER:
97  tado, zone["name"], zone["id"], str(zone["name"])
98  )
99  entities.append(entity)
100 
101  return entities
102 
103 
104 def create_water_heater_entity(tado: TadoConnector, name: str, zone_id: int, zone: str):
105  """Create a Tado water heater device."""
106  capabilities = tado.get_capabilities(zone_id)
107 
108  supports_temperature_control = capabilities["canSetTemperature"]
109 
110  if supports_temperature_control and "temperatures" in capabilities:
111  temperatures = capabilities["temperatures"]
112  min_temp = float(temperatures["celsius"]["min"])
113  max_temp = float(temperatures["celsius"]["max"])
114  else:
115  min_temp = None
116  max_temp = None
117 
118  return TadoWaterHeater(
119  tado,
120  name,
121  zone_id,
122  supports_temperature_control,
123  min_temp,
124  max_temp,
125  )
126 
127 
129  """Representation of a Tado water heater."""
130 
131  _attr_name = None
132  _attr_operation_list = OPERATION_MODES
133  _attr_temperature_unit = UnitOfTemperature.CELSIUS
134 
135  def __init__(
136  self,
137  tado: TadoConnector,
138  zone_name: str,
139  zone_id: int,
140  supports_temperature_control: bool,
141  min_temp,
142  max_temp,
143  ) -> None:
144  """Initialize of Tado water heater entity."""
145  self._tado_tado = tado
146  super().__init__(zone_name, tado.home_id, zone_id)
147 
148  self.zone_idzone_idzone_id = zone_id
149  self._attr_unique_id_attr_unique_id = f"{zone_id} {tado.home_id}"
150 
151  self._device_is_active_device_is_active = False
152 
153  self._supports_temperature_control_supports_temperature_control = supports_temperature_control
154  self._min_temperature_min_temperature = min_temp
155  self._max_temperature_max_temperature = max_temp
156 
157  self._target_temp_target_temp: float | None = None
158 
159  self._attr_supported_features_attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE
160  if self._supports_temperature_control_supports_temperature_control:
161  self._attr_supported_features_attr_supported_features |= WaterHeaterEntityFeature.TARGET_TEMPERATURE
162 
163  self._current_tado_hvac_mode_current_tado_hvac_mode = CONST_MODE_SMART_SCHEDULE
164  self._overlay_mode_overlay_mode = CONST_MODE_SMART_SCHEDULE
165  self._tado_zone_data_tado_zone_data: Any = None
166 
167  async def async_added_to_hass(self) -> None:
168  """Register for sensor updates."""
169  self.async_on_removeasync_on_remove(
171  self.hasshass,
172  SIGNAL_TADO_UPDATE_RECEIVED.format(
173  self._tado_tado.home_id, "zone", self.zone_idzone_idzone_id
174  ),
175  self._async_update_callback_async_update_callback,
176  )
177  )
178  self._async_update_data_async_update_data()
179 
180  @property
181  def current_operation(self) -> str | None:
182  """Return current readable operation mode."""
183  return WATER_HEATER_MAP_TADO.get(self._current_tado_hvac_mode_current_tado_hvac_mode)
184 
185  @property
186  def target_temperature(self) -> float | None:
187  """Return the temperature we try to reach."""
188  return self._tado_zone_data_tado_zone_data.target_temp
189 
190  @property
191  def is_away_mode_on(self) -> bool:
192  """Return true if away mode is on."""
193  return self._tado_zone_data_tado_zone_data.is_away
194 
195  @property
196  def min_temp(self) -> float:
197  """Return the minimum temperature."""
198  return self._min_temperature_min_temperature
199 
200  @property
201  def max_temp(self) -> float:
202  """Return the maximum temperature."""
203  return self._max_temperature_max_temperature
204 
205  def set_operation_mode(self, operation_mode: str) -> None:
206  """Set new operation mode."""
207  mode = None
208 
209  if operation_mode == MODE_OFF:
210  mode = CONST_MODE_OFF
211  elif operation_mode == MODE_AUTO:
212  mode = CONST_MODE_SMART_SCHEDULE
213  elif operation_mode == MODE_HEAT:
214  mode = CONST_MODE_HEAT
215 
216  self._control_heater_control_heater(hvac_mode=mode)
217 
218  def set_timer(self, time_period: int, temperature: float | None = None):
219  """Set the timer on the entity, and temperature if supported."""
220  if not self._supports_temperature_control_supports_temperature_control and temperature is not None:
221  temperature = None
222 
223  self._control_heater_control_heater(
224  hvac_mode=CONST_MODE_HEAT, target_temp=temperature, duration=time_period
225  )
226 
227  def set_temperature(self, **kwargs: Any) -> None:
228  """Set new target temperature."""
229  temperature = kwargs.get(ATTR_TEMPERATURE)
230  if not self._supports_temperature_control_supports_temperature_control or temperature is None:
231  return
232 
233  if self._current_tado_hvac_mode_current_tado_hvac_mode not in (
234  CONST_MODE_OFF,
235  CONST_MODE_AUTO,
236  CONST_MODE_SMART_SCHEDULE,
237  ):
238  self._control_heater_control_heater(target_temp=temperature)
239  return
240 
241  self._control_heater_control_heater(target_temp=temperature, hvac_mode=CONST_MODE_HEAT)
242 
243  @callback
244  def _async_update_callback(self) -> None:
245  """Load tado data and update state."""
246  self._async_update_data_async_update_data()
247  self.async_write_ha_stateasync_write_ha_state()
248 
249  @callback
250  def _async_update_data(self) -> None:
251  """Load tado data."""
252  _LOGGER.debug("Updating water_heater platform for zone %d", self.zone_idzone_idzone_id)
253  self._tado_zone_data_tado_zone_data = self._tado_tado.data["zone"][self.zone_idzone_idzone_id]
254  self._current_tado_hvac_mode_current_tado_hvac_mode = self._tado_zone_data_tado_zone_data.current_hvac_mode
255 
257  self,
258  hvac_mode: str | None = None,
259  target_temp: float | None = None,
260  duration: int | None = None,
261  ):
262  """Send new target temperature."""
263  if hvac_mode:
264  self._current_tado_hvac_mode_current_tado_hvac_mode = hvac_mode
265 
266  if target_temp:
267  self._target_temp_target_temp = target_temp
268 
269  # Set a target temperature if we don't have any
270  if self._target_temp_target_temp is None:
271  self._target_temp_target_temp = self.min_tempmin_tempmin_temp
272 
273  if self._current_tado_hvac_mode_current_tado_hvac_mode == CONST_MODE_SMART_SCHEDULE:
274  _LOGGER.debug(
275  "Switching to SMART_SCHEDULE for zone %s (%d)",
276  self.zone_namezone_name,
277  self.zone_idzone_idzone_id,
278  )
279  self._tado_tado.reset_zone_overlay(self.zone_idzone_idzone_id)
280  return
281 
282  if self._current_tado_hvac_mode_current_tado_hvac_mode == CONST_MODE_OFF:
283  _LOGGER.debug(
284  "Switching to OFF for zone %s (%d)", self.zone_namezone_name, self.zone_idzone_idzone_id
285  )
286  self._tado_tado.set_zone_off(self.zone_idzone_idzone_id, CONST_OVERLAY_MANUAL, TYPE_HOT_WATER)
287  return
288 
289  overlay_mode = decide_overlay_mode(
290  tado=self._tado_tado,
291  duration=duration,
292  zone_id=self.zone_idzone_idzone_id,
293  )
294  duration = decide_duration(
295  tado=self._tado_tado,
296  duration=duration,
297  zone_id=self.zone_idzone_idzone_id,
298  overlay_mode=overlay_mode,
299  )
300  _LOGGER.debug(
301  "Switching to %s for zone %s (%d) with temperature %s",
302  self._current_tado_hvac_mode_current_tado_hvac_mode,
303  self.zone_namezone_name,
304  self.zone_idzone_idzone_id,
305  self._target_temp_target_temp,
306  )
307  self._tado_tado.set_zone_overlay(
308  zone_id=self.zone_idzone_idzone_id,
309  overlay_mode=overlay_mode,
310  temperature=self._target_temp_target_temp,
311  duration=duration,
312  device_type=TYPE_HOT_WATER,
313  )
314  self._overlay_mode_overlay_mode = self._current_tado_hvac_mode_current_tado_hvac_mode
def set_timer(self, int time_period, float|None temperature=None)
None __init__(self, TadoConnector tado, str zone_name, int zone_id, bool supports_temperature_control, min_temp, max_temp)
def _control_heater(self, str|None hvac_mode=None, float|None target_temp=None, int|None duration=None)
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
None|int decide_duration(TadoConnector tado, int|None duration, int zone_id, str|None overlay_mode=None)
Definition: helper.py:39
str decide_overlay_mode(TadoConnector tado, int|None duration, int zone_id, str|None overlay_mode=None)
Definition: helper.py:16
None manage_water_heater_fallback_issue(HomeAssistant hass, list[str] water_heater_names, str|None integration_overlay_fallback)
Definition: repairs.py:18
def create_water_heater_entity(TadoConnector tado, str name, int zone_id, str zone)
None async_setup_entry(HomeAssistant hass, TadoConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: water_heater.py:67
list _generate_entities(TadoConnector tado)
Definition: water_heater.py:90
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103