Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for water heater devices."""
2 
3 from __future__ import annotations
4 
5 from datetime import timedelta
6 from enum import IntFlag
7 import functools as ft
8 import logging
9 from typing import Any, final
10 
11 from propcache import cached_property
12 import voluptuous as vol
13 
14 from homeassistant.config_entries import ConfigEntry
15 from homeassistant.const import (
16  ATTR_TEMPERATURE,
17  PRECISION_TENTHS,
18  PRECISION_WHOLE,
19  SERVICE_TURN_OFF,
20  SERVICE_TURN_ON,
21  STATE_OFF,
22  STATE_ON,
23  UnitOfTemperature,
24 )
25 from homeassistant.core import HomeAssistant, ServiceCall
26 from homeassistant.exceptions import ServiceValidationError
27 from homeassistant.helpers import config_validation as cv
29  DeprecatedConstantEnum,
30  all_with_deprecated_constants,
31  check_if_deprecated_constant,
32  dir_with_deprecated_constants,
33 )
34 from homeassistant.helpers.entity import Entity, EntityDescription
35 from homeassistant.helpers.entity_component import EntityComponent
36 from homeassistant.helpers.temperature import display_temp as show_temp
37 from homeassistant.helpers.typing import ConfigType, VolDictType
38 from homeassistant.util.hass_dict import HassKey
39 from homeassistant.util.unit_conversion import TemperatureConverter
40 
41 from .const import DOMAIN
42 
43 DATA_COMPONENT: HassKey[EntityComponent[WaterHeaterEntity]] = HassKey(DOMAIN)
44 ENTITY_ID_FORMAT = DOMAIN + ".{}"
45 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
46 PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
47 SCAN_INTERVAL = timedelta(seconds=60)
48 
49 DEFAULT_MIN_TEMP = 110
50 DEFAULT_MAX_TEMP = 140
51 
52 SERVICE_SET_AWAY_MODE = "set_away_mode"
53 SERVICE_SET_TEMPERATURE = "set_temperature"
54 SERVICE_SET_OPERATION_MODE = "set_operation_mode"
55 
56 STATE_ECO = "eco"
57 STATE_ELECTRIC = "electric"
58 STATE_PERFORMANCE = "performance"
59 STATE_HIGH_DEMAND = "high_demand"
60 STATE_HEAT_PUMP = "heat_pump"
61 STATE_GAS = "gas"
62 
63 
64 class WaterHeaterEntityFeature(IntFlag):
65  """Supported features of the fan entity."""
66 
67  TARGET_TEMPERATURE = 1
68  OPERATION_MODE = 2
69  AWAY_MODE = 4
70  ON_OFF = 8
71 
72 
73 # These SUPPORT_* constants are deprecated as of Home Assistant 2022.5.
74 # Please use the WaterHeaterEntityFeature enum instead.
75 _DEPRECATED_SUPPORT_TARGET_TEMPERATURE = DeprecatedConstantEnum(
76  WaterHeaterEntityFeature.TARGET_TEMPERATURE, "2025.1"
77 )
78 _DEPRECATED_SUPPORT_OPERATION_MODE = DeprecatedConstantEnum(
79  WaterHeaterEntityFeature.OPERATION_MODE, "2025.1"
80 )
81 _DEPRECATED_SUPPORT_AWAY_MODE = DeprecatedConstantEnum(
82  WaterHeaterEntityFeature.AWAY_MODE, "2025.1"
83 )
84 
85 ATTR_MAX_TEMP = "max_temp"
86 ATTR_MIN_TEMP = "min_temp"
87 ATTR_AWAY_MODE = "away_mode"
88 ATTR_OPERATION_MODE = "operation_mode"
89 ATTR_OPERATION_LIST = "operation_list"
90 ATTR_TARGET_TEMP_HIGH = "target_temp_high"
91 ATTR_TARGET_TEMP_LOW = "target_temp_low"
92 ATTR_CURRENT_TEMPERATURE = "current_temperature"
93 
94 CONVERTIBLE_ATTRIBUTE = [ATTR_TEMPERATURE]
95 
96 _LOGGER = logging.getLogger(__name__)
97 
98 SET_AWAY_MODE_SCHEMA: VolDictType = {
99  vol.Required(ATTR_AWAY_MODE): cv.boolean,
100 }
101 SET_TEMPERATURE_SCHEMA: VolDictType = {
102  vol.Required(ATTR_TEMPERATURE, "temperature"): vol.Coerce(float),
103  vol.Optional(ATTR_OPERATION_MODE): cv.string,
104 }
105 SET_OPERATION_MODE_SCHEMA: VolDictType = {
106  vol.Required(ATTR_OPERATION_MODE): cv.string,
107 }
108 
109 # mypy: disallow-any-generics
110 
111 
112 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
113  """Set up water_heater devices."""
114  component = hass.data[DATA_COMPONENT] = EntityComponent[WaterHeaterEntity](
115  _LOGGER, DOMAIN, hass, SCAN_INTERVAL
116  )
117  await component.async_setup(config)
118 
119  component.async_register_entity_service(
120  SERVICE_TURN_ON, None, "async_turn_on", [WaterHeaterEntityFeature.ON_OFF]
121  )
122  component.async_register_entity_service(
123  SERVICE_TURN_OFF, None, "async_turn_off", [WaterHeaterEntityFeature.ON_OFF]
124  )
125  component.async_register_entity_service(
126  SERVICE_SET_AWAY_MODE, SET_AWAY_MODE_SCHEMA, async_service_away_mode
127  )
128  component.async_register_entity_service(
129  SERVICE_SET_TEMPERATURE, SET_TEMPERATURE_SCHEMA, async_service_temperature_set
130  )
131  component.async_register_entity_service(
132  SERVICE_SET_OPERATION_MODE,
133  SET_OPERATION_MODE_SCHEMA,
134  "async_handle_set_operation_mode",
135  )
136 
137  return True
138 
139 
140 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
141  """Set up a config entry."""
142  return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
143 
144 
145 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
146  """Unload a config entry."""
147  return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
148 
149 
151  """A class that describes water heater entities."""
152 
153 
154 CACHED_PROPERTIES_WITH_ATTR_ = {
155  "temperature_unit",
156  "current_operation",
157  "operation_list",
158  "current_temperature",
159  "target_temperature",
160  "target_temperature_high",
161  "target_temperature_low",
162  "is_away_mode_on",
163 }
164 
165 
166 class WaterHeaterEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
167  """Base class for water heater entities."""
168 
169  _entity_component_unrecorded_attributes = frozenset(
170  {ATTR_OPERATION_LIST, ATTR_MIN_TEMP, ATTR_MAX_TEMP}
171  )
172 
173  entity_description: WaterHeaterEntityEntityDescription
174  _attr_current_operation: str | None = None
175  _attr_current_temperature: float | None = None
176  _attr_is_away_mode_on: bool | None = None
177  _attr_max_temp: float
178  _attr_min_temp: float
179  _attr_operation_list: list[str] | None = None
180  _attr_precision: float
181  _attr_state: None = None
182  _attr_supported_features: WaterHeaterEntityFeature = WaterHeaterEntityFeature(0)
183  _attr_target_temperature_high: float | None = None
184  _attr_target_temperature_low: float | None = None
185  _attr_target_temperature: float | None = None
186  _attr_temperature_unit: str
187 
188  @final
189  @property
190  def state(self) -> str | None:
191  """Return the current state."""
192  return self.current_operationcurrent_operation
193 
194  @property
195  def precision(self) -> float:
196  """Return the precision of the system."""
197  if hasattr(self, "_attr_precision"):
198  return self._attr_precision
199  if self.hasshass.config.units.temperature_unit == UnitOfTemperature.CELSIUS:
200  return PRECISION_TENTHS
201  return PRECISION_WHOLE
202 
203  @property
204  def capability_attributes(self) -> dict[str, Any]:
205  """Return capability attributes."""
206  data: dict[str, Any] = {
207  ATTR_MIN_TEMP: show_temp(
208  self.hasshass, self.min_tempmin_temp, self.temperature_unittemperature_unit, self.precisionprecision
209  ),
210  ATTR_MAX_TEMP: show_temp(
211  self.hasshass, self.max_tempmax_temp, self.temperature_unittemperature_unit, self.precisionprecision
212  ),
213  }
214 
215  if WaterHeaterEntityFeature.OPERATION_MODE in self.supported_features_compatsupported_features_compat:
216  data[ATTR_OPERATION_LIST] = self.operation_listoperation_list
217 
218  return data
219 
220  @final
221  @property
222  def state_attributes(self) -> dict[str, Any]:
223  """Return the optional state attributes."""
224  data: dict[str, Any] = {
225  ATTR_CURRENT_TEMPERATURE: show_temp(
226  self.hasshass,
227  self.current_temperaturecurrent_temperature,
228  self.temperature_unittemperature_unit,
229  self.precisionprecision,
230  ),
231  ATTR_TEMPERATURE: show_temp(
232  self.hasshass,
233  self.target_temperaturetarget_temperature,
234  self.temperature_unittemperature_unit,
235  self.precisionprecision,
236  ),
237  ATTR_TARGET_TEMP_HIGH: show_temp(
238  self.hasshass,
239  self.target_temperature_hightarget_temperature_high,
240  self.temperature_unittemperature_unit,
241  self.precisionprecision,
242  ),
243  ATTR_TARGET_TEMP_LOW: show_temp(
244  self.hasshass,
245  self.target_temperature_lowtarget_temperature_low,
246  self.temperature_unittemperature_unit,
247  self.precisionprecision,
248  ),
249  }
250 
251  supported_features = self.supported_features_compatsupported_features_compat
252 
253  if WaterHeaterEntityFeature.OPERATION_MODE in supported_features:
254  data[ATTR_OPERATION_MODE] = self.current_operationcurrent_operation
255 
256  if WaterHeaterEntityFeature.AWAY_MODE in supported_features:
257  is_away = self.is_away_mode_onis_away_mode_on
258  data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF
259 
260  return data
261 
262  @cached_property
263  def temperature_unit(self) -> str:
264  """Return the unit of measurement used by the platform."""
265  return self._attr_temperature_unit
266 
267  @cached_property
268  def current_operation(self) -> str | None:
269  """Return current operation ie. eco, electric, performance, ..."""
270  return self._attr_current_operation
271 
272  @cached_property
273  def operation_list(self) -> list[str] | None:
274  """Return the list of available operation modes."""
275  return self._attr_operation_list
276 
277  @cached_property
278  def current_temperature(self) -> float | None:
279  """Return the current temperature."""
280  return self._attr_current_temperature
281 
282  @cached_property
283  def target_temperature(self) -> float | None:
284  """Return the temperature we try to reach."""
285  return self._attr_target_temperature
286 
287  @cached_property
288  def target_temperature_high(self) -> float | None:
289  """Return the highbound target temperature we try to reach."""
290  return self._attr_target_temperature_high
291 
292  @cached_property
293  def target_temperature_low(self) -> float | None:
294  """Return the lowbound target temperature we try to reach."""
295  return self._attr_target_temperature_low
296 
297  @cached_property
298  def is_away_mode_on(self) -> bool | None:
299  """Return true if away mode is on."""
300  return self._attr_is_away_mode_on
301 
302  def set_temperature(self, **kwargs: Any) -> None:
303  """Set new target temperature."""
304  raise NotImplementedError
305 
306  async def async_set_temperature(self, **kwargs: Any) -> None:
307  """Set new target temperature."""
308  await self.hasshass.async_add_executor_job(
309  ft.partial(self.set_temperatureset_temperature, **kwargs)
310  )
311 
312  def turn_on(self, **kwargs: Any) -> None:
313  """Turn the water heater on."""
314  raise NotImplementedError
315 
316  async def async_turn_on(self, **kwargs: Any) -> None:
317  """Turn the water heater on."""
318  await self.hasshass.async_add_executor_job(ft.partial(self.turn_onturn_on, **kwargs))
319 
320  def turn_off(self, **kwargs: Any) -> None:
321  """Turn the water heater off."""
322  raise NotImplementedError
323 
324  async def async_turn_off(self, **kwargs: Any) -> None:
325  """Turn the water heater off."""
326  await self.hasshass.async_add_executor_job(ft.partial(self.turn_offturn_off, **kwargs))
327 
328  def set_operation_mode(self, operation_mode: str) -> None:
329  """Set new target operation mode."""
330  raise NotImplementedError
331 
332  async def async_set_operation_mode(self, operation_mode: str) -> None:
333  """Set new target operation mode."""
334  await self.hasshass.async_add_executor_job(self.set_operation_modeset_operation_mode, operation_mode)
335 
336  @final
337  async def async_handle_set_operation_mode(self, operation_mode: str) -> None:
338  """Handle a set target operation mode service call."""
339  if self.operation_listoperation_list is None:
341  translation_domain=DOMAIN,
342  translation_key="operation_list_not_defined",
343  translation_placeholders={
344  "entity_id": self.entity_identity_id,
345  "operation_mode": operation_mode,
346  },
347  )
348  if operation_mode not in self.operation_listoperation_list:
349  operation_list = ", ".join(self.operation_listoperation_list)
351  translation_domain=DOMAIN,
352  translation_key="not_valid_operation_mode",
353  translation_placeholders={
354  "entity_id": self.entity_identity_id,
355  "operation_mode": operation_mode,
356  "operation_list": operation_list,
357  },
358  )
359  await self.async_set_operation_modeasync_set_operation_mode(operation_mode)
360 
361  def turn_away_mode_on(self) -> None:
362  """Turn away mode on."""
363  raise NotImplementedError
364 
365  async def async_turn_away_mode_on(self) -> None:
366  """Turn away mode on."""
367  await self.hasshass.async_add_executor_job(self.turn_away_mode_onturn_away_mode_on)
368 
369  def turn_away_mode_off(self) -> None:
370  """Turn away mode off."""
371  raise NotImplementedError
372 
373  async def async_turn_away_mode_off(self) -> None:
374  """Turn away mode off."""
375  await self.hasshass.async_add_executor_job(self.turn_away_mode_offturn_away_mode_off)
376 
377  @property
378  def min_temp(self) -> float:
379  """Return the minimum temperature."""
380  if hasattr(self, "_attr_min_temp"):
381  return self._attr_min_temp
382  return TemperatureConverter.convert(
383  DEFAULT_MIN_TEMP, UnitOfTemperature.FAHRENHEIT, self.temperature_unittemperature_unit
384  )
385 
386  @property
387  def max_temp(self) -> float:
388  """Return the maximum temperature."""
389  if hasattr(self, "_attr_max_temp"):
390  return self._attr_max_temp
391  return TemperatureConverter.convert(
392  DEFAULT_MAX_TEMP, UnitOfTemperature.FAHRENHEIT, self.temperature_unittemperature_unit
393  )
394 
395  @property
396  def supported_features(self) -> WaterHeaterEntityFeature:
397  """Return the list of supported features."""
398  return self._attr_supported_features
399 
400  @property
401  def supported_features_compat(self) -> WaterHeaterEntityFeature:
402  """Return the supported features as WaterHeaterEntityFeature.
403 
404  Remove this compatibility shim in 2025.1 or later.
405  """
406  features = self.supported_featuressupported_featuressupported_features
407  if type(features) is int: # noqa: E721
408  new_features = WaterHeaterEntityFeature(features)
409  self._report_deprecated_supported_features_values_report_deprecated_supported_features_values(new_features)
410  return new_features
411  return features
412 
413 
415  entity: WaterHeaterEntity, service: ServiceCall
416 ) -> None:
417  """Handle away mode service."""
418  if service.data[ATTR_AWAY_MODE]:
419  await entity.async_turn_away_mode_on()
420  else:
421  await entity.async_turn_away_mode_off()
422 
423 
425  entity: WaterHeaterEntity, service: ServiceCall
426 ) -> None:
427  """Handle set temperature service."""
428  hass = entity.hass
429  kwargs = {}
430 
431  for value, temp in service.data.items():
432  if value in CONVERTIBLE_ATTRIBUTE:
433  kwargs[value] = TemperatureConverter.convert(
434  temp, hass.config.units.temperature_unit, entity.temperature_unit
435  )
436  else:
437  kwargs[value] = temp
438 
439  await entity.async_set_temperature(**kwargs)
440 
441 
442 # These can be removed if no deprecated constant are in this module anymore
443 __getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals())
444 __dir__ = ft.partial(
445  dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
446 )
447 __all__ = all_with_deprecated_constants(globals())
None async_set_operation_mode(self, str operation_mode)
Definition: __init__.py:332
WaterHeaterEntityFeature supported_features_compat(self)
Definition: __init__.py:401
WaterHeaterEntityFeature supported_features(self)
Definition: __init__.py:396
None async_handle_set_operation_mode(self, str operation_mode)
Definition: __init__.py:337
None set_operation_mode(self, str operation_mode)
Definition: __init__.py:328
None _report_deprecated_supported_features_values(self, IntFlag replacement)
Definition: entity.py:1645
int|None supported_features(self)
Definition: entity.py:861
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:140
None async_service_away_mode(WaterHeaterEntity entity, ServiceCall service)
Definition: __init__.py:416
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:112
None async_service_temperature_set(WaterHeaterEntity entity, ServiceCall service)
Definition: __init__.py:426
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:145
list[str] all_with_deprecated_constants(dict[str, Any] module_globals)
Definition: deprecation.py:356