Home Assistant Unofficial Reference 2024.12.1
climate.py
Go to the documentation of this file.
1 """AirTouch 5 component to control AirTouch 5 Climate Devices."""
2 
3 import logging
4 from typing import Any
5 
6 from airtouch5py.airtouch5_simple_client import Airtouch5SimpleClient
7 from airtouch5py.packets.ac_ability import AcAbility
8 from airtouch5py.packets.ac_control import (
9  AcControl,
10  SetAcFanSpeed,
11  SetAcMode,
12  SetpointControl,
13  SetPowerSetting,
14 )
15 from airtouch5py.packets.ac_status import AcFanSpeed, AcMode, AcPowerState, AcStatus
16 from airtouch5py.packets.zone_control import (
17  ZoneControlZone,
18  ZoneSettingPower,
19  ZoneSettingValue,
20 )
21 from airtouch5py.packets.zone_name import ZoneName
22 from airtouch5py.packets.zone_status import ZonePowerState, ZoneStatusZone
23 
25  FAN_AUTO,
26  FAN_DIFFUSE,
27  FAN_FOCUS,
28  FAN_HIGH,
29  FAN_LOW,
30  FAN_MEDIUM,
31  PRESET_BOOST,
32  PRESET_NONE,
33  ClimateEntity,
34  ClimateEntityFeature,
35  HVACMode,
36 )
37 from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
38 from homeassistant.core import HomeAssistant, callback
39 from homeassistant.helpers.device_registry import DeviceInfo
40 from homeassistant.helpers.entity_platform import AddEntitiesCallback
41 
42 from . import Airtouch5ConfigEntry
43 from .const import DOMAIN, FAN_INTELLIGENT_AUTO, FAN_TURBO
44 from .entity import Airtouch5Entity
45 
46 _LOGGER = logging.getLogger(__name__)
47 
48 AC_MODE_TO_HVAC_MODE = {
49  AcMode.AUTO: HVACMode.AUTO,
50  AcMode.AUTO_COOL: HVACMode.AUTO,
51  AcMode.AUTO_HEAT: HVACMode.AUTO,
52  AcMode.COOL: HVACMode.COOL,
53  AcMode.DRY: HVACMode.DRY,
54  AcMode.FAN: HVACMode.FAN_ONLY,
55  AcMode.HEAT: HVACMode.HEAT,
56 }
57 HVAC_MODE_TO_SET_AC_MODE = {
58  HVACMode.AUTO: SetAcMode.SET_TO_AUTO,
59  HVACMode.COOL: SetAcMode.SET_TO_COOL,
60  HVACMode.DRY: SetAcMode.SET_TO_DRY,
61  HVACMode.FAN_ONLY: SetAcMode.SET_TO_FAN,
62  HVACMode.HEAT: SetAcMode.SET_TO_HEAT,
63 }
64 
65 
66 AC_FAN_SPEED_TO_FAN_SPEED = {
67  AcFanSpeed.AUTO: FAN_AUTO,
68  AcFanSpeed.QUIET: FAN_DIFFUSE,
69  AcFanSpeed.LOW: FAN_LOW,
70  AcFanSpeed.MEDIUM: FAN_MEDIUM,
71  AcFanSpeed.HIGH: FAN_HIGH,
72  AcFanSpeed.POWERFUL: FAN_FOCUS,
73  AcFanSpeed.TURBO: FAN_TURBO,
74  AcFanSpeed.INTELLIGENT_AUTO_1: FAN_INTELLIGENT_AUTO,
75  AcFanSpeed.INTELLIGENT_AUTO_2: FAN_INTELLIGENT_AUTO,
76  AcFanSpeed.INTELLIGENT_AUTO_3: FAN_INTELLIGENT_AUTO,
77  AcFanSpeed.INTELLIGENT_AUTO_4: FAN_INTELLIGENT_AUTO,
78  AcFanSpeed.INTELLIGENT_AUTO_5: FAN_INTELLIGENT_AUTO,
79  AcFanSpeed.INTELLIGENT_AUTO_6: FAN_INTELLIGENT_AUTO,
80 }
81 FAN_MODE_TO_SET_AC_FAN_SPEED = {
82  FAN_AUTO: SetAcFanSpeed.SET_TO_AUTO,
83  FAN_DIFFUSE: SetAcFanSpeed.SET_TO_QUIET,
84  FAN_LOW: SetAcFanSpeed.SET_TO_LOW,
85  FAN_MEDIUM: SetAcFanSpeed.SET_TO_MEDIUM,
86  FAN_HIGH: SetAcFanSpeed.SET_TO_HIGH,
87  FAN_FOCUS: SetAcFanSpeed.SET_TO_POWERFUL,
88  FAN_TURBO: SetAcFanSpeed.SET_TO_TURBO,
89  FAN_INTELLIGENT_AUTO: SetAcFanSpeed.SET_TO_INTELLIGENT_AUTO,
90 }
91 
92 
94  hass: HomeAssistant,
95  config_entry: Airtouch5ConfigEntry,
96  async_add_entities: AddEntitiesCallback,
97 ) -> None:
98  """Set up the Airtouch 5 Climate entities."""
99  client = config_entry.runtime_data
100 
101  entities: list[ClimateEntity] = []
102 
103  # Add each AC (and remember what zones they apply to).
104  # Each zone is controlled by a single AC
105  zone_to_ac: dict[int, AcAbility] = {}
106  for ac in client.ac:
107  for i in range(ac.start_zone_number, ac.start_zone_number + ac.zone_count):
108  zone_to_ac[i] = ac
109  entities.append(Airtouch5AC(client, ac))
110 
111  # Add each zone
112  entities.extend(
113  Airtouch5Zone(client, zone, zone_to_ac[zone.zone_number])
114  for zone in client.zones
115  )
116 
117  async_add_entities(entities)
118 
119 
121  """Base class for Airtouch5 Climate Entities."""
122 
123  _attr_temperature_unit = UnitOfTemperature.CELSIUS
124  _attr_translation_key = DOMAIN
125  _attr_target_temperature_step = 1
126  _attr_name = None
127  _enable_turn_on_off_backwards_compatibility = False
128 
129 
131  """Representation of the AC unit. Used to control the overall HVAC Mode."""
132 
133  def __init__(self, client: Airtouch5SimpleClient, ability: AcAbility) -> None:
134  """Initialise the Climate Entity."""
135  super().__init__(client)
136  self._ability_ability = ability
137  self._attr_unique_id_attr_unique_id = f"ac_{ability.ac_number}"
138  self._attr_device_info_attr_device_info = DeviceInfo(
139  identifiers={(DOMAIN, f"ac_{ability.ac_number}")},
140  name=f"AC {ability.ac_number}",
141  manufacturer="Polyaire",
142  model="AirTouch 5",
143  )
144  self._attr_hvac_modes_attr_hvac_modes = [HVACMode.OFF]
145  if ability.supports_mode_auto:
146  self._attr_hvac_modes_attr_hvac_modes.append(HVACMode.AUTO)
147  if ability.supports_mode_cool:
148  self._attr_hvac_modes_attr_hvac_modes.append(HVACMode.COOL)
149  if ability.supports_mode_dry:
150  self._attr_hvac_modes_attr_hvac_modes.append(HVACMode.DRY)
151  if ability.supports_mode_fan:
152  self._attr_hvac_modes_attr_hvac_modes.append(HVACMode.FAN_ONLY)
153  if ability.supports_mode_heat:
154  self._attr_hvac_modes_attr_hvac_modes.append(HVACMode.HEAT)
155 
156  self._attr_supported_features_attr_supported_features = (
157  ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
158  )
159  if len(self.hvac_modeshvac_modes) > 1:
160  self._attr_supported_features_attr_supported_features |= (
161  ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
162  )
163 
164  self._attr_fan_modes_attr_fan_modes = []
165  if ability.supports_fan_speed_quiet:
166  self._attr_fan_modes_attr_fan_modes.append(FAN_DIFFUSE)
167  if ability.supports_fan_speed_low:
168  self._attr_fan_modes_attr_fan_modes.append(FAN_LOW)
169  if ability.supports_fan_speed_medium:
170  self._attr_fan_modes_attr_fan_modes.append(FAN_MEDIUM)
171  if ability.supports_fan_speed_high:
172  self._attr_fan_modes_attr_fan_modes.append(FAN_HIGH)
173  if ability.supports_fan_speed_powerful:
174  self._attr_fan_modes_attr_fan_modes.append(FAN_FOCUS)
175  if ability.supports_fan_speed_turbo:
176  self._attr_fan_modes_attr_fan_modes.append(FAN_TURBO)
177  if ability.supports_fan_speed_auto:
178  self._attr_fan_modes_attr_fan_modes.append(FAN_AUTO)
179  if ability.supports_fan_speed_intelligent_auto:
180  self._attr_fan_modes_attr_fan_modes.append(FAN_INTELLIGENT_AUTO)
181 
182  # We can have different setpoints for heat cool, we expose the lowest low and highest high
183  self._attr_min_temp_attr_min_temp = min(
184  ability.min_cool_set_point, ability.min_heat_set_point
185  )
186  self._attr_max_temp_attr_max_temp = max(
187  ability.max_cool_set_point, ability.max_heat_set_point
188  )
189 
190  @callback
191  def _async_update_attrs(self, data: dict[int, AcStatus]) -> None:
192  if self._ability_ability.ac_number not in data:
193  return
194  status = data[self._ability_ability.ac_number]
195 
196  self._attr_current_temperature_attr_current_temperature = status.temperature
197  self._attr_target_temperature_attr_target_temperature = status.ac_setpoint
198  if status.ac_power_state in [AcPowerState.OFF, AcPowerState.AWAY_OFF]:
199  self._attr_hvac_mode_attr_hvac_mode = HVACMode.OFF
200  else:
201  self._attr_hvac_mode_attr_hvac_mode = AC_MODE_TO_HVAC_MODE[status.ac_mode]
202  self._attr_fan_mode_attr_fan_mode = AC_FAN_SPEED_TO_FAN_SPEED[status.ac_fan_speed]
203  self.async_write_ha_stateasync_write_ha_state()
204 
205  async def async_added_to_hass(self) -> None:
206  """Add data updated listener after this object has been initialized."""
207  await super().async_added_to_hass()
208  self._client_client.ac_status_callbacks.append(self._async_update_attrs_async_update_attrs)
209  self._async_update_attrs_async_update_attrs(self._client_client.latest_ac_status)
210 
211  async def async_will_remove_from_hass(self) -> None:
212  """Remove data updated listener after this object has been initialized."""
213  await super().async_will_remove_from_hass()
214  self._client_client.ac_status_callbacks.remove(self._async_update_attrs_async_update_attrs)
215 
216  async def _control(
217  self,
218  *,
219  power: SetPowerSetting = SetPowerSetting.KEEP_POWER_SETTING,
220  ac_mode: SetAcMode = SetAcMode.KEEP_AC_MODE,
221  fan: SetAcFanSpeed = SetAcFanSpeed.KEEP_AC_FAN_SPEED,
222  setpoint: SetpointControl = SetpointControl.KEEP_SETPOINT_VALUE,
223  temp: int = 0,
224  ) -> None:
225  control = AcControl(
226  power,
227  self._ability_ability.ac_number,
228  ac_mode,
229  fan,
230  setpoint,
231  temp,
232  )
233  packet = self._client_client.data_packet_factory.ac_control([control])
234  await self._client_client.send_packet(packet)
235 
236  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
237  """Set new operation mode."""
238  set_power_setting: SetPowerSetting
239  set_ac_mode: SetAcMode
240 
241  if hvac_mode == HVACMode.OFF:
242  set_power_setting = SetPowerSetting.SET_TO_OFF
243  set_ac_mode = SetAcMode.KEEP_AC_MODE
244  else:
245  set_power_setting = SetPowerSetting.SET_TO_ON
246  if hvac_mode not in HVAC_MODE_TO_SET_AC_MODE:
247  raise ValueError(f"Unsupported hvac mode: {hvac_mode}")
248  set_ac_mode = HVAC_MODE_TO_SET_AC_MODE[hvac_mode]
249 
250  await self._control_control(power=set_power_setting, ac_mode=set_ac_mode)
251 
252  async def async_set_fan_mode(self, fan_mode: str) -> None:
253  """Set new fan mode."""
254  if fan_mode not in FAN_MODE_TO_SET_AC_FAN_SPEED:
255  raise ValueError(f"Unsupported fan mode: {fan_mode}")
256  fan_speed = FAN_MODE_TO_SET_AC_FAN_SPEED[fan_mode]
257  await self._control_control(fan=fan_speed)
258 
259  async def async_set_temperature(self, **kwargs: Any) -> None:
260  """Set new target temperatures."""
261  if (temp := kwargs.get(ATTR_TEMPERATURE)) is None:
262  _LOGGER.debug("Argument `temperature` is missing in set_temperature")
263  return
264 
265  await self._control_control(setpoint=SetpointControl.CHANGE_SETPOINT, temp=temp)
266 
267 
269  """Representation of a Zone. Used to control the AC effect in the zone."""
270 
271  _attr_hvac_modes = [HVACMode.OFF, HVACMode.FAN_ONLY]
272  _attr_preset_modes = [PRESET_NONE, PRESET_BOOST]
273  _attr_supported_features = (
274  ClimateEntityFeature.TARGET_TEMPERATURE
275  | ClimateEntityFeature.PRESET_MODE
276  | ClimateEntityFeature.TURN_OFF
277  | ClimateEntityFeature.TURN_ON
278  )
279 
280  def __init__(
281  self, client: Airtouch5SimpleClient, name: ZoneName, ac: AcAbility
282  ) -> None:
283  """Initialise the Climate Entity."""
284  super().__init__(client)
285  self._name_name = name
286 
287  self._attr_unique_id_attr_unique_id = f"zone_{name.zone_number}"
288  self._attr_device_info_attr_device_info = DeviceInfo(
289  identifiers={(DOMAIN, f"zone_{name.zone_number}")},
290  name=name.zone_name,
291  manufacturer="Polyaire",
292  model="AirTouch 5",
293  )
294  # We can have different setpoints for heat and cool, we expose the lowest low and highest high
295  self._attr_min_temp_attr_min_temp = min(ac.min_cool_set_point, ac.min_heat_set_point)
296  self._attr_max_temp_attr_max_temp = max(ac.max_cool_set_point, ac.max_heat_set_point)
297 
298  @callback
299  def _async_update_attrs(self, data: dict[int, ZoneStatusZone]) -> None:
300  if self._name_name.zone_number not in data:
301  return
302  status = data[self._name_name.zone_number]
303  self._attr_current_temperature_attr_current_temperature = status.temperature
304  self._attr_target_temperature_attr_target_temperature = status.set_point
305 
306  if status.zone_power_state == ZonePowerState.OFF:
307  self._attr_hvac_mode_attr_hvac_mode = HVACMode.OFF
308  self._attr_preset_mode_attr_preset_mode = PRESET_NONE
309  elif status.zone_power_state == ZonePowerState.ON:
310  self._attr_hvac_mode_attr_hvac_mode = HVACMode.FAN_ONLY
311  self._attr_preset_mode_attr_preset_mode = PRESET_NONE
312  elif status.zone_power_state == ZonePowerState.TURBO:
313  self._attr_hvac_mode_attr_hvac_mode = HVACMode.FAN_ONLY
314  self._attr_preset_mode_attr_preset_mode = PRESET_BOOST
315  else:
316  self._attr_hvac_mode_attr_hvac_mode = None
317 
318  self.async_write_ha_stateasync_write_ha_state()
319 
320  async def async_added_to_hass(self) -> None:
321  """Add data updated listener after this object has been initialized."""
322  await super().async_added_to_hass()
323  self._client_client.zone_status_callbacks.append(self._async_update_attrs_async_update_attrs)
324  self._async_update_attrs_async_update_attrs(self._client_client.latest_zone_status)
325 
326  async def async_will_remove_from_hass(self) -> None:
327  """Remove data updated listener after this object has been initialized."""
328  await super().async_will_remove_from_hass()
329  self._client_client.zone_status_callbacks.remove(self._async_update_attrs_async_update_attrs)
330 
331  async def _control(
332  self,
333  *,
334  zsv: ZoneSettingValue = ZoneSettingValue.KEEP_SETTING_VALUE,
335  power: ZoneSettingPower = ZoneSettingPower.KEEP_POWER_STATE,
336  value: float = 0,
337  ) -> None:
338  control = ZoneControlZone(self._name_name.zone_number, zsv, power, value)
339  packet = self._client_client.data_packet_factory.zone_control([control])
340  await self._client_client.send_packet(packet)
341 
342  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
343  """Set new operation mode."""
344  power: ZoneSettingPower
345 
346  if hvac_mode is HVACMode.OFF:
347  power = ZoneSettingPower.SET_TO_OFF
348  elif self._attr_preset_mode_attr_preset_mode is PRESET_BOOST:
349  power = ZoneSettingPower.SET_TO_TURBO
350  else:
351  power = ZoneSettingPower.SET_TO_ON
352 
353  await self._control_control(power=power)
354 
355  async def async_set_preset_mode(self, preset_mode: str) -> None:
356  """Enable or disable Turbo. Done this way as we can't have a turbo HVACMode."""
357  power: ZoneSettingPower
358  if preset_mode == PRESET_BOOST:
359  power = ZoneSettingPower.SET_TO_TURBO
360  else:
361  power = ZoneSettingPower.SET_TO_ON
362 
363  await self._control_control(power=power)
364 
365  async def async_set_temperature(self, **kwargs: Any) -> None:
366  """Set new target temperatures."""
367 
368  if (temp := kwargs.get(ATTR_TEMPERATURE)) is None:
369  _LOGGER.debug("Argument `temperature` is missing in set_temperature")
370  return
371 
372  await self._control_control(
373  zsv=ZoneSettingValue.SET_TARGET_SETPOINT,
374  value=float(temp),
375  )
376 
377  async def async_turn_on(self) -> None:
378  """Turn the zone on."""
379  await self.async_set_hvac_modeasync_set_hvac_modeasync_set_hvac_mode(HVACMode.FAN_ONLY)
380 
381  async def async_turn_off(self) -> None:
382  """Turn the zone off."""
383  await self.async_set_hvac_modeasync_set_hvac_modeasync_set_hvac_mode(HVACMode.OFF)
None async_set_hvac_mode(self, HVACMode hvac_mode)
Definition: climate.py:236
None __init__(self, Airtouch5SimpleClient client, AcAbility ability)
Definition: climate.py:133
None _async_update_attrs(self, dict[int, AcStatus] data)
Definition: climate.py:191
None _control(self, *SetPowerSetting power=SetPowerSetting.KEEP_POWER_SETTING, SetAcMode ac_mode=SetAcMode.KEEP_AC_MODE, SetAcFanSpeed fan=SetAcFanSpeed.KEEP_AC_FAN_SPEED, SetpointControl setpoint=SetpointControl.KEEP_SETPOINT_VALUE, int temp=0)
Definition: climate.py:224
None _control(self, *ZoneSettingValue zsv=ZoneSettingValue.KEEP_SETTING_VALUE, ZoneSettingPower power=ZoneSettingPower.KEEP_POWER_STATE, float value=0)
Definition: climate.py:337
None async_set_hvac_mode(self, HVACMode hvac_mode)
Definition: climate.py:342
None __init__(self, Airtouch5SimpleClient client, ZoneName name, AcAbility ac)
Definition: climate.py:282
None _async_update_attrs(self, dict[int, ZoneStatusZone] data)
Definition: climate.py:299
None async_set_hvac_mode(self, HVACMode hvac_mode)
Definition: __init__.py:813
None async_setup_entry(HomeAssistant hass, Airtouch5ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: climate.py:97