Home Assistant Unofficial Reference 2024.12.1
climate.py
Go to the documentation of this file.
1 """Support for climate devices through the SmartThings cloud API."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Iterable, Sequence
7 import logging
8 from typing import Any
9 
10 from pysmartthings import Attribute, Capability
11 
13  ATTR_HVAC_MODE,
14  ATTR_TARGET_TEMP_HIGH,
15  ATTR_TARGET_TEMP_LOW,
16  DOMAIN as CLIMATE_DOMAIN,
17  SWING_BOTH,
18  SWING_HORIZONTAL,
19  SWING_OFF,
20  SWING_VERTICAL,
21  ClimateEntity,
22  ClimateEntityFeature,
23  HVACAction,
24  HVACMode,
25 )
26 from homeassistant.config_entries import ConfigEntry
27 from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
28 from homeassistant.core import HomeAssistant
29 from homeassistant.helpers.entity_platform import AddEntitiesCallback
30 
31 from .const import DATA_BROKERS, DOMAIN
32 from .entity import SmartThingsEntity
33 
34 ATTR_OPERATION_STATE = "operation_state"
35 MODE_TO_STATE = {
36  "auto": HVACMode.HEAT_COOL,
37  "cool": HVACMode.COOL,
38  "eco": HVACMode.AUTO,
39  "rush hour": HVACMode.AUTO,
40  "emergency heat": HVACMode.HEAT,
41  "heat": HVACMode.HEAT,
42  "off": HVACMode.OFF,
43 }
44 STATE_TO_MODE = {
45  HVACMode.HEAT_COOL: "auto",
46  HVACMode.COOL: "cool",
47  HVACMode.HEAT: "heat",
48  HVACMode.OFF: "off",
49 }
50 
51 OPERATING_STATE_TO_ACTION = {
52  "cooling": HVACAction.COOLING,
53  "fan only": HVACAction.FAN,
54  "heating": HVACAction.HEATING,
55  "idle": HVACAction.IDLE,
56  "pending cool": HVACAction.COOLING,
57  "pending heat": HVACAction.HEATING,
58  "vent economizer": HVACAction.FAN,
59  "wind": HVACAction.FAN,
60 }
61 
62 AC_MODE_TO_STATE = {
63  "auto": HVACMode.HEAT_COOL,
64  "cool": HVACMode.COOL,
65  "dry": HVACMode.DRY,
66  "coolClean": HVACMode.COOL,
67  "dryClean": HVACMode.DRY,
68  "heat": HVACMode.HEAT,
69  "heatClean": HVACMode.HEAT,
70  "fanOnly": HVACMode.FAN_ONLY,
71  "wind": HVACMode.FAN_ONLY,
72 }
73 STATE_TO_AC_MODE = {
74  HVACMode.HEAT_COOL: "auto",
75  HVACMode.COOL: "cool",
76  HVACMode.DRY: "dry",
77  HVACMode.HEAT: "heat",
78  HVACMode.FAN_ONLY: "fanOnly",
79 }
80 
81 SWING_TO_FAN_OSCILLATION = {
82  SWING_BOTH: "all",
83  SWING_HORIZONTAL: "horizontal",
84  SWING_VERTICAL: "vertical",
85  SWING_OFF: "fixed",
86 }
87 
88 FAN_OSCILLATION_TO_SWING = {
89  value: key for key, value in SWING_TO_FAN_OSCILLATION.items()
90 }
91 
92 WIND = "wind"
93 WINDFREE = "windFree"
94 
95 UNIT_MAP = {"C": UnitOfTemperature.CELSIUS, "F": UnitOfTemperature.FAHRENHEIT}
96 
97 _LOGGER = logging.getLogger(__name__)
98 
99 
101  hass: HomeAssistant,
102  config_entry: ConfigEntry,
103  async_add_entities: AddEntitiesCallback,
104 ) -> None:
105  """Add climate entities for a config entry."""
106  ac_capabilities = [
107  Capability.air_conditioner_mode,
108  Capability.air_conditioner_fan_mode,
109  Capability.switch,
110  Capability.temperature_measurement,
111  Capability.thermostat_cooling_setpoint,
112  ]
113 
114  broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id]
115  entities: list[ClimateEntity] = []
116  for device in broker.devices.values():
117  if not broker.any_assigned(device.device_id, CLIMATE_DOMAIN):
118  continue
119  if all(capability in device.capabilities for capability in ac_capabilities):
120  entities.append(SmartThingsAirConditioner(device))
121  else:
122  entities.append(SmartThingsThermostat(device))
123  async_add_entities(entities, True)
124 
125 
126 def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None:
127  """Return all capabilities supported if minimum required are present."""
128  supported = [
129  Capability.air_conditioner_mode,
130  Capability.demand_response_load_control,
131  Capability.air_conditioner_fan_mode,
132  Capability.switch,
133  Capability.thermostat,
134  Capability.thermostat_cooling_setpoint,
135  Capability.thermostat_fan_mode,
136  Capability.thermostat_heating_setpoint,
137  Capability.thermostat_mode,
138  Capability.thermostat_operating_state,
139  ]
140  # Can have this legacy/deprecated capability
141  if Capability.thermostat in capabilities:
142  return supported
143  # Or must have all of these thermostat capabilities
144  thermostat_capabilities = [
145  Capability.temperature_measurement,
146  Capability.thermostat_heating_setpoint,
147  Capability.thermostat_mode,
148  ]
149  if all(capability in capabilities for capability in thermostat_capabilities):
150  return supported
151  # Or must have all of these A/C capabilities
152  ac_capabilities = [
153  Capability.air_conditioner_mode,
154  Capability.air_conditioner_fan_mode,
155  Capability.switch,
156  Capability.temperature_measurement,
157  Capability.thermostat_cooling_setpoint,
158  ]
159  if all(capability in capabilities for capability in ac_capabilities):
160  return supported
161  return None
162 
163 
165  """Define a SmartThings climate entities."""
166 
167  _enable_turn_on_off_backwards_compatibility = False
168 
169  def __init__(self, device):
170  """Init the class."""
171  super().__init__(device)
172  self._attr_supported_features_attr_supported_features = self._determine_features_determine_features()
173  self._hvac_mode_hvac_mode = None
174  self._hvac_modes_hvac_modes = None
175 
177  flags = (
178  ClimateEntityFeature.TARGET_TEMPERATURE
179  | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
180  | ClimateEntityFeature.TURN_OFF
181  | ClimateEntityFeature.TURN_ON
182  )
183  if self._device_device.get_capability(
184  Capability.thermostat_fan_mode, Capability.thermostat
185  ):
186  flags |= ClimateEntityFeature.FAN_MODE
187  return flags
188 
189  async def async_set_fan_mode(self, fan_mode: str) -> None:
190  """Set new target fan mode."""
191  await self._device_device.set_thermostat_fan_mode(fan_mode, set_status=True)
192 
193  # State is set optimistically in the command above, therefore update
194  # the entity state ahead of receiving the confirming push updates
195  self.async_schedule_update_ha_stateasync_schedule_update_ha_state(True)
196 
197  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
198  """Set new target operation mode."""
199  mode = STATE_TO_MODE[hvac_mode]
200  await self._device_device.set_thermostat_mode(mode, set_status=True)
201 
202  # State is set optimistically in the command above, therefore update
203  # the entity state ahead of receiving the confirming push updates
204  self.async_schedule_update_ha_stateasync_schedule_update_ha_state(True)
205 
206  async def async_set_temperature(self, **kwargs: Any) -> None:
207  """Set new operation mode and target temperatures."""
208  # Operation state
209  if operation_state := kwargs.get(ATTR_HVAC_MODE):
210  mode = STATE_TO_MODE[operation_state]
211  await self._device_device.set_thermostat_mode(mode, set_status=True)
212  await self.async_updateasync_update()
213 
214  # Heat/cool setpoint
215  heating_setpoint = None
216  cooling_setpoint = None
217  if self.hvac_modehvac_modehvac_modehvac_modehvac_mode == HVACMode.HEAT:
218  heating_setpoint = kwargs.get(ATTR_TEMPERATURE)
219  elif self.hvac_modehvac_modehvac_modehvac_modehvac_mode == HVACMode.COOL:
220  cooling_setpoint = kwargs.get(ATTR_TEMPERATURE)
221  else:
222  heating_setpoint = kwargs.get(ATTR_TARGET_TEMP_LOW)
223  cooling_setpoint = kwargs.get(ATTR_TARGET_TEMP_HIGH)
224  tasks = []
225  if heating_setpoint is not None:
226  tasks.append(
227  self._device_device.set_heating_setpoint(
228  round(heating_setpoint, 3), set_status=True
229  )
230  )
231  if cooling_setpoint is not None:
232  tasks.append(
233  self._device_device.set_cooling_setpoint(
234  round(cooling_setpoint, 3), set_status=True
235  )
236  )
237  await asyncio.gather(*tasks)
238 
239  # State is set optimistically in the commands above, therefore update
240  # the entity state ahead of receiving the confirming push updates
241  self.async_schedule_update_ha_stateasync_schedule_update_ha_state(True)
242 
243  async def async_update(self) -> None:
244  """Update the attributes of the climate device."""
245  thermostat_mode = self._device_device.status.thermostat_mode
246  self._hvac_mode_hvac_mode = MODE_TO_STATE.get(thermostat_mode)
247  if self._hvac_mode_hvac_mode is None:
248  _LOGGER.debug(
249  "Device %s (%s) returned an invalid hvac mode: %s",
250  self._device_device.label,
251  self._device_device.device_id,
252  thermostat_mode,
253  )
254 
255  modes = set()
256  supported_modes = self._device_device.status.supported_thermostat_modes
257  if isinstance(supported_modes, Iterable):
258  for mode in supported_modes:
259  if (state := MODE_TO_STATE.get(mode)) is not None:
260  modes.add(state)
261  else:
262  _LOGGER.debug(
263  (
264  "Device %s (%s) returned an invalid supported thermostat"
265  " mode: %s"
266  ),
267  self._device_device.label,
268  self._device_device.device_id,
269  mode,
270  )
271  else:
272  _LOGGER.debug(
273  "Device %s (%s) returned invalid supported thermostat modes: %s",
274  self._device_device.label,
275  self._device_device.device_id,
276  supported_modes,
277  )
278  self._hvac_modes_hvac_modes = list(modes)
279 
280  @property
281  def current_humidity(self):
282  """Return the current humidity."""
283  return self._device_device.status.humidity
284 
285  @property
287  """Return the current temperature."""
288  return self._device_device.status.temperature
289 
290  @property
291  def fan_mode(self):
292  """Return the fan setting."""
293  return self._device_device.status.thermostat_fan_mode
294 
295  @property
296  def fan_modes(self):
297  """Return the list of available fan modes."""
298  return self._device_device.status.supported_thermostat_fan_modes
299 
300  @property
301  def hvac_action(self) -> HVACAction | None:
302  """Return the current running hvac operation if supported."""
303  return OPERATING_STATE_TO_ACTION.get(
304  self._device_device.status.thermostat_operating_state
305  )
306 
307  @property
308  def hvac_mode(self) -> HVACMode:
309  """Return current operation ie. heat, cool, idle."""
310  return self._hvac_mode_hvac_mode
311 
312  @property
313  def hvac_modes(self) -> list[HVACMode]:
314  """Return the list of available operation modes."""
315  return self._hvac_modes_hvac_modes
316 
317  @property
319  """Return the temperature we try to reach."""
320  if self.hvac_modehvac_modehvac_modehvac_modehvac_mode == HVACMode.COOL:
321  return self._device_device.status.cooling_setpoint
322  if self.hvac_modehvac_modehvac_modehvac_modehvac_mode == HVACMode.HEAT:
323  return self._device_device.status.heating_setpoint
324  return None
325 
326  @property
328  """Return the highbound target temperature we try to reach."""
329  if self.hvac_modehvac_modehvac_modehvac_modehvac_mode == HVACMode.HEAT_COOL:
330  return self._device_device.status.cooling_setpoint
331  return None
332 
333  @property
335  """Return the lowbound target temperature we try to reach."""
336  if self.hvac_modehvac_modehvac_modehvac_modehvac_mode == HVACMode.HEAT_COOL:
337  return self._device_device.status.heating_setpoint
338  return None
339 
340  @property
341  def temperature_unit(self):
342  """Return the unit of measurement."""
343  return UNIT_MAP.get(self._device_device.status.attributes[Attribute.temperature].unit)
344 
345 
347  """Define a SmartThings Air Conditioner."""
348 
349  _hvac_modes: list[HVACMode]
350  _enable_turn_on_off_backwards_compatibility = False
351 
352  def __init__(self, device) -> None:
353  """Init the class."""
354  super().__init__(device)
355  self._hvac_modes_hvac_modes = []
356  self._attr_preset_mode_attr_preset_mode = None
357  self._attr_preset_modes_attr_preset_modes = self._determine_preset_modes_determine_preset_modes()
358  self._attr_swing_modes_attr_swing_modes = self._determine_swing_modes_determine_swing_modes()
359  self._attr_supported_features_attr_supported_features = self._determine_supported_features_determine_supported_features()
360 
361  def _determine_supported_features(self) -> ClimateEntityFeature:
362  features = (
363  ClimateEntityFeature.TARGET_TEMPERATURE
364  | ClimateEntityFeature.FAN_MODE
365  | ClimateEntityFeature.TURN_OFF
366  | ClimateEntityFeature.TURN_ON
367  )
368  if self._device_device.get_capability(Capability.fan_oscillation_mode):
369  features |= ClimateEntityFeature.SWING_MODE
370  if (self._attr_preset_modes_attr_preset_modes is not None) and len(self._attr_preset_modes_attr_preset_modes) > 0:
371  features |= ClimateEntityFeature.PRESET_MODE
372  return features
373 
374  async def async_set_fan_mode(self, fan_mode: str) -> None:
375  """Set new target fan mode."""
376  await self._device_device.set_fan_mode(fan_mode, set_status=True)
377 
378  # setting the fan must reset the preset mode (it deactivates the windFree function)
379  self._attr_preset_mode_attr_preset_mode = None
380 
381  # State is set optimistically in the command above, therefore update
382  # the entity state ahead of receiving the confirming push updates
383  self.async_write_ha_stateasync_write_ha_state()
384 
385  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
386  """Set new target operation mode."""
387  if hvac_mode == HVACMode.OFF:
388  await self.async_turn_offasync_turn_offasync_turn_off()
389  return
390  tasks = []
391  # Turn on the device if it's off before setting mode.
392  if not self._device_device.status.switch:
393  tasks.append(self._device_device.switch_on(set_status=True))
394 
395  mode = STATE_TO_AC_MODE[hvac_mode]
396  # If new hvac_mode is HVAC_MODE_FAN_ONLY and AirConditioner support "wind" mode the AirConditioner new mode has to be "wind"
397  # The conversion make the mode change working
398  # The conversion is made only for device that wrongly has capability "wind" instead "fan_only"
399  if hvac_mode == HVACMode.FAN_ONLY:
400  supported_modes = self._device_device.status.supported_ac_modes
401  if WIND in supported_modes:
402  mode = WIND
403 
404  tasks.append(self._device_device.set_air_conditioner_mode(mode, set_status=True))
405  await asyncio.gather(*tasks)
406  # State is set optimistically in the command above, therefore update
407  # the entity state ahead of receiving the confirming push updates
408  self.async_write_ha_stateasync_write_ha_state()
409 
410  async def async_set_temperature(self, **kwargs: Any) -> None:
411  """Set new target temperature."""
412  tasks = []
413  # operation mode
414  if operation_mode := kwargs.get(ATTR_HVAC_MODE):
415  if operation_mode == HVACMode.OFF:
416  tasks.append(self._device_device.switch_off(set_status=True))
417  else:
418  if not self._device_device.status.switch:
419  tasks.append(self._device_device.switch_on(set_status=True))
420  tasks.append(self.async_set_hvac_modeasync_set_hvac_modeasync_set_hvac_mode(operation_mode))
421  # temperature
422  tasks.append(
423  self._device_device.set_cooling_setpoint(kwargs[ATTR_TEMPERATURE], set_status=True)
424  )
425  await asyncio.gather(*tasks)
426  # State is set optimistically in the command above, therefore update
427  # the entity state ahead of receiving the confirming push updates
428  self.async_write_ha_stateasync_write_ha_state()
429 
430  async def async_turn_on(self) -> None:
431  """Turn device on."""
432  await self._device_device.switch_on(set_status=True)
433  # State is set optimistically in the command above, therefore update
434  # the entity state ahead of receiving the confirming push updates
435  self.async_write_ha_stateasync_write_ha_state()
436 
437  async def async_turn_off(self) -> None:
438  """Turn device off."""
439  await self._device_device.switch_off(set_status=True)
440  # State is set optimistically in the command above, therefore update
441  # the entity state ahead of receiving the confirming push updates
442  self.async_write_ha_stateasync_write_ha_state()
443 
444  async def async_update(self) -> None:
445  """Update the calculated fields of the AC."""
446  modes = {HVACMode.OFF}
447  for mode in self._device_device.status.supported_ac_modes:
448  if (state := AC_MODE_TO_STATE.get(mode)) is not None:
449  modes.add(state)
450  else:
451  _LOGGER.debug(
452  "Device %s (%s) returned an invalid supported AC mode: %s",
453  self._device_device.label,
454  self._device_device.device_id,
455  mode,
456  )
457  self._hvac_modes_hvac_modes = list(modes)
458 
459  @property
460  def current_temperature(self) -> float | None:
461  """Return the current temperature."""
462  return self._device_device.status.temperature
463 
464  @property
465  def extra_state_attributes(self) -> dict[str, Any]:
466  """Return device specific state attributes.
467 
468  Include attributes from the Demand Response Load Control (drlc)
469  and Power Consumption capabilities.
470  """
471  attributes = [
472  "drlc_status_duration",
473  "drlc_status_level",
474  "drlc_status_start",
475  "drlc_status_override",
476  ]
477  state_attributes = {}
478  for attribute in attributes:
479  value = getattr(self._device_device.status, attribute)
480  if value is not None:
481  state_attributes[attribute] = value
482  return state_attributes
483 
484  @property
485  def fan_mode(self) -> str:
486  """Return the fan setting."""
487  return self._device_device.status.fan_mode
488 
489  @property
490  def fan_modes(self) -> list[str]:
491  """Return the list of available fan modes."""
492  return self._device_device.status.supported_ac_fan_modes
493 
494  @property
495  def hvac_mode(self) -> HVACMode | None:
496  """Return current operation ie. heat, cool, idle."""
497  if not self._device_device.status.switch:
498  return HVACMode.OFF
499  return AC_MODE_TO_STATE.get(self._device_device.status.air_conditioner_mode)
500 
501  @property
502  def hvac_modes(self) -> list[HVACMode]:
503  """Return the list of available operation modes."""
504  return self._hvac_modes_hvac_modes
505 
506  @property
507  def target_temperature(self) -> float:
508  """Return the temperature we try to reach."""
509  return self._device_device.status.cooling_setpoint
510 
511  @property
512  def temperature_unit(self) -> str:
513  """Return the unit of measurement."""
514  return UNIT_MAP[self._device_device.status.attributes[Attribute.temperature].unit]
515 
516  def _determine_swing_modes(self) -> list[str] | None:
517  """Return the list of available swing modes."""
518  supported_swings = None
519  supported_modes = self._device_device.status.attributes[
520  Attribute.supported_fan_oscillation_modes
521  ][0]
522  if supported_modes is not None:
523  supported_swings = [
524  FAN_OSCILLATION_TO_SWING.get(m, SWING_OFF) for m in supported_modes
525  ]
526  return supported_swings
527 
528  async def async_set_swing_mode(self, swing_mode: str) -> None:
529  """Set swing mode."""
530  fan_oscillation_mode = SWING_TO_FAN_OSCILLATION[swing_mode]
531  await self._device_device.set_fan_oscillation_mode(fan_oscillation_mode)
532 
533  # setting the fan must reset the preset mode (it deactivates the windFree function)
534  self._attr_preset_mode_attr_preset_mode = None
535 
536  self.async_schedule_update_ha_stateasync_schedule_update_ha_state(True)
537 
538  @property
539  def swing_mode(self) -> str:
540  """Return the swing setting."""
541  return FAN_OSCILLATION_TO_SWING.get(
542  self._device_device.status.fan_oscillation_mode, SWING_OFF
543  )
544 
545  def _determine_preset_modes(self) -> list[str] | None:
546  """Return a list of available preset modes."""
547  supported_modes: list | None = self._device_device.status.attributes[
548  "supportedAcOptionalMode"
549  ].value
550  if supported_modes and WINDFREE in supported_modes:
551  return [WINDFREE]
552  return None
553 
554  async def async_set_preset_mode(self, preset_mode: str) -> None:
555  """Set special modes (currently only windFree is supported)."""
556  result = await self._device_device.command(
557  "main",
558  "custom.airConditionerOptionalMode",
559  "setAcOptionalMode",
560  [preset_mode],
561  )
562  if result:
563  self._device_device.status.update_attribute_value("acOptionalMode", preset_mode)
564 
565  self._attr_preset_mode_attr_preset_mode = preset_mode
566 
567  self.async_write_ha_stateasync_write_ha_state()
None async_set_hvac_mode(self, HVACMode hvac_mode)
Definition: __init__.py:813
None async_schedule_update_ha_state(self, bool force_refresh=False)
Definition: entity.py:1265
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: climate.py:104
Sequence[str]|None get_capabilities(Sequence[str] capabilities)
Definition: climate.py:126
Any|None get_capability(HomeAssistant hass, str entity_id, str capability)
Definition: entity.py:139