Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Provides functionality to interact with climate devices."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from datetime import timedelta
7 import functools as ft
8 import logging
9 from typing import Any, Literal, 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_TOGGLE,
20  SERVICE_TURN_OFF,
21  SERVICE_TURN_ON,
22  STATE_OFF,
23  STATE_ON,
24  UnitOfTemperature,
25 )
26 from homeassistant.core import HomeAssistant, ServiceCall, callback
27 from homeassistant.exceptions import ServiceValidationError
28 from homeassistant.helpers import config_validation as cv, issue_registry as ir
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.entity_platform import EntityPlatform
37 from homeassistant.helpers.temperature import display_temp as show_temp
38 from homeassistant.helpers.typing import ConfigType
39 from homeassistant.loader import async_get_issue_tracker, async_suggest_report_issue
40 from homeassistant.util.hass_dict import HassKey
41 from homeassistant.util.unit_conversion import TemperatureConverter
42 
43 from .const import ( # noqa: F401
44  _DEPRECATED_HVAC_MODE_AUTO,
45  _DEPRECATED_HVAC_MODE_COOL,
46  _DEPRECATED_HVAC_MODE_DRY,
47  _DEPRECATED_HVAC_MODE_FAN_ONLY,
48  _DEPRECATED_HVAC_MODE_HEAT,
49  _DEPRECATED_HVAC_MODE_HEAT_COOL,
50  _DEPRECATED_HVAC_MODE_OFF,
51  _DEPRECATED_SUPPORT_AUX_HEAT,
52  _DEPRECATED_SUPPORT_FAN_MODE,
53  _DEPRECATED_SUPPORT_PRESET_MODE,
54  _DEPRECATED_SUPPORT_SWING_MODE,
55  _DEPRECATED_SUPPORT_TARGET_HUMIDITY,
56  _DEPRECATED_SUPPORT_TARGET_TEMPERATURE,
57  _DEPRECATED_SUPPORT_TARGET_TEMPERATURE_RANGE,
58  ATTR_AUX_HEAT,
59  ATTR_CURRENT_HUMIDITY,
60  ATTR_CURRENT_TEMPERATURE,
61  ATTR_FAN_MODE,
62  ATTR_FAN_MODES,
63  ATTR_HUMIDITY,
64  ATTR_HVAC_ACTION,
65  ATTR_HVAC_MODE,
66  ATTR_HVAC_MODES,
67  ATTR_MAX_HUMIDITY,
68  ATTR_MAX_TEMP,
69  ATTR_MIN_HUMIDITY,
70  ATTR_MIN_TEMP,
71  ATTR_PRESET_MODE,
72  ATTR_PRESET_MODES,
73  ATTR_SWING_HORIZONTAL_MODE,
74  ATTR_SWING_HORIZONTAL_MODES,
75  ATTR_SWING_MODE,
76  ATTR_SWING_MODES,
77  ATTR_TARGET_TEMP_HIGH,
78  ATTR_TARGET_TEMP_LOW,
79  ATTR_TARGET_TEMP_STEP,
80  DOMAIN,
81  FAN_AUTO,
82  FAN_DIFFUSE,
83  FAN_FOCUS,
84  FAN_HIGH,
85  FAN_LOW,
86  FAN_MEDIUM,
87  FAN_MIDDLE,
88  FAN_OFF,
89  FAN_ON,
90  FAN_TOP,
91  HVAC_MODES,
92  INTENT_GET_TEMPERATURE,
93  PRESET_ACTIVITY,
94  PRESET_AWAY,
95  PRESET_BOOST,
96  PRESET_COMFORT,
97  PRESET_ECO,
98  PRESET_HOME,
99  PRESET_NONE,
100  PRESET_SLEEP,
101  SERVICE_SET_AUX_HEAT,
102  SERVICE_SET_FAN_MODE,
103  SERVICE_SET_HUMIDITY,
104  SERVICE_SET_HVAC_MODE,
105  SERVICE_SET_PRESET_MODE,
106  SERVICE_SET_SWING_HORIZONTAL_MODE,
107  SERVICE_SET_SWING_MODE,
108  SERVICE_SET_TEMPERATURE,
109  SWING_BOTH,
110  SWING_HORIZONTAL,
111  SWING_OFF,
112  SWING_ON,
113  SWING_VERTICAL,
114  ClimateEntityFeature,
115  HVACAction,
116  HVACMode,
117 )
118 
119 _LOGGER = logging.getLogger(__name__)
120 
121 DATA_COMPONENT: HassKey[EntityComponent[ClimateEntity]] = HassKey(DOMAIN)
122 ENTITY_ID_FORMAT = DOMAIN + ".{}"
123 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
124 PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
125 SCAN_INTERVAL = timedelta(seconds=60)
126 
127 DEFAULT_MIN_TEMP = 7
128 DEFAULT_MAX_TEMP = 35
129 DEFAULT_MIN_HUMIDITY = 30
130 DEFAULT_MAX_HUMIDITY = 99
131 
132 CONVERTIBLE_ATTRIBUTE = [ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH]
133 
134 # Can be removed in 2025.1 after deprecation period of the new feature flags
135 CHECK_TURN_ON_OFF_FEATURE_FLAG = (
136  ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
137 )
138 
139 SET_TEMPERATURE_SCHEMA = vol.All(
140  cv.has_at_least_one_key(
141  ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW
142  ),
143  cv.make_entity_service_schema(
144  {
145  vol.Exclusive(ATTR_TEMPERATURE, "temperature"): vol.Coerce(float),
146  vol.Inclusive(ATTR_TARGET_TEMP_HIGH, "temperature"): vol.Coerce(float),
147  vol.Inclusive(ATTR_TARGET_TEMP_LOW, "temperature"): vol.Coerce(float),
148  vol.Optional(ATTR_HVAC_MODE): vol.Coerce(HVACMode),
149  }
150  ),
151 )
152 
153 # mypy: disallow-any-generics
154 
155 
156 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
157  """Set up climate entities."""
158  component = hass.data[DATA_COMPONENT] = EntityComponent[ClimateEntity](
159  _LOGGER, DOMAIN, hass, SCAN_INTERVAL
160  )
161  await component.async_setup(config)
162 
163  component.async_register_entity_service(
164  SERVICE_TURN_ON,
165  None,
166  "async_turn_on",
167  [ClimateEntityFeature.TURN_ON],
168  )
169  component.async_register_entity_service(
170  SERVICE_TURN_OFF,
171  None,
172  "async_turn_off",
173  [ClimateEntityFeature.TURN_OFF],
174  )
175  component.async_register_entity_service(
176  SERVICE_TOGGLE,
177  None,
178  "async_toggle",
179  [ClimateEntityFeature.TURN_OFF, ClimateEntityFeature.TURN_ON],
180  )
181  component.async_register_entity_service(
182  SERVICE_SET_HVAC_MODE,
183  {vol.Required(ATTR_HVAC_MODE): vol.Coerce(HVACMode)},
184  "async_handle_set_hvac_mode_service",
185  )
186  component.async_register_entity_service(
187  SERVICE_SET_PRESET_MODE,
188  {vol.Required(ATTR_PRESET_MODE): cv.string},
189  "async_handle_set_preset_mode_service",
190  [ClimateEntityFeature.PRESET_MODE],
191  )
192  component.async_register_entity_service(
193  SERVICE_SET_AUX_HEAT,
194  {vol.Required(ATTR_AUX_HEAT): cv.boolean},
195  async_service_aux_heat,
196  [ClimateEntityFeature.AUX_HEAT],
197  )
198  component.async_register_entity_service(
199  SERVICE_SET_TEMPERATURE,
200  SET_TEMPERATURE_SCHEMA,
201  async_service_temperature_set,
202  [
203  ClimateEntityFeature.TARGET_TEMPERATURE,
204  ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
205  ],
206  )
207  component.async_register_entity_service(
208  SERVICE_SET_HUMIDITY,
209  {vol.Required(ATTR_HUMIDITY): vol.Coerce(int)},
210  async_service_humidity_set,
211  [ClimateEntityFeature.TARGET_HUMIDITY],
212  )
213  component.async_register_entity_service(
214  SERVICE_SET_FAN_MODE,
215  {vol.Required(ATTR_FAN_MODE): cv.string},
216  "async_handle_set_fan_mode_service",
217  [ClimateEntityFeature.FAN_MODE],
218  )
219  component.async_register_entity_service(
220  SERVICE_SET_SWING_MODE,
221  {vol.Required(ATTR_SWING_MODE): cv.string},
222  "async_handle_set_swing_mode_service",
223  [ClimateEntityFeature.SWING_MODE],
224  )
225  component.async_register_entity_service(
226  SERVICE_SET_SWING_HORIZONTAL_MODE,
227  {vol.Required(ATTR_SWING_HORIZONTAL_MODE): cv.string},
228  "async_handle_set_swing_horizontal_mode_service",
229  [ClimateEntityFeature.SWING_HORIZONTAL_MODE],
230  )
231 
232  return True
233 
234 
235 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
236  """Set up a config entry."""
237  return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
238 
239 
240 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
241  """Unload a config entry."""
242  return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
243 
244 
245 class ClimateEntityDescription(EntityDescription, frozen_or_thawed=True):
246  """A class that describes climate entities."""
247 
248 
249 CACHED_PROPERTIES_WITH_ATTR_ = {
250  "temperature_unit",
251  "current_humidity",
252  "target_humidity",
253  "hvac_mode",
254  "hvac_modes",
255  "hvac_action",
256  "current_temperature",
257  "target_temperature",
258  "target_temperature_step",
259  "target_temperature_high",
260  "target_temperature_low",
261  "preset_mode",
262  "preset_modes",
263  "is_aux_heat",
264  "fan_mode",
265  "fan_modes",
266  "swing_mode",
267  "swing_modes",
268  "swing_horizontal_mode",
269  "swing_horizontal_modes",
270  "supported_features",
271  "min_temp",
272  "max_temp",
273  "min_humidity",
274  "max_humidity",
275 }
276 
277 
278 class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
279  """Base class for climate entities."""
280 
281  _entity_component_unrecorded_attributes = frozenset(
282  {
283  ATTR_HVAC_MODES,
284  ATTR_FAN_MODES,
285  ATTR_SWING_MODES,
286  ATTR_MIN_TEMP,
287  ATTR_MAX_TEMP,
288  ATTR_MIN_HUMIDITY,
289  ATTR_MAX_HUMIDITY,
290  ATTR_TARGET_TEMP_STEP,
291  ATTR_PRESET_MODES,
292  }
293  )
294 
295  entity_description: ClimateEntityDescription
296  _attr_current_humidity: int | None = None
297  _attr_current_temperature: float | None = None
298  _attr_fan_mode: str | None
299  _attr_fan_modes: list[str] | None
300  _attr_hvac_action: HVACAction | None = None
301  _attr_hvac_mode: HVACMode | None
302  _attr_hvac_modes: list[HVACMode]
303  _attr_is_aux_heat: bool | None
304  _attr_max_humidity: float = DEFAULT_MAX_HUMIDITY
305  _attr_max_temp: float
306  _attr_min_humidity: float = DEFAULT_MIN_HUMIDITY
307  _attr_min_temp: float
308  _attr_precision: float
309  _attr_preset_mode: str | None
310  _attr_preset_modes: list[str] | None
311  _attr_supported_features: ClimateEntityFeature = ClimateEntityFeature(0)
312  _attr_swing_mode: str | None
313  _attr_swing_modes: list[str] | None
314  _attr_swing_horizontal_mode: str | None
315  _attr_swing_horizontal_modes: list[str] | None
316  _attr_target_humidity: float | None = None
317  _attr_target_temperature_high: float | None
318  _attr_target_temperature_low: float | None
319  _attr_target_temperature_step: float | None = None
320  _attr_target_temperature: float | None = None
321  _attr_temperature_unit: str
322 
323  __climate_reported_legacy_aux = False
324 
325  __mod_supported_features: ClimateEntityFeature = ClimateEntityFeature(0)
326  # Integrations should set `_enable_turn_on_off_backwards_compatibility` to False
327  # once migrated and set the feature flags TURN_ON/TURN_OFF as needed.
328  _enable_turn_on_off_backwards_compatibility: bool = True
329 
330  def __getattribute__(self, name: str, /) -> Any:
331  """Get attribute.
332 
333  Modify return of `supported_features` to
334  include `_mod_supported_features` if attribute is set.
335  """
336  if name != "supported_features":
337  return super().__getattribute__(name)
338 
339  # Convert the supported features to ClimateEntityFeature.
340  # Remove this compatibility shim in 2025.1 or later.
341  _supported_features: ClimateEntityFeature = super().__getattribute__(
342  "supported_features"
343  )
344  _mod_supported_features: ClimateEntityFeature = super().__getattribute__(
345  "_ClimateEntity__mod_supported_features"
346  )
347  if type(_supported_features) is int: # noqa: E721
348  _features = ClimateEntityFeature(_supported_features)
349  self._report_deprecated_supported_features_values_report_deprecated_supported_features_values(_features)
350  else:
351  _features = _supported_features
352 
353  if not _mod_supported_features:
354  return _features
355 
356  # Add automatically calculated ClimateEntityFeature.TURN_OFF/TURN_ON to
357  # supported features and return it
358  return _features | _mod_supported_features
359 
360  @callback
362  self,
363  hass: HomeAssistant,
364  platform: EntityPlatform,
365  parallel_updates: asyncio.Semaphore | None,
366  ) -> None:
367  """Start adding an entity to a platform."""
368  super().add_to_platform_start(hass, platform, parallel_updates)
369 
370  def _report_turn_on_off(feature: str, method: str) -> None:
371  """Log warning not implemented turn on/off feature."""
372  report_issue = self._suggest_report_issue_suggest_report_issue()
373  if feature.startswith("TURN"):
374  message = (
375  "Entity %s (%s) does not set ClimateEntityFeature.%s"
376  " but implements the %s method. Please %s"
377  )
378  else:
379  message = (
380  "Entity %s (%s) implements HVACMode(s): %s and therefore implicitly"
381  " supports the %s methods without setting the proper"
382  " ClimateEntityFeature. Please %s"
383  )
384  _LOGGER.warning(
385  message,
386  self.entity_identity_id,
387  type(self),
388  feature,
389  method,
390  report_issue,
391  )
392 
393  # Adds ClimateEntityFeature.TURN_OFF/TURN_ON depending on service calls implemented
394  # This should be removed in 2025.1.
395  if self._enable_turn_on_off_backwards_compatibility is False:
396  # Return if integration has migrated already
397  return
398 
399  supported_features = self.supported_featuressupported_featuressupported_features
400  if supported_features & CHECK_TURN_ON_OFF_FEATURE_FLAG:
401  # The entity supports both turn_on and turn_off, the backwards compatibility
402  # checks are not needed
403  return
404 
405  if not supported_features & ClimateEntityFeature.TURN_OFF and (
406  type(self).async_turn_off is not ClimateEntity.async_turn_off
407  or type(self).turn_off is not ClimateEntity.turn_off
408  ):
409  # turn_off implicitly supported by implementing turn_off method
410  _report_turn_on_off("TURN_OFF", "turn_off")
411  self.__mod_supported_features |= ( # pylint: disable=unused-private-member
412  ClimateEntityFeature.TURN_OFF
413  )
414 
415  if not supported_features & ClimateEntityFeature.TURN_ON and (
416  type(self).async_turn_on is not ClimateEntity.async_turn_on
417  or type(self).turn_on is not ClimateEntity.turn_on
418  ):
419  # turn_on implicitly supported by implementing turn_on method
420  _report_turn_on_off("TURN_ON", "turn_on")
421  self.__mod_supported_features |= ( # pylint: disable=unused-private-member
422  ClimateEntityFeature.TURN_ON
423  )
424 
425  if (modes := self.hvac_modeshvac_modes) and len(modes) >= 2 and HVACMode.OFF in modes:
426  # turn_on/off implicitly supported by including more modes than 1 and one of these
427  # are HVACMode.OFF
428  _modes = [_mode for _mode in modes if _mode is not None]
429  _report_turn_on_off(", ".join(_modes or []), "turn_on/turn_off")
430  self.__mod_supported_features |= ( # pylint: disable=unused-private-member
431  ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
432  )
433 
434  def _report_legacy_aux(self) -> None:
435  """Log warning and create an issue if the entity implements legacy auxiliary heater."""
436 
437  report_issue = async_suggest_report_issue(
438  self.hasshass,
439  integration_domain=self.platformplatform.platform_name,
440  module=type(self).__module__,
441  )
442  _LOGGER.warning(
443  (
444  "%s::%s implements the `is_aux_heat` property or uses the auxiliary "
445  "heater methods in a subclass of ClimateEntity which is "
446  "deprecated and will be unsupported from Home Assistant 2025.4."
447  " Please %s"
448  ),
449  self.platformplatform.platform_name,
450  self.__class__.__name__,
451  report_issue,
452  )
453 
454  translation_placeholders = {"platform": self.platformplatform.platform_name}
455  translation_key = "deprecated_climate_aux_no_url"
456  issue_tracker = async_get_issue_tracker(
457  self.hasshass,
458  integration_domain=self.platformplatform.platform_name,
459  module=type(self).__module__,
460  )
461  if issue_tracker:
462  translation_placeholders["issue_tracker"] = issue_tracker
463  translation_key = "deprecated_climate_aux_url_custom"
464  ir.async_create_issue(
465  self.hasshass,
466  DOMAIN,
467  f"deprecated_climate_aux_{self.platform.platform_name}",
468  breaks_in_ha_version="2025.4.0",
469  is_fixable=False,
470  is_persistent=False,
471  issue_domain=self.platformplatform.platform_name,
472  severity=ir.IssueSeverity.WARNING,
473  translation_key=translation_key,
474  translation_placeholders=translation_placeholders,
475  )
476  self.__climate_reported_legacy_aux__climate_reported_legacy_aux = True
477 
478  @final
479  @property
480  def state(self) -> str | None:
481  """Return the current state."""
482  hvac_mode = self.hvac_modehvac_modehvac_mode
483  if hvac_mode is None:
484  return None
485  # Support hvac_mode as string for custom integration backwards compatibility
486  if not isinstance(hvac_mode, HVACMode):
487  return HVACMode(hvac_mode).value # type: ignore[unreachable]
488  return hvac_mode.value
489 
490  @property
491  def precision(self) -> float:
492  """Return the precision of the system."""
493  if hasattr(self, "_attr_precision"):
494  return self._attr_precision
495  if self.hasshass.config.units.temperature_unit == UnitOfTemperature.CELSIUS:
496  return PRECISION_TENTHS
497  return PRECISION_WHOLE
498 
499  @property
500  def capability_attributes(self) -> dict[str, Any] | None:
501  """Return the capability attributes."""
502  supported_features = self.supported_featuressupported_featuressupported_features
503  temperature_unit = self.temperature_unittemperature_unit
504  precision = self.precisionprecision
505  hass = self.hasshass
506 
507  data: dict[str, Any] = {
508  ATTR_HVAC_MODES: self.hvac_modeshvac_modes,
509  ATTR_MIN_TEMP: show_temp(hass, self.min_tempmin_temp, temperature_unit, precision),
510  ATTR_MAX_TEMP: show_temp(hass, self.max_tempmax_temp, temperature_unit, precision),
511  }
512 
513  if target_temperature_step := self.target_temperature_steptarget_temperature_step:
514  data[ATTR_TARGET_TEMP_STEP] = target_temperature_step
515 
516  if ClimateEntityFeature.TARGET_HUMIDITY in supported_features:
517  data[ATTR_MIN_HUMIDITY] = self.min_humiditymin_humidity
518  data[ATTR_MAX_HUMIDITY] = self.max_humiditymax_humidity
519 
520  if ClimateEntityFeature.FAN_MODE in supported_features:
521  data[ATTR_FAN_MODES] = self.fan_modesfan_modes
522 
523  if ClimateEntityFeature.PRESET_MODE in supported_features:
524  data[ATTR_PRESET_MODES] = self.preset_modespreset_modes
525 
526  if ClimateEntityFeature.SWING_MODE in supported_features:
527  data[ATTR_SWING_MODES] = self.swing_modesswing_modes
528 
529  if ClimateEntityFeature.SWING_HORIZONTAL_MODE in supported_features:
530  data[ATTR_SWING_HORIZONTAL_MODES] = self.swing_horizontal_modesswing_horizontal_modes
531 
532  return data
533 
534  @final
535  @property
536  def state_attributes(self) -> dict[str, Any]:
537  """Return the optional state attributes."""
538  supported_features = self.supported_featuressupported_featuressupported_features
539  temperature_unit = self.temperature_unittemperature_unit
540  precision = self.precisionprecision
541  hass = self.hasshass
542 
543  data: dict[str, str | float | None] = {
544  ATTR_CURRENT_TEMPERATURE: show_temp(
545  hass, self.current_temperaturecurrent_temperature, temperature_unit, precision
546  ),
547  }
548 
549  if ClimateEntityFeature.TARGET_TEMPERATURE in supported_features:
550  data[ATTR_TEMPERATURE] = show_temp(
551  hass,
552  self.target_temperaturetarget_temperature,
553  temperature_unit,
554  precision,
555  )
556 
557  if ClimateEntityFeature.TARGET_TEMPERATURE_RANGE in supported_features:
558  data[ATTR_TARGET_TEMP_HIGH] = show_temp(
559  hass, self.target_temperature_hightarget_temperature_high, temperature_unit, precision
560  )
561  data[ATTR_TARGET_TEMP_LOW] = show_temp(
562  hass, self.target_temperature_lowtarget_temperature_low, temperature_unit, precision
563  )
564 
565  if (current_humidity := self.current_humiditycurrent_humidity) is not None:
566  data[ATTR_CURRENT_HUMIDITY] = current_humidity
567 
568  if ClimateEntityFeature.TARGET_HUMIDITY in supported_features:
569  data[ATTR_HUMIDITY] = self.target_humiditytarget_humidity
570 
571  if ClimateEntityFeature.FAN_MODE in supported_features:
572  data[ATTR_FAN_MODE] = self.fan_modefan_mode
573 
574  if hvac_action := self.hvac_actionhvac_action:
575  data[ATTR_HVAC_ACTION] = hvac_action
576 
577  if ClimateEntityFeature.PRESET_MODE in supported_features:
578  data[ATTR_PRESET_MODE] = self.preset_modepreset_mode
579 
580  if ClimateEntityFeature.SWING_MODE in supported_features:
581  data[ATTR_SWING_MODE] = self.swing_modeswing_mode
582 
583  if ClimateEntityFeature.SWING_HORIZONTAL_MODE in supported_features:
584  data[ATTR_SWING_HORIZONTAL_MODE] = self.swing_horizontal_modeswing_horizontal_mode
585 
586  if ClimateEntityFeature.AUX_HEAT in supported_features:
587  data[ATTR_AUX_HEAT] = STATE_ON if self.is_aux_heatis_aux_heat else STATE_OFF
588  if (
589  self.__climate_reported_legacy_aux__climate_reported_legacy_aux is False
590  and "custom_components" in type(self).__module__
591  ):
592  self._report_legacy_aux_report_legacy_aux()
593 
594  return data
595 
596  @cached_property
597  def temperature_unit(self) -> str:
598  """Return the unit of measurement used by the platform."""
599  return self._attr_temperature_unit
600 
601  @cached_property
602  def current_humidity(self) -> float | None:
603  """Return the current humidity."""
604  return self._attr_current_humidity
605 
606  @cached_property
607  def target_humidity(self) -> float | None:
608  """Return the humidity we try to reach."""
609  return self._attr_target_humidity
610 
611  @cached_property
612  def hvac_mode(self) -> HVACMode | None:
613  """Return hvac operation ie. heat, cool mode."""
614  return self._attr_hvac_mode
615 
616  @cached_property
617  def hvac_modes(self) -> list[HVACMode]:
618  """Return the list of available hvac operation modes."""
619  return self._attr_hvac_modes
620 
621  @cached_property
622  def hvac_action(self) -> HVACAction | None:
623  """Return the current running hvac operation if supported."""
624  return self._attr_hvac_action
625 
626  @cached_property
627  def current_temperature(self) -> float | None:
628  """Return the current temperature."""
629  return self._attr_current_temperature
630 
631  @cached_property
632  def target_temperature(self) -> float | None:
633  """Return the temperature we try to reach."""
634  return self._attr_target_temperature
635 
636  @cached_property
637  def target_temperature_step(self) -> float | None:
638  """Return the supported step of target temperature."""
639  return self._attr_target_temperature_step
640 
641  @cached_property
642  def target_temperature_high(self) -> float | None:
643  """Return the highbound target temperature we try to reach.
644 
645  Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
646  """
647  return self._attr_target_temperature_high
648 
649  @cached_property
650  def target_temperature_low(self) -> float | None:
651  """Return the lowbound target temperature we try to reach.
652 
653  Requires ClimateEntityFeature.TARGET_TEMPERATURE_RANGE.
654  """
655  return self._attr_target_temperature_low
656 
657  @cached_property
658  def preset_mode(self) -> str | None:
659  """Return the current preset mode, e.g., home, away, temp.
660 
661  Requires ClimateEntityFeature.PRESET_MODE.
662  """
663  return self._attr_preset_mode
664 
665  @cached_property
666  def preset_modes(self) -> list[str] | None:
667  """Return a list of available preset modes.
668 
669  Requires ClimateEntityFeature.PRESET_MODE.
670  """
671  return self._attr_preset_modes
672 
673  @cached_property
674  def is_aux_heat(self) -> bool | None:
675  """Return true if aux heater.
676 
677  Requires ClimateEntityFeature.AUX_HEAT.
678  """
679  return self._attr_is_aux_heat
680 
681  @cached_property
682  def fan_mode(self) -> str | None:
683  """Return the fan setting.
684 
685  Requires ClimateEntityFeature.FAN_MODE.
686  """
687  return self._attr_fan_mode
688 
689  @cached_property
690  def fan_modes(self) -> list[str] | None:
691  """Return the list of available fan modes.
692 
693  Requires ClimateEntityFeature.FAN_MODE.
694  """
695  return self._attr_fan_modes
696 
697  @cached_property
698  def swing_mode(self) -> str | None:
699  """Return the swing setting.
700 
701  Requires ClimateEntityFeature.SWING_MODE.
702  """
703  return self._attr_swing_mode
704 
705  @cached_property
706  def swing_modes(self) -> list[str] | None:
707  """Return the list of available swing modes.
708 
709  Requires ClimateEntityFeature.SWING_MODE.
710  """
711  return self._attr_swing_modes
712 
713  @cached_property
714  def swing_horizontal_mode(self) -> str | None:
715  """Return the horizontal swing setting.
716 
717  Requires ClimateEntityFeature.SWING_HORIZONTAL_MODE.
718  """
719  return self._attr_swing_horizontal_mode
720 
721  @cached_property
722  def swing_horizontal_modes(self) -> list[str] | None:
723  """Return the list of available horizontal swing modes.
724 
725  Requires ClimateEntityFeature.SWING_HORIZONTAL_MODE.
726  """
727  return self._attr_swing_horizontal_modes
728 
729  @final
730  @callback
732  self,
733  mode_type: Literal["preset", "horizontal_swing", "swing", "fan", "hvac"],
734  mode: str | HVACMode,
735  modes: list[str] | list[HVACMode] | None,
736  ) -> None:
737  """Raise ServiceValidationError on invalid modes."""
738  if modes and mode in modes:
739  return
740  modes_str: str = ", ".join(modes) if modes else ""
741  translation_key = f"not_valid_{mode_type}_mode"
742  if mode_type == "hvac":
743  report_issue = async_suggest_report_issue(
744  self.hasshass,
745  integration_domain=self.platformplatform.platform_name,
746  module=type(self).__module__,
747  )
748  _LOGGER.warning(
749  (
750  "%s::%s sets the hvac_mode %s which is not "
751  "valid for this entity with modes: %s. "
752  "This will stop working in 2025.4 and raise an error instead. "
753  "Please %s"
754  ),
755  self.platformplatform.platform_name,
756  self.__class__.__name__,
757  mode,
758  modes_str,
759  report_issue,
760  )
761  return
763  translation_domain=DOMAIN,
764  translation_key=translation_key,
765  translation_placeholders={
766  "mode": mode,
767  "modes": modes_str,
768  },
769  )
770 
771  def set_temperature(self, **kwargs: Any) -> None:
772  """Set new target temperature."""
773  raise NotImplementedError
774 
775  async def async_set_temperature(self, **kwargs: Any) -> None:
776  """Set new target temperature."""
777  await self.hasshass.async_add_executor_job(
778  ft.partial(self.set_temperatureset_temperature, **kwargs)
779  )
780 
781  def set_humidity(self, humidity: int) -> None:
782  """Set new target humidity."""
783  raise NotImplementedError
784 
785  async def async_set_humidity(self, humidity: int) -> None:
786  """Set new target humidity."""
787  await self.hasshass.async_add_executor_job(self.set_humidityset_humidity, humidity)
788 
789  @final
790  async def async_handle_set_fan_mode_service(self, fan_mode: str) -> None:
791  """Validate and set new preset mode."""
792  self._valid_mode_or_raise_valid_mode_or_raise("fan", fan_mode, self.fan_modesfan_modes)
793  await self.async_set_fan_modeasync_set_fan_mode(fan_mode)
794 
795  def set_fan_mode(self, fan_mode: str) -> None:
796  """Set new target fan mode."""
797  raise NotImplementedError
798 
799  async def async_set_fan_mode(self, fan_mode: str) -> None:
800  """Set new target fan mode."""
801  await self.hasshass.async_add_executor_job(self.set_fan_modeset_fan_mode, fan_mode)
802 
803  @final
804  async def async_handle_set_hvac_mode_service(self, hvac_mode: HVACMode) -> None:
805  """Validate and set new preset mode."""
806  self._valid_mode_or_raise_valid_mode_or_raise("hvac", hvac_mode, self.hvac_modeshvac_modes)
807  await self.async_set_hvac_modeasync_set_hvac_mode(hvac_mode)
808 
809  def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
810  """Set new target hvac mode."""
811  raise NotImplementedError
812 
813  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
814  """Set new target hvac mode."""
815  await self.hasshass.async_add_executor_job(self.set_hvac_modeset_hvac_mode, hvac_mode)
816 
817  @final
818  async def async_handle_set_swing_mode_service(self, swing_mode: str) -> None:
819  """Validate and set new preset mode."""
820  self._valid_mode_or_raise_valid_mode_or_raise("swing", swing_mode, self.swing_modesswing_modes)
821  await self.async_set_swing_modeasync_set_swing_mode(swing_mode)
822 
823  def set_swing_mode(self, swing_mode: str) -> None:
824  """Set new target swing operation."""
825  raise NotImplementedError
826 
827  async def async_set_swing_mode(self, swing_mode: str) -> None:
828  """Set new target swing operation."""
829  await self.hasshass.async_add_executor_job(self.set_swing_modeset_swing_mode, swing_mode)
830 
831  @final
833  self, swing_horizontal_mode: str
834  ) -> None:
835  """Validate and set new horizontal swing mode."""
836  self._valid_mode_or_raise_valid_mode_or_raise(
837  "horizontal_swing", swing_horizontal_mode, self.swing_horizontal_modesswing_horizontal_modes
838  )
839  await self.async_set_swing_horizontal_modeasync_set_swing_horizontal_mode(swing_horizontal_mode)
840 
841  def set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
842  """Set new target horizontal swing operation."""
843  raise NotImplementedError
844 
845  async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
846  """Set new target horizontal swing operation."""
847  await self.hasshass.async_add_executor_job(
848  self.set_swing_horizontal_modeset_swing_horizontal_mode, swing_horizontal_mode
849  )
850 
851  @final
852  async def async_handle_set_preset_mode_service(self, preset_mode: str) -> None:
853  """Validate and set new preset mode."""
854  self._valid_mode_or_raise_valid_mode_or_raise("preset", preset_mode, self.preset_modespreset_modes)
855  await self.async_set_preset_modeasync_set_preset_mode(preset_mode)
856 
857  def set_preset_mode(self, preset_mode: str) -> None:
858  """Set new preset mode."""
859  raise NotImplementedError
860 
861  async def async_set_preset_mode(self, preset_mode: str) -> None:
862  """Set new preset mode."""
863  await self.hasshass.async_add_executor_job(self.set_preset_modeset_preset_mode, preset_mode)
864 
865  def turn_aux_heat_on(self) -> None:
866  """Turn auxiliary heater on."""
867  raise NotImplementedError
868 
869  async def async_turn_aux_heat_on(self) -> None:
870  """Turn auxiliary heater on."""
871  await self.hasshass.async_add_executor_job(self.turn_aux_heat_onturn_aux_heat_on)
872 
873  def turn_aux_heat_off(self) -> None:
874  """Turn auxiliary heater off."""
875  raise NotImplementedError
876 
877  async def async_turn_aux_heat_off(self) -> None:
878  """Turn auxiliary heater off."""
879  await self.hasshass.async_add_executor_job(self.turn_aux_heat_offturn_aux_heat_off)
880 
881  def turn_on(self) -> None:
882  """Turn the entity on."""
883  raise NotImplementedError
884 
885  async def async_turn_on(self) -> None:
886  """Turn the entity on."""
887  # Forward to self.turn_on if it's been overridden.
888  if type(self).turn_on is not ClimateEntity.turn_on:
889  await self.hasshass.async_add_executor_job(self.turn_onturn_on)
890  return
891 
892  # If there are only two HVAC modes, and one of those modes is OFF,
893  # then we can just turn on the other mode.
894  if len(self.hvac_modeshvac_modes) == 2 and HVACMode.OFF in self.hvac_modeshvac_modes:
895  for mode in self.hvac_modeshvac_modes:
896  if mode != HVACMode.OFF:
897  await self.async_set_hvac_modeasync_set_hvac_mode(mode)
898  return
899 
900  # Fake turn on
901  for mode in (HVACMode.HEAT_COOL, HVACMode.HEAT, HVACMode.COOL):
902  if mode not in self.hvac_modeshvac_modes:
903  continue
904  await self.async_set_hvac_modeasync_set_hvac_mode(mode)
905  return
906 
907  raise NotImplementedError
908 
909  def turn_off(self) -> None:
910  """Turn the entity off."""
911  raise NotImplementedError
912 
913  async def async_turn_off(self) -> None:
914  """Turn the entity off."""
915  # Forward to self.turn_on if it's been overridden.
916  if type(self).turn_off is not ClimateEntity.turn_off:
917  await self.hasshass.async_add_executor_job(self.turn_offturn_off)
918  return
919 
920  # Fake turn off
921  if HVACMode.OFF in self.hvac_modeshvac_modes:
922  await self.async_set_hvac_modeasync_set_hvac_mode(HVACMode.OFF)
923  return
924 
925  raise NotImplementedError
926 
927  def toggle(self) -> None:
928  """Toggle the entity."""
929  raise NotImplementedError
930 
931  async def async_toggle(self) -> None:
932  """Toggle the entity."""
933  # Forward to self.toggle if it's been overridden.
934  if type(self).toggle is not ClimateEntity.toggle:
935  await self.hasshass.async_add_executor_job(self.toggletoggle)
936  return
937 
938  # We assume that since turn_off is supported, HVACMode.OFF is as well.
939  if self.hvac_modehvac_modehvac_mode == HVACMode.OFF:
940  await self.async_turn_onasync_turn_on()
941  else:
942  await self.async_turn_offasync_turn_off()
943 
944  @cached_property
945  def supported_features(self) -> ClimateEntityFeature:
946  """Return the list of supported features."""
947  return self._attr_supported_features
948 
949  @cached_property
950  def min_temp(self) -> float:
951  """Return the minimum temperature."""
952  if not hasattr(self, "_attr_min_temp"):
953  return TemperatureConverter.convert(
954  DEFAULT_MIN_TEMP, UnitOfTemperature.CELSIUS, self.temperature_unittemperature_unit
955  )
956  return self._attr_min_temp
957 
958  @cached_property
959  def max_temp(self) -> float:
960  """Return the maximum temperature."""
961  if not hasattr(self, "_attr_max_temp"):
962  return TemperatureConverter.convert(
963  DEFAULT_MAX_TEMP, UnitOfTemperature.CELSIUS, self.temperature_unittemperature_unit
964  )
965  return self._attr_max_temp
966 
967  @cached_property
968  def min_humidity(self) -> float:
969  """Return the minimum humidity."""
970  return self._attr_min_humidity
971 
972  @cached_property
973  def max_humidity(self) -> float:
974  """Return the maximum humidity."""
975  return self._attr_max_humidity
976 
977 
979  entity: ClimateEntity, service_call: ServiceCall
980 ) -> None:
981  """Handle aux heat service."""
982  if service_call.data[ATTR_AUX_HEAT]:
983  await entity.async_turn_aux_heat_on()
984  else:
985  await entity.async_turn_aux_heat_off()
986 
987 
989  entity: ClimateEntity, service_call: ServiceCall
990 ) -> None:
991  """Handle set humidity service."""
992  humidity = service_call.data[ATTR_HUMIDITY]
993  min_humidity = entity.min_humidity
994  max_humidity = entity.max_humidity
995  _LOGGER.debug(
996  "Check valid humidity %d in range %d - %d",
997  humidity,
998  min_humidity,
999  max_humidity,
1000  )
1001  if humidity < min_humidity or humidity > max_humidity:
1002  raise ServiceValidationError(
1003  translation_domain=DOMAIN,
1004  translation_key="humidity_out_of_range",
1005  translation_placeholders={
1006  "humidity": str(humidity),
1007  "min_humidity": str(min_humidity),
1008  "max_humidity": str(max_humidity),
1009  },
1010  )
1011 
1012  await entity.async_set_humidity(humidity)
1013 
1014 
1016  entity: ClimateEntity, service_call: ServiceCall
1017 ) -> None:
1018  """Handle set temperature service."""
1019  if (
1020  ATTR_TEMPERATURE in service_call.data
1021  and not entity.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE
1022  ):
1023  raise ServiceValidationError(
1024  translation_domain=DOMAIN,
1025  translation_key="missing_target_temperature_entity_feature",
1026  )
1027  if (
1028  ATTR_TARGET_TEMP_LOW in service_call.data
1029  and not entity.supported_features
1030  & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
1031  ):
1032  raise ServiceValidationError(
1033  translation_domain=DOMAIN,
1034  translation_key="missing_target_temperature_range_entity_feature",
1035  )
1036 
1037  hass = entity.hass
1038  kwargs: dict[str, Any] = {}
1039  min_temp = entity.min_temp
1040  max_temp = entity.max_temp
1041  temp_unit = entity.temperature_unit
1042 
1043  if (
1044  (target_low_temp := service_call.data.get(ATTR_TARGET_TEMP_LOW))
1045  and (target_high_temp := service_call.data.get(ATTR_TARGET_TEMP_HIGH))
1046  and target_low_temp > target_high_temp
1047  ):
1048  # Ensure target_low_temp is not higher than target_high_temp.
1049  raise ServiceValidationError(
1050  translation_domain=DOMAIN,
1051  translation_key="low_temp_higher_than_high_temp",
1052  )
1053 
1054  for value, temp in service_call.data.items():
1055  if value in CONVERTIBLE_ATTRIBUTE:
1056  kwargs[value] = check_temp = TemperatureConverter.convert(
1057  temp, hass.config.units.temperature_unit, temp_unit
1058  )
1059 
1060  _LOGGER.debug(
1061  "Check valid temperature %d %s (%d %s) in range %d %s - %d %s",
1062  check_temp,
1063  entity.temperature_unit,
1064  temp,
1065  hass.config.units.temperature_unit,
1066  min_temp,
1067  temp_unit,
1068  max_temp,
1069  temp_unit,
1070  )
1071  if check_temp < min_temp or check_temp > max_temp:
1072  raise ServiceValidationError(
1073  translation_domain=DOMAIN,
1074  translation_key="temp_out_of_range",
1075  translation_placeholders={
1076  "check_temp": str(check_temp),
1077  "min_temp": str(min_temp),
1078  "max_temp": str(max_temp),
1079  },
1080  )
1081  else:
1082  kwargs[value] = temp
1083 
1084  await entity.async_set_temperature(**kwargs)
1085 
1086 
1087 # As we import deprecated constants from the const module, we need to add these two functions
1088 # otherwise this module will be logged for using deprecated constants and not the custom component
1089 # These can be removed if no deprecated constant are in this module anymore
1090 __getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals())
1091 __dir__ = ft.partial(
1092  dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
1093 )
1094 __all__ = all_with_deprecated_constants(globals())
None set_swing_mode(self, str swing_mode)
Definition: __init__.py:823
None async_set_preset_mode(self, str preset_mode)
Definition: __init__.py:861
None set_hvac_mode(self, HVACMode hvac_mode)
Definition: __init__.py:809
None async_set_fan_mode(self, str fan_mode)
Definition: __init__.py:799
None set_temperature(self, **Any kwargs)
Definition: __init__.py:771
None async_set_humidity(self, int humidity)
Definition: __init__.py:785
None async_set_temperature(self, **Any kwargs)
Definition: __init__.py:775
None async_handle_set_swing_horizontal_mode_service(self, str swing_horizontal_mode)
Definition: __init__.py:834
None async_handle_set_swing_mode_service(self, str swing_mode)
Definition: __init__.py:818
None set_swing_horizontal_mode(self, str swing_horizontal_mode)
Definition: __init__.py:841
None add_to_platform_start(self, HomeAssistant hass, EntityPlatform platform, asyncio.Semaphore|None parallel_updates)
Definition: __init__.py:366
None async_set_swing_mode(self, str swing_mode)
Definition: __init__.py:827
ClimateEntityFeature supported_features(self)
Definition: __init__.py:945
None _valid_mode_or_raise(self, Literal["preset", "horizontal_swing", "swing", "fan", "hvac"] mode_type, str|HVACMode mode, list[str]|list[HVACMode]|None modes)
Definition: __init__.py:736
None async_handle_set_hvac_mode_service(self, HVACMode hvac_mode)
Definition: __init__.py:804
None async_set_hvac_mode(self, HVACMode hvac_mode)
Definition: __init__.py:813
None async_handle_set_preset_mode_service(self, str preset_mode)
Definition: __init__.py:852
None async_handle_set_fan_mode_service(self, str fan_mode)
Definition: __init__.py:790
None async_set_swing_horizontal_mode(self, str swing_horizontal_mode)
Definition: __init__.py:845
dict[str, Any]|None capability_attributes(self)
Definition: __init__.py:500
None set_preset_mode(self, str preset_mode)
Definition: __init__.py:857
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:235
None async_service_temperature_set(ClimateEntity entity, ServiceCall service_call)
Definition: __init__.py:1017
None async_service_aux_heat(ClimateEntity entity, ServiceCall service_call)
Definition: __init__.py:980
None async_service_humidity_set(ClimateEntity entity, ServiceCall service_call)
Definition: __init__.py:990
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:156
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:240
list[str] all_with_deprecated_constants(dict[str, Any] module_globals)
Definition: deprecation.py:356
str|None async_get_issue_tracker(HomeAssistant|None hass, *Integration|None integration=None, str|None integration_domain=None, str|None module=None)
Definition: loader.py:1719
str async_suggest_report_issue(HomeAssistant|None hass, *Integration|None integration=None, str|None integration_domain=None, str|None module=None)
Definition: loader.py:1752