Home Assistant Unofficial Reference 2024.12.1
climate.py
Go to the documentation of this file.
1 """Support for IntesisHome and airconwithme Smart AC Controllers."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from random import randrange
7 from typing import Any, NamedTuple
8 
9 from pyintesishome import IHAuthenticationError, IHConnectionError, IntesisHome
10 import voluptuous as vol
11 
13  ATTR_HVAC_MODE,
14  PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA,
15  PRESET_BOOST,
16  PRESET_COMFORT,
17  PRESET_ECO,
18  SWING_BOTH,
19  SWING_HORIZONTAL,
20  SWING_OFF,
21  SWING_VERTICAL,
22  ClimateEntity,
23  ClimateEntityFeature,
24  HVACMode,
25 )
26 from homeassistant.const import (
27  ATTR_TEMPERATURE,
28  CONF_DEVICE,
29  CONF_PASSWORD,
30  CONF_USERNAME,
31  UnitOfTemperature,
32 )
33 from homeassistant.core import HomeAssistant
34 from homeassistant.exceptions import PlatformNotReady
35 from homeassistant.helpers.aiohttp_client import async_get_clientsession
37 from homeassistant.helpers.entity_platform import AddEntitiesCallback
38 from homeassistant.helpers.event import async_call_later
39 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
40 
41 _LOGGER = logging.getLogger(__name__)
42 
43 IH_DEVICE_INTESISHOME = "IntesisHome"
44 IH_DEVICE_AIRCONWITHME = "airconwithme"
45 IH_DEVICE_ANYWAIR = "anywair"
46 
47 PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend(
48  {
49  vol.Required(CONF_USERNAME): cv.string,
50  vol.Required(CONF_PASSWORD): cv.string,
51  vol.Optional(CONF_DEVICE, default=IH_DEVICE_INTESISHOME): vol.In(
52  [IH_DEVICE_AIRCONWITHME, IH_DEVICE_ANYWAIR, IH_DEVICE_INTESISHOME]
53  ),
54  }
55 )
56 
57 
58 class SwingSettings(NamedTuple):
59  """Settings for swing mode."""
60 
61  vvane: str
62  hvane: str
63 
64 
65 MAP_IH_TO_HVAC_MODE = {
66  "auto": HVACMode.HEAT_COOL,
67  "cool": HVACMode.COOL,
68  "dry": HVACMode.DRY,
69  "fan": HVACMode.FAN_ONLY,
70  "heat": HVACMode.HEAT,
71  "off": HVACMode.OFF,
72 }
73 MAP_HVAC_MODE_TO_IH = {v: k for k, v in MAP_IH_TO_HVAC_MODE.items()}
74 
75 MAP_IH_TO_PRESET_MODE = {
76  "eco": PRESET_ECO,
77  "comfort": PRESET_COMFORT,
78  "powerful": PRESET_BOOST,
79 }
80 MAP_PRESET_MODE_TO_IH = {v: k for k, v in MAP_IH_TO_PRESET_MODE.items()}
81 
82 IH_SWING_STOP = "auto/stop"
83 IH_SWING_SWING = "swing"
84 MAP_SWING_TO_IH = {
85  SWING_OFF: SwingSettings(vvane=IH_SWING_STOP, hvane=IH_SWING_STOP),
86  SWING_BOTH: SwingSettings(vvane=IH_SWING_SWING, hvane=IH_SWING_SWING),
87  SWING_HORIZONTAL: SwingSettings(vvane=IH_SWING_STOP, hvane=IH_SWING_SWING),
88  SWING_VERTICAL: SwingSettings(vvane=IH_SWING_SWING, hvane=IH_SWING_STOP),
89 }
90 
91 
92 MAP_STATE_ICONS = {
93  HVACMode.COOL: "mdi:snowflake",
94  HVACMode.DRY: "mdi:water-off",
95  HVACMode.FAN_ONLY: "mdi:fan",
96  HVACMode.HEAT: "mdi:white-balance-sunny",
97  HVACMode.HEAT_COOL: "mdi:cached",
98 }
99 
100 
102  hass: HomeAssistant,
103  config: ConfigType,
104  async_add_entities: AddEntitiesCallback,
105  discovery_info: DiscoveryInfoType | None = None,
106 ) -> None:
107  """Create the IntesisHome climate devices."""
108  ih_user = config[CONF_USERNAME]
109  ih_pass = config[CONF_PASSWORD]
110  device_type = config[CONF_DEVICE]
111 
112  controller = IntesisHome(
113  ih_user,
114  ih_pass,
115  hass.loop,
116  websession=async_get_clientsession(hass),
117  device_type=device_type,
118  )
119  try:
120  await controller.poll_status()
121  except IHAuthenticationError:
122  _LOGGER.error("Invalid username or password")
123  return
124  except IHConnectionError as ex:
125  _LOGGER.error("Error connecting to the %s server", device_type)
126  raise PlatformNotReady from ex
127 
128  if ih_devices := controller.get_devices():
130  [
131  IntesisAC(ih_device_id, device, controller)
132  for ih_device_id, device in ih_devices.items()
133  ],
134  True,
135  )
136  else:
137  _LOGGER.error(
138  "Error getting device list from %s API: %s",
139  device_type,
140  controller.error_message,
141  )
142  await controller.stop()
143 
144 
146  """Represents an Intesishome air conditioning device."""
147 
148  _attr_should_poll = False
149  _attr_temperature_unit = UnitOfTemperature.CELSIUS
150  _enable_turn_on_off_backwards_compatibility = False
151 
152  def __init__(self, ih_device_id, ih_device, controller):
153  """Initialize the thermostat."""
154  self._controller_controller = controller
155  self._device_id_device_id = ih_device_id
156  self._ih_device_ih_device = ih_device
157  self._device_name_device_name = ih_device.get("name")
158  self._device_type_device_type = controller.device_type
159  self._connected_connected = None
160  self._setpoint_step_setpoint_step = 1
161  self._current_temp_current_temp = None
162  self._max_temp_max_temp = None
163  self._attr_hvac_modes_attr_hvac_modes = []
164  self._min_temp_min_temp = None
165  self._target_temp_target_temp = None
166  self._outdoor_temp_outdoor_temp = None
167  self._hvac_mode_hvac_mode = None
168  self._preset_preset = None
169  self._preset_list_preset_list = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST]
170  self._run_hours_run_hours = None
171  self._rssi_rssi = None
172  self._swing_list_swing_list = [SWING_OFF]
173  self._vvane_vvane = None
174  self._hvane_hvane = None
175  self._power_power = False
176  self._fan_speed_fan_speed = None
177  self._power_consumption_heat_power_consumption_heat = None
178  self._power_consumption_cool_power_consumption_cool = None
179 
180  # Setpoint support
181  if controller.has_setpoint_control(ih_device_id):
182  self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
183 
184  # Setup swing list
185  if controller.has_vertical_swing(ih_device_id):
186  self._swing_list_swing_list.append(SWING_VERTICAL)
187  if controller.has_horizontal_swing(ih_device_id):
188  self._swing_list_swing_list.append(SWING_HORIZONTAL)
189  if SWING_HORIZONTAL in self._swing_list_swing_list and SWING_VERTICAL in self._swing_list_swing_list:
190  self._swing_list_swing_list.append(SWING_BOTH)
191  if len(self._swing_list_swing_list) > 1:
192  self._attr_supported_features |= ClimateEntityFeature.SWING_MODE
193 
194  # Setup fan speeds
195  self._fan_modes_fan_modes = controller.get_fan_speed_list(ih_device_id)
196  if self._fan_modes_fan_modes:
197  self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
198 
199  # Preset support
200  if ih_device.get("climate_working_mode"):
201  self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
202 
203  # Setup HVAC modes
204  if modes := controller.get_mode_list(ih_device_id):
205  mode_list = [MAP_IH_TO_HVAC_MODE[mode] for mode in modes]
206  self._attr_hvac_modes_attr_hvac_modes.extend(mode_list)
207  self._attr_hvac_modes_attr_hvac_modes.append(HVACMode.OFF)
208 
209  if len(self.hvac_modeshvac_modes) > 1:
210  self._attr_supported_features |= (
211  ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
212  )
213 
214  async def async_added_to_hass(self) -> None:
215  """Subscribe to event updates."""
216  _LOGGER.debug("Added climate device with state: %s", repr(self._ih_device_ih_device))
217  await self._controller_controller.add_update_callback(self.async_update_callbackasync_update_callback)
218  try:
219  await self._controller_controller.connect()
220  except IHConnectionError as ex:
221  _LOGGER.error("Exception connecting to IntesisHome: %s", ex)
222  raise PlatformNotReady from ex
223 
224  @property
225  def name(self):
226  """Return the name of the AC device."""
227  return self._device_name_device_name
228 
229  @property
231  """Return the device specific state attributes."""
232  attrs = {}
233  if self._outdoor_temp_outdoor_temp:
234  attrs["outdoor_temp"] = self._outdoor_temp_outdoor_temp
235  if self._power_consumption_heat_power_consumption_heat:
236  attrs["power_consumption_heat_kw"] = round(
237  self._power_consumption_heat_power_consumption_heat / 1000, 1
238  )
239  if self._power_consumption_cool_power_consumption_cool:
240  attrs["power_consumption_cool_kw"] = round(
241  self._power_consumption_cool_power_consumption_cool / 1000, 1
242  )
243 
244  return attrs
245 
246  @property
247  def unique_id(self):
248  """Return unique ID for this device."""
249  return self._device_id_device_id
250 
251  @property
252  def target_temperature_step(self) -> float:
253  """Return whether setpoint should be whole or half degree precision."""
254  return self._setpoint_step_setpoint_step
255 
256  @property
257  def preset_modes(self):
258  """Return a list of HVAC preset modes."""
259  return self._preset_list_preset_list
260 
261  @property
262  def preset_mode(self):
263  """Return the current preset mode."""
264  return self._preset_preset
265 
266  async def async_set_temperature(self, **kwargs: Any) -> None:
267  """Set new target temperature."""
268  if hvac_mode := kwargs.get(ATTR_HVAC_MODE):
269  await self.async_set_hvac_modeasync_set_hvac_modeasync_set_hvac_mode(hvac_mode)
270 
271  if temperature := kwargs.get(ATTR_TEMPERATURE):
272  _LOGGER.debug("Setting %s to %s degrees", self._device_type_device_type, temperature)
273  await self._controller_controller.set_temperature(self._device_id_device_id, temperature)
274  self._target_temp_target_temp = temperature
275 
276  # Write updated temperature to HA state to avoid flapping (API confirmation is slow)
277  self.async_write_ha_stateasync_write_ha_state()
278 
279  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
280  """Set operation mode."""
281  _LOGGER.debug("Setting %s to %s mode", self._device_type_device_type, hvac_mode)
282  if hvac_mode == HVACMode.OFF:
283  self._power_power = False
284  await self._controller_controller.set_power_off(self._device_id_device_id)
285  # Write changes to HA, API can be slow to push changes
286  self.async_write_ha_stateasync_write_ha_state()
287  return
288 
289  # First check device is turned on
290  if not self._controller_controller.is_on(self._device_id_device_id):
291  self._power_power = True
292  await self._controller_controller.set_power_on(self._device_id_device_id)
293 
294  # Set the mode
295  await self._controller_controller.set_mode(self._device_id_device_id, MAP_HVAC_MODE_TO_IH[hvac_mode])
296 
297  # Send the temperature again in case changing modes has changed it
298  if self._target_temp_target_temp:
299  await self._controller_controller.set_temperature(self._device_id_device_id, self._target_temp_target_temp)
300 
301  # Updates can take longer than 2 seconds, so update locally
302  self._hvac_mode_hvac_mode = hvac_mode
303  self.async_write_ha_stateasync_write_ha_state()
304 
305  async def async_set_fan_mode(self, fan_mode: str) -> None:
306  """Set fan mode (from quiet, low, medium, high, auto)."""
307  await self._controller_controller.set_fan_speed(self._device_id_device_id, fan_mode)
308 
309  # Updates can take longer than 2 seconds, so update locally
310  self._fan_speed_fan_speed = fan_mode
311  self.async_write_ha_stateasync_write_ha_state()
312 
313  async def async_set_preset_mode(self, preset_mode: str) -> None:
314  """Set preset mode."""
315  ih_preset_mode = MAP_PRESET_MODE_TO_IH.get(preset_mode)
316  await self._controller_controller.set_preset_mode(self._device_id_device_id, ih_preset_mode)
317 
318  async def async_set_swing_mode(self, swing_mode: str) -> None:
319  """Set the vertical vane."""
320  if swing_settings := MAP_SWING_TO_IH.get(swing_mode):
321  await self._controller_controller.set_vertical_vane(
322  self._device_id_device_id, swing_settings.vvane
323  )
324  await self._controller_controller.set_horizontal_vane(
325  self._device_id_device_id, swing_settings.hvane
326  )
327 
328  async def async_update(self) -> None:
329  """Copy values from controller dictionary to climate device."""
330  # Update values from controller's device dictionary
331  self._connected_connected = self._controller_controller.is_connected
332  self._current_temp_current_temp = self._controller_controller.get_temperature(self._device_id_device_id)
333  self._fan_speed_fan_speed = self._controller_controller.get_fan_speed(self._device_id_device_id)
334  self._power_power = self._controller_controller.is_on(self._device_id_device_id)
335  self._min_temp_min_temp = self._controller_controller.get_min_setpoint(self._device_id_device_id)
336  self._max_temp_max_temp = self._controller_controller.get_max_setpoint(self._device_id_device_id)
337  self._rssi_rssi = self._controller_controller.get_rssi(self._device_id_device_id)
338  self._run_hours_run_hours = self._controller_controller.get_run_hours(self._device_id_device_id)
339  self._target_temp_target_temp = self._controller_controller.get_setpoint(self._device_id_device_id)
340  self._outdoor_temp_outdoor_temp = self._controller_controller.get_outdoor_temperature(self._device_id_device_id)
341 
342  # Operation mode
343  mode = self._controller_controller.get_mode(self._device_id_device_id)
344  self._hvac_mode_hvac_mode = MAP_IH_TO_HVAC_MODE.get(mode)
345 
346  # Preset mode
347  preset = self._controller_controller.get_preset_mode(self._device_id_device_id)
348  self._preset_preset = MAP_IH_TO_PRESET_MODE.get(preset)
349 
350  # Swing mode
351  # Climate module only supports one swing setting.
352  self._vvane_vvane = self._controller_controller.get_vertical_swing(self._device_id_device_id)
353  self._hvane_hvane = self._controller_controller.get_horizontal_swing(self._device_id_device_id)
354 
355  # Power usage
356  self._power_consumption_heat_power_consumption_heat = self._controller_controller.get_heat_power_consumption(
357  self._device_id_device_id
358  )
359  self._power_consumption_cool_power_consumption_cool = self._controller_controller.get_cool_power_consumption(
360  self._device_id_device_id
361  )
362 
363  async def async_will_remove_from_hass(self) -> None:
364  """Shutdown the controller when the device is being removed."""
365  await self._controller_controller.stop()
366 
367  @property
368  def icon(self):
369  """Return the icon for the current state."""
370  icon = None
371  if self._power_power:
372  icon = MAP_STATE_ICONS.get(self._hvac_mode_hvac_mode)
373  return icon
374 
375  async def async_update_callback(self, device_id=None):
376  """Let HA know there has been an update from the controller."""
377  # Track changes in connection state
378  if not self._controller_controller.is_connected and self._connected_connected:
379  # Connection has dropped
380  self._connected_connected = False
381  reconnect_minutes = 1 + randrange(10)
382  _LOGGER.error(
383  "Connection to %s API was lost. Reconnecting in %i minutes",
384  self._device_type_device_type,
385  reconnect_minutes,
386  )
387  # Schedule reconnection
388 
389  async def try_connect(_now):
390  await self._controller_controller.connect()
391 
392  async_call_later(self.hasshass, reconnect_minutes * 60, try_connect)
393 
394  if self._controller_controller.is_connected and not self._connected_connected:
395  # Connection has been restored
396  self._connected_connected = True
397  _LOGGER.debug("Connection to %s API was restored", self._device_type_device_type)
398 
399  if not device_id or self._device_id_device_id == device_id:
400  # Update all devices if no device_id was specified
401  _LOGGER.debug(
402  "%s API sent a status update for device %s",
403  self._device_type_device_type,
404  device_id,
405  )
406  self.async_schedule_update_ha_stateasync_schedule_update_ha_state(True)
407 
408  @property
409  def min_temp(self):
410  """Return the minimum temperature for the current mode of operation."""
411  return self._min_temp_min_temp
412 
413  @property
414  def max_temp(self):
415  """Return the maximum temperature for the current mode of operation."""
416  return self._max_temp_max_temp
417 
418  @property
419  def fan_mode(self):
420  """Return whether the fan is on."""
421  return self._fan_speed_fan_speed
422 
423  @property
424  def swing_mode(self):
425  """Return current swing mode."""
426  if self._vvane_vvane == IH_SWING_SWING and self._hvane_hvane == IH_SWING_SWING:
427  swing = SWING_BOTH
428  elif self._vvane_vvane == IH_SWING_SWING:
429  swing = SWING_VERTICAL
430  elif self._hvane_hvane == IH_SWING_SWING:
431  swing = SWING_HORIZONTAL
432  else:
433  swing = SWING_OFF
434  return swing
435 
436  @property
437  def fan_modes(self):
438  """List of available fan modes."""
439  return self._fan_modes_fan_modes
440 
441  @property
442  def swing_modes(self):
443  """List of available swing positions."""
444  return self._swing_list_swing_list
445 
446  @property
447  def available(self) -> bool:
448  """If the device hasn't been able to connect, mark as unavailable."""
449  return self._connected_connected or self._connected_connected is None
450 
451  @property
453  """Return the current temperature."""
454  return self._current_temp_current_temp
455 
456  @property
457  def hvac_mode(self) -> HVACMode:
458  """Return the current mode of operation if unit is on."""
459  if self._power_power:
460  return self._hvac_mode_hvac_mode
461  return HVACMode.OFF
462 
463  @property
465  """Return the current setpoint temperature if unit is on."""
466  return self._target_temp_target_temp
None set_temperature(self, **Any kwargs)
Definition: __init__.py:771
None async_set_hvac_mode(self, HVACMode hvac_mode)
Definition: __init__.py:813
None set_preset_mode(self, str preset_mode)
Definition: __init__.py:857
None async_set_hvac_mode(self, HVACMode hvac_mode)
Definition: climate.py:279
None async_set_preset_mode(self, str preset_mode)
Definition: climate.py:313
def __init__(self, ih_device_id, ih_device, controller)
Definition: climate.py:152
None async_schedule_update_ha_state(self, bool force_refresh=False)
Definition: entity.py:1265
bool is_on(HomeAssistant hass, str entity_id)
Definition: __init__.py:155
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: climate.py:106
float|None get_temperature(MadVRCoordinator coordinator, str key)
Definition: sensor.py:57
bool try_connect(HomeAssistant hass, ConfGatewayType gateway_type, dict[str, Any] user_input)
Definition: gateway.py:82
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597