Home Assistant Unofficial Reference 2024.12.1
climate.py
Go to the documentation of this file.
1 """Support for ESPHome climate devices."""
2 
3 from __future__ import annotations
4 
5 from functools import partial
6 from typing import Any, cast
7 
8 from aioesphomeapi import (
9  ClimateAction,
10  ClimateFanMode,
11  ClimateInfo,
12  ClimateMode,
13  ClimatePreset,
14  ClimateState,
15  ClimateSwingMode,
16  EntityInfo,
17 )
18 
20  ATTR_HVAC_MODE,
21  ATTR_TARGET_TEMP_HIGH,
22  ATTR_TARGET_TEMP_LOW,
23  FAN_AUTO,
24  FAN_DIFFUSE,
25  FAN_FOCUS,
26  FAN_HIGH,
27  FAN_LOW,
28  FAN_MEDIUM,
29  FAN_MIDDLE,
30  FAN_OFF,
31  FAN_ON,
32  PRESET_ACTIVITY,
33  PRESET_AWAY,
34  PRESET_BOOST,
35  PRESET_COMFORT,
36  PRESET_ECO,
37  PRESET_HOME,
38  PRESET_NONE,
39  PRESET_SLEEP,
40  SWING_BOTH,
41  SWING_HORIZONTAL,
42  SWING_OFF,
43  SWING_VERTICAL,
44  ClimateEntity,
45  ClimateEntityFeature,
46  HVACAction,
47  HVACMode,
48 )
49 from homeassistant.const import (
50  ATTR_TEMPERATURE,
51  PRECISION_HALVES,
52  PRECISION_TENTHS,
53  PRECISION_WHOLE,
54  UnitOfTemperature,
55 )
56 from homeassistant.core import callback
57 
58 from .entity import (
59  EsphomeEntity,
60  convert_api_error_ha_error,
61  esphome_float_state_property,
62  esphome_state_property,
63  platform_async_setup_entry,
64 )
65 from .enum_mapper import EsphomeEnumMapper
66 
67 FAN_QUIET = "quiet"
68 
69 
70 _CLIMATE_MODES: EsphomeEnumMapper[ClimateMode, HVACMode] = EsphomeEnumMapper(
71  {
72  ClimateMode.OFF: HVACMode.OFF,
73  ClimateMode.HEAT_COOL: HVACMode.HEAT_COOL,
74  ClimateMode.COOL: HVACMode.COOL,
75  ClimateMode.HEAT: HVACMode.HEAT,
76  ClimateMode.FAN_ONLY: HVACMode.FAN_ONLY,
77  ClimateMode.DRY: HVACMode.DRY,
78  ClimateMode.AUTO: HVACMode.AUTO,
79  }
80 )
81 _CLIMATE_ACTIONS: EsphomeEnumMapper[ClimateAction, HVACAction] = EsphomeEnumMapper(
82  {
83  ClimateAction.OFF: HVACAction.OFF,
84  ClimateAction.COOLING: HVACAction.COOLING,
85  ClimateAction.HEATING: HVACAction.HEATING,
86  ClimateAction.IDLE: HVACAction.IDLE,
87  ClimateAction.DRYING: HVACAction.DRYING,
88  ClimateAction.FAN: HVACAction.FAN,
89  }
90 )
91 _FAN_MODES: EsphomeEnumMapper[ClimateFanMode, str] = EsphomeEnumMapper(
92  {
93  ClimateFanMode.ON: FAN_ON,
94  ClimateFanMode.OFF: FAN_OFF,
95  ClimateFanMode.AUTO: FAN_AUTO,
96  ClimateFanMode.LOW: FAN_LOW,
97  ClimateFanMode.MEDIUM: FAN_MEDIUM,
98  ClimateFanMode.HIGH: FAN_HIGH,
99  ClimateFanMode.MIDDLE: FAN_MIDDLE,
100  ClimateFanMode.FOCUS: FAN_FOCUS,
101  ClimateFanMode.DIFFUSE: FAN_DIFFUSE,
102  ClimateFanMode.QUIET: FAN_QUIET,
103  }
104 )
105 _SWING_MODES: EsphomeEnumMapper[ClimateSwingMode, str] = EsphomeEnumMapper(
106  {
107  ClimateSwingMode.OFF: SWING_OFF,
108  ClimateSwingMode.BOTH: SWING_BOTH,
109  ClimateSwingMode.VERTICAL: SWING_VERTICAL,
110  ClimateSwingMode.HORIZONTAL: SWING_HORIZONTAL,
111  }
112 )
113 _PRESETS: EsphomeEnumMapper[ClimatePreset, str] = EsphomeEnumMapper(
114  {
115  ClimatePreset.NONE: PRESET_NONE,
116  ClimatePreset.HOME: PRESET_HOME,
117  ClimatePreset.AWAY: PRESET_AWAY,
118  ClimatePreset.BOOST: PRESET_BOOST,
119  ClimatePreset.COMFORT: PRESET_COMFORT,
120  ClimatePreset.ECO: PRESET_ECO,
121  ClimatePreset.SLEEP: PRESET_SLEEP,
122  ClimatePreset.ACTIVITY: PRESET_ACTIVITY,
123  }
124 )
125 
126 
127 class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEntity):
128  """A climate implementation for ESPHome."""
129 
130  _attr_temperature_unit = UnitOfTemperature.CELSIUS
131  _attr_translation_key = "climate"
132  _enable_turn_on_off_backwards_compatibility = False
133 
134  @callback
135  def _on_static_info_update(self, static_info: EntityInfo) -> None:
136  """Set attrs from static info."""
137  super()._on_static_info_update(static_info)
138  static_info = self._static_info_static_info
139  self._attr_precision_attr_precision = self._get_precision_get_precision()
140  self._attr_hvac_modes_attr_hvac_modes = [
141  _CLIMATE_MODES.from_esphome(mode) for mode in static_info.supported_modes
142  ]
143  self._attr_fan_modes_attr_fan_modes = [
144  _FAN_MODES.from_esphome(mode) for mode in static_info.supported_fan_modes
145  ] + static_info.supported_custom_fan_modes
146  self._attr_preset_modes_attr_preset_modes = [
147  _PRESETS.from_esphome(preset)
148  for preset in static_info.supported_presets_compat(self._api_version_api_version)
149  ] + static_info.supported_custom_presets
150  self._attr_swing_modes_attr_swing_modes = [
151  _SWING_MODES.from_esphome(mode)
152  for mode in static_info.supported_swing_modes
153  ]
154  # Round to one digit because of floating point math
155  self._attr_target_temperature_step_attr_target_temperature_step = round(
156  static_info.visual_target_temperature_step, 1
157  )
158  self._attr_min_temp_attr_min_temp = static_info.visual_min_temperature
159  self._attr_max_temp_attr_max_temp = static_info.visual_max_temperature
160  self._attr_min_humidity_attr_min_humidity = round(static_info.visual_min_humidity)
161  self._attr_max_humidity_attr_max_humidity = round(static_info.visual_max_humidity)
162  features = ClimateEntityFeature(0)
163  if static_info.supports_two_point_target_temperature:
164  features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
165  else:
166  features |= ClimateEntityFeature.TARGET_TEMPERATURE
167  if static_info.supports_target_humidity:
168  features |= ClimateEntityFeature.TARGET_HUMIDITY
169  if self.preset_modespreset_modes:
170  features |= ClimateEntityFeature.PRESET_MODE
171  if self.fan_modesfan_modes:
172  features |= ClimateEntityFeature.FAN_MODE
173  if self.swing_modesswing_modes:
174  features |= ClimateEntityFeature.SWING_MODE
175  if len(self.hvac_modeshvac_modes) > 1 and HVACMode.OFF in self.hvac_modeshvac_modes:
176  features |= ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
177  self._attr_supported_features_attr_supported_features = features
178 
179  def _get_precision(self) -> float:
180  """Return the precision of the climate device."""
181  precicions = [PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS]
182  static_info = self._static_info_static_info
183  if static_info.visual_current_temperature_step != 0:
184  step = static_info.visual_current_temperature_step
185  else:
186  step = static_info.visual_target_temperature_step
187  for prec in precicions:
188  if step >= prec:
189  return prec
190  # Fall back to highest precision, tenths
191  return PRECISION_TENTHS
192 
193  @property
194  @esphome_state_property
195  def hvac_mode(self) -> HVACMode | None:
196  """Return current operation ie. heat, cool, idle."""
197  return _CLIMATE_MODES.from_esphome(self._state_state.mode)
198 
199  @property
200  @esphome_state_property
201  def hvac_action(self) -> HVACAction | None:
202  """Return current action."""
203  # HA has no support feature field for hvac_action
204  if not self._static_info_static_info.supports_action:
205  return None
206  return _CLIMATE_ACTIONS.from_esphome(self._state_state.action)
207 
208  @property
209  @esphome_state_property
210  def fan_mode(self) -> str | None:
211  """Return current fan setting."""
212  state = self._state_state
213  return state.custom_fan_mode or _FAN_MODES.from_esphome(state.fan_mode)
214 
215  @property
216  @esphome_state_property
217  def preset_mode(self) -> str | None:
218  """Return current preset mode."""
219  state = self._state_state
220  return state.custom_preset or _PRESETS.from_esphome(
221  state.preset_compat(self._api_version_api_version)
222  )
223 
224  @property
225  @esphome_state_property
226  def swing_mode(self) -> str | None:
227  """Return current swing mode."""
228  return _SWING_MODES.from_esphome(self._state_state.swing_mode)
229 
230  @property
231  @esphome_float_state_property
232  def current_temperature(self) -> float | None:
233  """Return the current temperature."""
234  return self._state_state.current_temperature
235 
236  @property
237  @esphome_state_property
238  def current_humidity(self) -> int | None:
239  """Return the current humidity."""
240  if not self._static_info_static_info.supports_current_humidity:
241  return None
242  return round(self._state_state.current_humidity)
243 
244  @property
245  @esphome_float_state_property
246  def target_temperature(self) -> float | None:
247  """Return the temperature we try to reach."""
248  return self._state_state.target_temperature
249 
250  @property
251  @esphome_float_state_property
252  def target_temperature_low(self) -> float | None:
253  """Return the lowbound target temperature we try to reach."""
254  return self._state_state.target_temperature_low
255 
256  @property
257  @esphome_float_state_property
258  def target_temperature_high(self) -> float | None:
259  """Return the highbound target temperature we try to reach."""
260  return self._state_state.target_temperature_high
261 
262  @property
263  @esphome_state_property
264  def target_humidity(self) -> int:
265  """Return the humidity we try to reach."""
266  return round(self._state_state.target_humidity)
267 
268  @convert_api_error_ha_error
269  async def async_set_temperature(self, **kwargs: Any) -> None:
270  """Set new target temperature (and operation mode if set)."""
271  data: dict[str, Any] = {"key": self._key_key}
272  if ATTR_HVAC_MODE in kwargs:
273  data["mode"] = _CLIMATE_MODES.from_hass(
274  cast(HVACMode, kwargs[ATTR_HVAC_MODE])
275  )
276  if ATTR_TEMPERATURE in kwargs:
277  data["target_temperature"] = kwargs[ATTR_TEMPERATURE]
278  if ATTR_TARGET_TEMP_LOW in kwargs:
279  data["target_temperature_low"] = kwargs[ATTR_TARGET_TEMP_LOW]
280  if ATTR_TARGET_TEMP_HIGH in kwargs:
281  data["target_temperature_high"] = kwargs[ATTR_TARGET_TEMP_HIGH]
282  self._client_client.climate_command(**data)
283 
284  @convert_api_error_ha_error
285  async def async_set_humidity(self, humidity: int) -> None:
286  """Set new target humidity."""
287  self._client_client.climate_command(key=self._key_key, target_humidity=humidity)
288 
289  @convert_api_error_ha_error
290  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
291  """Set new target operation mode."""
292  self._client_client.climate_command(
293  key=self._key_key, mode=_CLIMATE_MODES.from_hass(hvac_mode)
294  )
295 
296  @convert_api_error_ha_error
297  async def async_set_preset_mode(self, preset_mode: str) -> None:
298  """Set preset mode."""
299  kwargs: dict[str, Any] = {"key": self._key_key}
300  if preset_mode in self._static_info_static_info.supported_custom_presets:
301  kwargs["custom_preset"] = preset_mode
302  else:
303  kwargs["preset"] = _PRESETS.from_hass(preset_mode)
304  self._client_client.climate_command(**kwargs)
305 
306  @convert_api_error_ha_error
307  async def async_set_fan_mode(self, fan_mode: str) -> None:
308  """Set new fan mode."""
309  kwargs: dict[str, Any] = {"key": self._key_key}
310  if fan_mode in self._static_info_static_info.supported_custom_fan_modes:
311  kwargs["custom_fan_mode"] = fan_mode
312  else:
313  kwargs["fan_mode"] = _FAN_MODES.from_hass(fan_mode)
314  self._client_client.climate_command(**kwargs)
315 
316  @convert_api_error_ha_error
317  async def async_set_swing_mode(self, swing_mode: str) -> None:
318  """Set new swing mode."""
319  self._client_client.climate_command(
320  key=self._key_key, swing_mode=_SWING_MODES.from_hass(swing_mode)
321  )
322 
323 
324 async_setup_entry = partial(
325  platform_async_setup_entry,
326  info_type=ClimateInfo,
327  entity_type=EsphomeClimateEntity,
328  state_type=ClimateState,
329 )
None _on_static_info_update(self, EntityInfo static_info)
Definition: climate.py:135