Home Assistant Unofficial Reference 2024.12.1
climate.py
Go to the documentation of this file.
1 """Platform for climate integration."""
2 
3 from __future__ import annotations
4 
5 from datetime import timedelta
6 from typing import Any
7 
8 from pymelcloud import DEVICE_TYPE_ATA, DEVICE_TYPE_ATW, AtaDevice, AtwDevice
9 import pymelcloud.ata_device as ata
10 import pymelcloud.atw_device as atw
11 from pymelcloud.atw_device import (
12  PROPERTY_ZONE_1_OPERATION_MODE,
13  PROPERTY_ZONE_2_OPERATION_MODE,
14  Zone,
15 )
16 import voluptuous as vol
17 
19  ATTR_HVAC_MODE,
20  DEFAULT_MAX_TEMP,
21  DEFAULT_MIN_TEMP,
22  ClimateEntity,
23  ClimateEntityFeature,
24  HVACAction,
25  HVACMode,
26 )
27 from homeassistant.config_entries import ConfigEntry
28 from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
29 from homeassistant.core import HomeAssistant
30 from homeassistant.helpers import config_validation as cv, entity_platform
31 from homeassistant.helpers.entity_platform import AddEntitiesCallback
32 
33 from . import MelCloudDevice
34 from .const import (
35  ATTR_STATUS,
36  ATTR_VANE_HORIZONTAL,
37  ATTR_VANE_HORIZONTAL_POSITIONS,
38  ATTR_VANE_VERTICAL,
39  ATTR_VANE_VERTICAL_POSITIONS,
40  CONF_POSITION,
41  DOMAIN,
42  SERVICE_SET_VANE_HORIZONTAL,
43  SERVICE_SET_VANE_VERTICAL,
44 )
45 
46 SCAN_INTERVAL = timedelta(seconds=60)
47 
48 
49 ATA_HVAC_MODE_LOOKUP = {
50  ata.OPERATION_MODE_HEAT: HVACMode.HEAT,
51  ata.OPERATION_MODE_DRY: HVACMode.DRY,
52  ata.OPERATION_MODE_COOL: HVACMode.COOL,
53  ata.OPERATION_MODE_FAN_ONLY: HVACMode.FAN_ONLY,
54  ata.OPERATION_MODE_HEAT_COOL: HVACMode.HEAT_COOL,
55 }
56 ATA_HVAC_MODE_REVERSE_LOOKUP = {v: k for k, v in ATA_HVAC_MODE_LOOKUP.items()}
57 
58 
59 ATW_ZONE_HVAC_MODE_LOOKUP = {
60  atw.ZONE_OPERATION_MODE_HEAT: HVACMode.HEAT,
61  atw.ZONE_OPERATION_MODE_COOL: HVACMode.COOL,
62 }
63 ATW_ZONE_HVAC_MODE_REVERSE_LOOKUP = {v: k for k, v in ATW_ZONE_HVAC_MODE_LOOKUP.items()}
64 
65 ATW_ZONE_HVAC_ACTION_LOOKUP = {
66  atw.STATUS_IDLE: HVACAction.IDLE,
67  atw.STATUS_HEAT_ZONES: HVACAction.HEATING,
68  atw.STATUS_COOL: HVACAction.COOLING,
69  atw.STATUS_STANDBY: HVACAction.IDLE,
70  # Heating water tank, so the zone is idle
71  atw.STATUS_HEAT_WATER: HVACAction.IDLE,
72  atw.STATUS_LEGIONELLA: HVACAction.IDLE,
73  # Heat pump cannot heat in this mode, but will be ready soon
74  atw.STATUS_DEFROST: HVACAction.PREHEATING,
75 }
76 
77 
79  hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
80 ) -> None:
81  """Set up MelCloud device climate based on config_entry."""
82  mel_devices = hass.data[DOMAIN][entry.entry_id]
83  entities: list[AtaDeviceClimate | AtwDeviceZoneClimate] = [
84  AtaDeviceClimate(mel_device, mel_device.device)
85  for mel_device in mel_devices[DEVICE_TYPE_ATA]
86  ]
87  entities.extend(
88  [
89  AtwDeviceZoneClimate(mel_device, mel_device.device, zone)
90  for mel_device in mel_devices[DEVICE_TYPE_ATW]
91  for zone in mel_device.device.zones
92  ]
93  )
95  entities,
96  True,
97  )
98 
99  platform = entity_platform.async_get_current_platform()
100  platform.async_register_entity_service(
101  SERVICE_SET_VANE_HORIZONTAL,
102  {vol.Required(CONF_POSITION): cv.string},
103  "async_set_vane_horizontal",
104  )
105  platform.async_register_entity_service(
106  SERVICE_SET_VANE_VERTICAL,
107  {vol.Required(CONF_POSITION): cv.string},
108  "async_set_vane_vertical",
109  )
110 
111 
113  """Base climate device."""
114 
115  _attr_temperature_unit = UnitOfTemperature.CELSIUS
116  _attr_has_entity_name = True
117  _attr_name = None
118  _enable_turn_on_off_backwards_compatibility = False
119 
120  def __init__(self, device: MelCloudDevice) -> None:
121  """Initialize the climate."""
122  self.apiapi = device
123  self._base_device_base_device = self.apiapi.device
124 
125  async def async_update(self) -> None:
126  """Update state from MELCloud."""
127  await self.apiapi.async_update()
128 
129  @property
130  def target_temperature_step(self) -> float | None:
131  """Return the supported step of target temperature."""
132  return self._base_device_base_device.temperature_increment
133 
134 
136  """Air-to-Air climate device."""
137 
138  _attr_supported_features = (
139  ClimateEntityFeature.FAN_MODE
140  | ClimateEntityFeature.TARGET_TEMPERATURE
141  | ClimateEntityFeature.SWING_MODE
142  | ClimateEntityFeature.TURN_OFF
143  | ClimateEntityFeature.TURN_ON
144  )
145 
146  def __init__(self, device: MelCloudDevice, ata_device: AtaDevice) -> None:
147  """Initialize the climate."""
148  super().__init__(device)
149  self._device_device = ata_device
150 
151  self._attr_unique_id_attr_unique_id = f"{self.api.device.serial}-{self.api.device.mac}"
152  self._attr_device_info_attr_device_info = self.apiapi.device_info
153 
154  @property
155  def extra_state_attributes(self) -> dict[str, Any] | None:
156  """Return the optional state attributes with device specific additions."""
157  attr = {}
158 
159  if vane_horizontal := self._device_device.vane_horizontal:
160  attr.update(
161  {
162  ATTR_VANE_HORIZONTAL: vane_horizontal,
163  ATTR_VANE_HORIZONTAL_POSITIONS: self._device_device.vane_horizontal_positions,
164  }
165  )
166 
167  if vane_vertical := self._device_device.vane_vertical:
168  attr.update(
169  {
170  ATTR_VANE_VERTICAL: vane_vertical,
171  ATTR_VANE_VERTICAL_POSITIONS: self._device_device.vane_vertical_positions,
172  }
173  )
174  return attr
175 
176  @property
177  def hvac_mode(self) -> HVACMode | None:
178  """Return hvac operation ie. heat, cool mode."""
179  mode = self._device_device.operation_mode
180  if not self._device_device.power or mode is None:
181  return HVACMode.OFF
182  return ATA_HVAC_MODE_LOOKUP.get(mode)
183 
185  self, hvac_mode: HVACMode, set_dict: dict[str, Any]
186  ) -> None:
187  """Apply hvac mode changes to a dict used to call _device.set."""
188  if hvac_mode == HVACMode.OFF:
189  set_dict["power"] = False
190  return
191 
192  operation_mode = ATA_HVAC_MODE_REVERSE_LOOKUP.get(hvac_mode)
193  if operation_mode is None:
194  raise ValueError(f"Invalid hvac_mode [{hvac_mode}]")
195 
196  set_dict["operation_mode"] = operation_mode
197  if self.hvac_modehvac_modehvac_modehvac_modehvac_mode == HVACMode.OFF:
198  set_dict["power"] = True
199 
200  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
201  """Set new target hvac mode."""
202  set_dict: dict[str, Any] = {}
203  self._apply_set_hvac_mode_apply_set_hvac_mode(hvac_mode, set_dict)
204  await self._device_device.set(set_dict)
205 
206  @property
207  def hvac_modes(self) -> list[HVACMode]:
208  """Return the list of available hvac operation modes."""
209  return [HVACMode.OFF] + [
210  ATA_HVAC_MODE_LOOKUP[mode]
211  for mode in self._device_device.operation_modes
212  if mode in ATA_HVAC_MODE_LOOKUP
213  ]
214 
215  @property
216  def current_temperature(self) -> float | None:
217  """Return the current temperature."""
218  return self._device_device.room_temperature
219 
220  @property
221  def target_temperature(self) -> float | None:
222  """Return the temperature we try to reach."""
223  return self._device_device.target_temperature
224 
225  async def async_set_temperature(self, **kwargs: Any) -> None:
226  """Set new target temperature."""
227  set_dict: dict[str, Any] = {}
228  if ATTR_HVAC_MODE in kwargs:
229  self._apply_set_hvac_mode_apply_set_hvac_mode(
230  kwargs.get(ATTR_HVAC_MODE, self.hvac_modehvac_modehvac_modehvac_modehvac_mode), set_dict
231  )
232 
233  if ATTR_TEMPERATURE in kwargs:
234  set_dict["target_temperature"] = kwargs.get(ATTR_TEMPERATURE)
235 
236  if set_dict:
237  await self._device_device.set(set_dict)
238 
239  @property
240  def fan_mode(self) -> str | None:
241  """Return the fan setting."""
242  return self._device_device.fan_speed
243 
244  async def async_set_fan_mode(self, fan_mode: str) -> None:
245  """Set new target fan mode."""
246  await self._device_device.set({"fan_speed": fan_mode})
247 
248  @property
249  def fan_modes(self) -> list[str] | None:
250  """Return the list of available fan modes."""
251  return self._device_device.fan_speeds
252 
253  async def async_set_vane_horizontal(self, position: str) -> None:
254  """Set horizontal vane position."""
255  if position not in self._device_device.vane_horizontal_positions:
256  raise ValueError(
257  f"Invalid horizontal vane position {position}. Valid positions:"
258  f" [{self._device.vane_horizontal_positions}]."
259  )
260  await self._device_device.set({ata.PROPERTY_VANE_HORIZONTAL: position})
261 
262  async def async_set_vane_vertical(self, position: str) -> None:
263  """Set vertical vane position."""
264  if position not in self._device_device.vane_vertical_positions:
265  raise ValueError(
266  f"Invalid vertical vane position {position}. Valid positions:"
267  f" [{self._device.vane_vertical_positions}]."
268  )
269  await self._device_device.set({ata.PROPERTY_VANE_VERTICAL: position})
270 
271  @property
272  def swing_mode(self) -> str | None:
273  """Return vertical vane position or mode."""
274  return self._device_device.vane_vertical
275 
276  async def async_set_swing_mode(self, swing_mode: str) -> None:
277  """Set vertical vane position or mode."""
278  await self.async_set_vane_verticalasync_set_vane_vertical(swing_mode)
279 
280  @property
281  def swing_modes(self) -> list[str] | None:
282  """Return a list of available vertical vane positions and modes."""
283  return self._device_device.vane_vertical_positions
284 
285  async def async_turn_on(self) -> None:
286  """Turn the entity on."""
287  await self._device_device.set({"power": True})
288 
289  async def async_turn_off(self) -> None:
290  """Turn the entity off."""
291  await self._device_device.set({"power": False})
292 
293  @property
294  def min_temp(self) -> float:
295  """Return the minimum temperature."""
296  min_value = self._device_device.target_temperature_min
297  if min_value is not None:
298  return min_value
299 
300  return DEFAULT_MIN_TEMP
301 
302  @property
303  def max_temp(self) -> float:
304  """Return the maximum temperature."""
305  max_value = self._device_device.target_temperature_max
306  if max_value is not None:
307  return max_value
308 
309  return DEFAULT_MAX_TEMP
310 
311 
313  """Air-to-Water zone climate device."""
314 
315  _attr_max_temp = 30
316  _attr_min_temp = 10
317  _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
318 
319  def __init__(
320  self, device: MelCloudDevice, atw_device: AtwDevice, atw_zone: Zone
321  ) -> None:
322  """Initialize the climate."""
323  super().__init__(device)
324  self._device_device = atw_device
325  self._zone_zone = atw_zone
326 
327  self._attr_unique_id_attr_unique_id = f"{self.api.device.serial}-{atw_zone.zone_index}"
328  self._attr_device_info_attr_device_info = self.apiapi.zone_device_info(atw_zone)
329 
330  @property
331  def extra_state_attributes(self) -> dict[str, Any]:
332  """Return the optional state attributes with device specific additions."""
333  return {
334  ATTR_STATUS: ATW_ZONE_HVAC_MODE_LOOKUP.get(
335  self._zone_zone.status, self._zone_zone.status
336  )
337  }
338 
339  @property
340  def hvac_mode(self) -> HVACMode:
341  """Return hvac operation ie. heat, cool mode."""
342  mode = self._zone_zone.operation_mode
343  if not self._device_device.power or mode is None:
344  return HVACMode.OFF
345  return ATW_ZONE_HVAC_MODE_LOOKUP.get(mode, HVACMode.OFF)
346 
347  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
348  """Set new target hvac mode."""
349  if hvac_mode == HVACMode.OFF:
350  await self._device_device.set({"power": False})
351  return
352 
353  operation_mode = ATW_ZONE_HVAC_MODE_REVERSE_LOOKUP.get(hvac_mode)
354  if operation_mode is None:
355  raise ValueError(f"Invalid hvac_mode [{hvac_mode}]")
356 
357  if self._zone_zone.zone_index == 1:
358  props = {PROPERTY_ZONE_1_OPERATION_MODE: operation_mode}
359  else:
360  props = {PROPERTY_ZONE_2_OPERATION_MODE: operation_mode}
361  if self.hvac_modehvac_modehvac_modehvac_modehvac_mode == HVACMode.OFF:
362  props["power"] = True
363  await self._device_device.set(props)
364 
365  @property
366  def hvac_modes(self) -> list[HVACMode]:
367  """Return the list of available hvac operation modes."""
368  return [self.hvac_modehvac_modehvac_modehvac_modehvac_mode]
369 
370  @property
371  def hvac_action(self) -> HVACAction | None:
372  """Return the current running hvac operation."""
373  if not self._device_device.power:
374  return HVACAction.OFF
375  return ATW_ZONE_HVAC_ACTION_LOOKUP.get(self._device_device.status)
376 
377  @property
378  def current_temperature(self) -> float | None:
379  """Return the current temperature."""
380  return self._zone_zone.room_temperature
381 
382  @property
383  def target_temperature(self) -> float | None:
384  """Return the temperature we try to reach."""
385  return self._zone_zone.target_temperature
386 
387  async def async_set_temperature(self, **kwargs: Any) -> None:
388  """Set new target temperature."""
389  await self._zone_zone.set_target_temperature(
390  kwargs.get("temperature", self.target_temperaturetarget_temperaturetarget_temperature)
391  )
None __init__(self, MelCloudDevice device, AtaDevice ata_device)
Definition: climate.py:146
None async_set_hvac_mode(self, HVACMode hvac_mode)
Definition: climate.py:200
None _apply_set_hvac_mode(self, HVACMode hvac_mode, dict[str, Any] set_dict)
Definition: climate.py:186
None __init__(self, MelCloudDevice device, AtwDevice atw_device, Zone atw_zone)
Definition: climate.py:321
None __init__(self, MelCloudDevice device)
Definition: climate.py:120
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: climate.py:80