Home Assistant Unofficial Reference 2024.12.1
climate.py
Go to the documentation of this file.
1 """Support for Netatmo Smart thermostats."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any, cast
7 
8 from pyatmo.modules import NATherm1
9 from pyatmo.modules.device_types import DeviceType
10 import voluptuous as vol
11 
13  ATTR_PRESET_MODE,
14  DEFAULT_MIN_TEMP,
15  PRESET_AWAY,
16  PRESET_BOOST,
17  PRESET_HOME,
18  ClimateEntity,
19  ClimateEntityFeature,
20  HVACAction,
21  HVACMode,
22 )
23 from homeassistant.config_entries import ConfigEntry
24 from homeassistant.const import (
25  ATTR_TEMPERATURE,
26  PRECISION_HALVES,
27  STATE_OFF,
28  UnitOfTemperature,
29 )
30 from homeassistant.core import HomeAssistant, callback
31 from homeassistant.helpers import config_validation as cv, entity_platform
32 from homeassistant.helpers.dispatcher import async_dispatcher_connect
33 from homeassistant.helpers.entity_platform import AddEntitiesCallback
34 from homeassistant.util import dt as dt_util
35 
36 from .const import (
37  ATTR_END_DATETIME,
38  ATTR_HEATING_POWER_REQUEST,
39  ATTR_SCHEDULE_NAME,
40  ATTR_SELECTED_SCHEDULE,
41  ATTR_TARGET_TEMPERATURE,
42  ATTR_TIME_PERIOD,
43  DATA_SCHEDULES,
44  DOMAIN,
45  EVENT_TYPE_CANCEL_SET_POINT,
46  EVENT_TYPE_SCHEDULE,
47  EVENT_TYPE_SET_POINT,
48  EVENT_TYPE_THERM_MODE,
49  NETATMO_CREATE_CLIMATE,
50  SERVICE_CLEAR_TEMPERATURE_SETTING,
51  SERVICE_SET_PRESET_MODE_WITH_END_DATETIME,
52  SERVICE_SET_SCHEDULE,
53  SERVICE_SET_TEMPERATURE_WITH_END_DATETIME,
54  SERVICE_SET_TEMPERATURE_WITH_TIME_PERIOD,
55 )
56 from .data_handler import HOME, SIGNAL_NAME, NetatmoRoom
57 from .entity import NetatmoRoomEntity
58 
59 _LOGGER = logging.getLogger(__name__)
60 
61 PRESET_FROST_GUARD = "frost_guard"
62 PRESET_SCHEDULE = "schedule"
63 PRESET_MANUAL = "manual"
64 
65 SUPPORT_FLAGS = (
66  ClimateEntityFeature.TARGET_TEMPERATURE
67  | ClimateEntityFeature.PRESET_MODE
68  | ClimateEntityFeature.TURN_OFF
69  | ClimateEntityFeature.TURN_ON
70 )
71 SUPPORT_PRESET = [PRESET_AWAY, PRESET_BOOST, PRESET_FROST_GUARD, PRESET_SCHEDULE]
72 
73 THERM_MODES = (PRESET_SCHEDULE, PRESET_FROST_GUARD, PRESET_AWAY)
74 
75 STATE_NETATMO_SCHEDULE = "schedule"
76 STATE_NETATMO_HG = "hg"
77 STATE_NETATMO_MAX = "max"
78 STATE_NETATMO_AWAY = PRESET_AWAY
79 STATE_NETATMO_OFF = STATE_OFF
80 STATE_NETATMO_MANUAL = "manual"
81 STATE_NETATMO_HOME = "home"
82 
83 PRESET_MAP_NETATMO = {
84  PRESET_FROST_GUARD: STATE_NETATMO_HG,
85  PRESET_BOOST: STATE_NETATMO_MAX,
86  PRESET_SCHEDULE: STATE_NETATMO_SCHEDULE,
87  PRESET_AWAY: STATE_NETATMO_AWAY,
88  STATE_NETATMO_OFF: STATE_NETATMO_OFF,
89 }
90 
91 NETATMO_MAP_PRESET = {
92  STATE_NETATMO_HG: PRESET_FROST_GUARD,
93  STATE_NETATMO_MAX: PRESET_BOOST,
94  STATE_NETATMO_SCHEDULE: PRESET_SCHEDULE,
95  STATE_NETATMO_AWAY: PRESET_AWAY,
96  STATE_NETATMO_OFF: STATE_NETATMO_OFF,
97  STATE_NETATMO_MANUAL: STATE_NETATMO_MANUAL,
98  STATE_NETATMO_HOME: PRESET_SCHEDULE,
99 }
100 
101 HVAC_MAP_NETATMO = {
102  PRESET_SCHEDULE: HVACMode.AUTO,
103  STATE_NETATMO_HG: HVACMode.AUTO,
104  PRESET_FROST_GUARD: HVACMode.AUTO,
105  PRESET_BOOST: HVACMode.HEAT,
106  STATE_NETATMO_OFF: HVACMode.OFF,
107  STATE_NETATMO_MANUAL: HVACMode.AUTO,
108  PRESET_MANUAL: HVACMode.AUTO,
109  STATE_NETATMO_AWAY: HVACMode.AUTO,
110 }
111 
112 CURRENT_HVAC_MAP_NETATMO = {True: HVACAction.HEATING, False: HVACAction.IDLE}
113 
114 DEFAULT_MAX_TEMP = 30
115 
116 NA_THERM = DeviceType.NATherm1
117 NA_VALVE = DeviceType.NRV
118 
119 
121  hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
122 ) -> None:
123  """Set up the Netatmo energy platform."""
124 
125  @callback
126  def _create_entity(netatmo_device: NetatmoRoom) -> None:
127  if not netatmo_device.room.climate_type:
128  msg = f"No climate type found for this room: {netatmo_device.room.name}"
129  _LOGGER.debug(msg)
130  return
131  entity = NetatmoThermostat(netatmo_device)
132  async_add_entities([entity])
133 
134  entry.async_on_unload(
135  async_dispatcher_connect(hass, NETATMO_CREATE_CLIMATE, _create_entity)
136  )
137 
138  platform = entity_platform.async_get_current_platform()
139  platform.async_register_entity_service(
140  SERVICE_SET_SCHEDULE,
141  {vol.Required(ATTR_SCHEDULE_NAME): cv.string},
142  "_async_service_set_schedule",
143  )
144  platform.async_register_entity_service(
145  SERVICE_SET_PRESET_MODE_WITH_END_DATETIME,
146  {
147  vol.Required(ATTR_PRESET_MODE): vol.In(THERM_MODES),
148  vol.Required(ATTR_END_DATETIME): cv.datetime,
149  },
150  "_async_service_set_preset_mode_with_end_datetime",
151  )
152  platform.async_register_entity_service(
153  SERVICE_SET_TEMPERATURE_WITH_END_DATETIME,
154  {
155  vol.Required(ATTR_TARGET_TEMPERATURE): vol.All(
156  vol.Coerce(float), vol.Range(min=7, max=30)
157  ),
158  vol.Required(ATTR_END_DATETIME): cv.datetime,
159  },
160  "_async_service_set_temperature_with_end_datetime",
161  )
162  platform.async_register_entity_service(
163  SERVICE_SET_TEMPERATURE_WITH_TIME_PERIOD,
164  {
165  vol.Required(ATTR_TARGET_TEMPERATURE): vol.All(
166  vol.Coerce(float), vol.Range(min=7, max=30)
167  ),
168  vol.Required(ATTR_TIME_PERIOD): vol.All(
169  cv.time_period,
170  cv.positive_timedelta,
171  ),
172  },
173  "_async_service_set_temperature_with_time_period",
174  )
175  platform.async_register_entity_service(
176  SERVICE_CLEAR_TEMPERATURE_SETTING,
177  None,
178  "_async_service_clear_temperature_setting",
179  )
180 
181 
183  """Representation a Netatmo thermostat."""
184 
185  _attr_hvac_mode = HVACMode.AUTO
186  _attr_max_temp = DEFAULT_MAX_TEMP
187  _attr_preset_modes = SUPPORT_PRESET
188  _attr_supported_features = SUPPORT_FLAGS
189  _attr_target_temperature_step = PRECISION_HALVES
190  _attr_temperature_unit = UnitOfTemperature.CELSIUS
191  _attr_translation_key = "thermostat"
192  _attr_name = None
193  _away: bool | None = None
194  _connected: bool | None = None
195  _enable_turn_on_off_backwards_compatibility = False
196 
197  _away_temperature: float | None = None
198  _hg_temperature: float | None = None
199  _boilerstatus: bool | None = None
200 
201  def __init__(self, room: NetatmoRoom) -> None:
202  """Initialize the sensor."""
203  super().__init__(room)
204 
205  self._signal_name_signal_name = f"{HOME}-{self.home.entity_id}"
206  self._publishers.extend(
207  [
208  {
209  "name": HOME,
210  "home_id": self.homehome.entity_id,
211  SIGNAL_NAME: self._signal_name_signal_name,
212  },
213  ]
214  )
215 
216  self._selected_schedule_selected_schedule = None
217 
218  self._attr_hvac_modes_attr_hvac_modes = [HVACMode.AUTO, HVACMode.HEAT]
219  if self.device_typedevice_typedevice_typedevice_type is NA_THERM:
220  self._attr_hvac_modes_attr_hvac_modes.append(HVACMode.OFF)
221 
222  self._attr_unique_id_attr_unique_id = f"{self.device.entity_id}-{self.device_type}"
223 
224  async def async_added_to_hass(self) -> None:
225  """Entity created."""
226  await super().async_added_to_hass()
227 
228  for event_type in (
229  EVENT_TYPE_SET_POINT,
230  EVENT_TYPE_THERM_MODE,
231  EVENT_TYPE_CANCEL_SET_POINT,
232  EVENT_TYPE_SCHEDULE,
233  ):
234  self.async_on_removeasync_on_remove(
236  self.hasshass,
237  f"signal-{DOMAIN}-webhook-{event_type}",
238  self.handle_eventhandle_event,
239  )
240  )
241 
242  @callback
243  def handle_event(self, event: dict) -> None:
244  """Handle webhook events."""
245  data = event["data"]
246 
247  if self.homehome.entity_id != data["home_id"]:
248  return
249 
250  if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data:
251  self._selected_schedule_selected_schedule = getattr(
252  self.hasshass.data[DOMAIN][DATA_SCHEDULES][self.homehome.entity_id].get(
253  data["schedule_id"]
254  ),
255  "name",
256  None,
257  )
258  self._attr_extra_state_attributes_attr_extra_state_attributes[ATTR_SELECTED_SCHEDULE] = (
259  self._selected_schedule_selected_schedule
260  )
261  self.async_write_ha_stateasync_write_ha_state()
262  self.data_handlerdata_handler.async_force_update(self._signal_name_signal_name)
263  return
264 
265  home = data["home"]
266 
267  if self.homehome.entity_id != home["id"]:
268  return
269 
270  if data["event_type"] == EVENT_TYPE_THERM_MODE:
271  self._attr_preset_mode_attr_preset_mode = NETATMO_MAP_PRESET[home[EVENT_TYPE_THERM_MODE]]
272  self._attr_hvac_mode_attr_hvac_mode = HVAC_MAP_NETATMO[self._attr_preset_mode_attr_preset_mode]
273  if self._attr_preset_mode_attr_preset_mode == PRESET_FROST_GUARD:
274  self._attr_target_temperature_attr_target_temperature = self._hg_temperature_hg_temperature
275  elif self._attr_preset_mode_attr_preset_mode == PRESET_AWAY:
276  self._attr_target_temperature_attr_target_temperature = self._away_temperature_away_temperature
277  elif self._attr_preset_mode_attr_preset_mode in [PRESET_SCHEDULE, PRESET_HOME]:
278  self.async_update_callbackasync_update_callbackasync_update_callback()
279  self.data_handlerdata_handler.async_force_update(self._signal_name_signal_name)
280  self.async_write_ha_stateasync_write_ha_state()
281  return
282 
283  for room in home.get("rooms", []):
284  if (
285  data["event_type"] == EVENT_TYPE_SET_POINT
286  and self.devicedevice.entity_id == room["id"]
287  ):
288  if room["therm_setpoint_mode"] == STATE_NETATMO_OFF:
289  self._attr_hvac_mode_attr_hvac_mode = HVACMode.OFF
290  self._attr_preset_mode_attr_preset_mode = STATE_NETATMO_OFF
291  self._attr_target_temperature_attr_target_temperature = 0
292  elif room["therm_setpoint_mode"] == STATE_NETATMO_MAX:
293  self._attr_hvac_mode_attr_hvac_mode = HVACMode.HEAT
294  self._attr_preset_mode_attr_preset_mode = PRESET_MAP_NETATMO[PRESET_BOOST]
295  self._attr_target_temperature_attr_target_temperature = DEFAULT_MAX_TEMP
296  elif room["therm_setpoint_mode"] == STATE_NETATMO_MANUAL:
297  self._attr_hvac_mode_attr_hvac_mode = HVACMode.HEAT
298  self._attr_target_temperature_attr_target_temperature = room["therm_setpoint_temperature"]
299  else:
300  self._attr_target_temperature_attr_target_temperature = room["therm_setpoint_temperature"]
301  if self._attr_target_temperature_attr_target_temperature == DEFAULT_MAX_TEMP:
302  self._attr_hvac_mode_attr_hvac_mode = HVACMode.HEAT
303  self.async_write_ha_stateasync_write_ha_state()
304  return
305 
306  if (
307  data["event_type"] == EVENT_TYPE_CANCEL_SET_POINT
308  and self.devicedevice.entity_id == room["id"]
309  ):
310  if self._attr_hvac_mode_attr_hvac_mode == HVACMode.OFF:
311  self._attr_hvac_mode_attr_hvac_mode = HVACMode.AUTO
312  self._attr_preset_mode_attr_preset_mode = PRESET_MAP_NETATMO[PRESET_SCHEDULE]
313 
314  self.async_update_callbackasync_update_callbackasync_update_callback()
315  self.async_write_ha_stateasync_write_ha_state()
316  return
317 
318  @property
319  def hvac_action(self) -> HVACAction:
320  """Return the current running hvac operation if supported."""
321  if self.device_typedevice_typedevice_typedevice_type != NA_VALVE and self._boilerstatus_boilerstatus is not None:
322  return CURRENT_HVAC_MAP_NETATMO[self._boilerstatus_boilerstatus]
323  # Maybe it is a valve
324  if (
325  heating_req := getattr(self.devicedevice, "heating_power_request", 0)
326  ) is not None and heating_req > 0:
327  return HVACAction.HEATING
328  return HVACAction.IDLE
329 
330  async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
331  """Set new target hvac mode."""
332  if hvac_mode == HVACMode.OFF:
333  await self.async_turn_offasync_turn_offasync_turn_off()
334  elif hvac_mode == HVACMode.AUTO:
335  await self.async_set_preset_modeasync_set_preset_modeasync_set_preset_mode(PRESET_SCHEDULE)
336  elif hvac_mode == HVACMode.HEAT:
337  await self.async_set_preset_modeasync_set_preset_modeasync_set_preset_mode(PRESET_BOOST)
338 
339  async def async_set_preset_mode(self, preset_mode: str) -> None:
340  """Set new preset mode."""
341  if (
342  preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX)
343  and self.device_typedevice_typedevice_typedevice_type == NA_VALVE
344  and self._attr_hvac_mode_attr_hvac_mode == HVACMode.HEAT
345  ):
346  await self.devicedevice.async_therm_set(
347  STATE_NETATMO_HOME,
348  )
349  elif (
350  preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX)
351  and self.device_typedevice_typedevice_typedevice_type == NA_VALVE
352  ):
353  await self.devicedevice.async_therm_set(
354  STATE_NETATMO_MANUAL,
355  DEFAULT_MAX_TEMP,
356  )
357  elif (
358  preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX)
359  and self._attr_hvac_mode_attr_hvac_mode == HVACMode.HEAT
360  ):
361  await self.devicedevice.async_therm_set(STATE_NETATMO_HOME)
362  elif preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX):
363  await self.devicedevice.async_therm_set(PRESET_MAP_NETATMO[preset_mode])
364  elif preset_mode in THERM_MODES:
365  await self.devicedevice.home.async_set_thermmode(PRESET_MAP_NETATMO[preset_mode])
366  else:
367  _LOGGER.error("Preset mode '%s' not available", preset_mode)
368 
369  self.async_write_ha_stateasync_write_ha_state()
370 
371  async def async_set_temperature(self, **kwargs: Any) -> None:
372  """Set new target temperature for 2 hours."""
373  await self.devicedevice.async_therm_set(
374  STATE_NETATMO_MANUAL, min(kwargs[ATTR_TEMPERATURE], DEFAULT_MAX_TEMP)
375  )
376  self.async_write_ha_stateasync_write_ha_state()
377 
378  async def async_turn_off(self) -> None:
379  """Turn the entity off."""
380  if self.device_typedevice_typedevice_typedevice_type == NA_VALVE:
381  await self.devicedevice.async_therm_set(
382  STATE_NETATMO_MANUAL,
383  DEFAULT_MIN_TEMP,
384  )
385  elif self._attr_hvac_mode_attr_hvac_mode != HVACMode.OFF:
386  await self.devicedevice.async_therm_set(STATE_NETATMO_OFF)
387  self.async_write_ha_stateasync_write_ha_state()
388 
389  async def async_turn_on(self) -> None:
390  """Turn the entity on."""
391  await self.devicedevice.async_therm_set(STATE_NETATMO_HOME)
392  self.async_write_ha_stateasync_write_ha_state()
393 
394  @property
395  def available(self) -> bool:
396  """If the device hasn't been able to connect, mark as unavailable."""
397  return bool(self._connected_connected)
398 
399  @callback
400  def async_update_callback(self) -> None:
401  """Update the entity's state."""
402  if not self.devicedevice.reachable:
403  if self.availableavailableavailable:
404  self._connected_connected = False
405  return
406 
407  self._connected_connected = True
408 
409  self._away_temperature_away_temperature = self.homehome.get_away_temp()
410  self._hg_temperature_hg_temperature = self.homehome.get_hg_temp()
411  self._attr_current_temperature_attr_current_temperature = self.devicedevice.therm_measured_temperature
412  self._attr_target_temperature_attr_target_temperature = self.devicedevice.therm_setpoint_temperature
413  self._attr_preset_mode_attr_preset_mode = NETATMO_MAP_PRESET[
414  getattr(self.devicedevice, "therm_setpoint_mode", STATE_NETATMO_SCHEDULE)
415  ]
416  self._attr_hvac_mode_attr_hvac_mode = HVAC_MAP_NETATMO[self._attr_preset_mode_attr_preset_mode]
417  self._away_away = self._attr_hvac_mode_attr_hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY]
418 
419  self._selected_schedule_selected_schedule = getattr(
420  self.homehome.get_selected_schedule(), "name", None
421  )
422  self._attr_extra_state_attributes_attr_extra_state_attributes[ATTR_SELECTED_SCHEDULE] = (
423  self._selected_schedule_selected_schedule
424  )
425 
426  if self.device_typedevice_typedevice_typedevice_type == NA_VALVE:
427  self._attr_extra_state_attributes_attr_extra_state_attributes[ATTR_HEATING_POWER_REQUEST] = (
428  self.devicedevice.heating_power_request
429  )
430  else:
431  for module in self.devicedevice.modules.values():
432  if hasattr(module, "boiler_status"):
433  module = cast(NATherm1, module)
434  if module.boiler_status is not None:
435  self._boilerstatus_boilerstatus = module.boiler_status
436  break
437 
438  async def _async_service_set_schedule(self, **kwargs: Any) -> None:
439  schedule_name = kwargs.get(ATTR_SCHEDULE_NAME)
440  schedule_id = None
441  for sid, schedule in self.hasshass.data[DOMAIN][DATA_SCHEDULES][
442  self.homehome.entity_id
443  ].items():
444  if schedule.name == schedule_name:
445  schedule_id = sid
446  break
447 
448  if not schedule_id:
449  _LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME))
450  return
451 
452  await self.homehome.async_switch_schedule(schedule_id=schedule_id)
453  _LOGGER.debug(
454  "Setting %s schedule to %s (%s)",
455  self.homehome.entity_id,
456  kwargs.get(ATTR_SCHEDULE_NAME),
457  schedule_id,
458  )
459 
461  self, **kwargs: Any
462  ) -> None:
463  preset_mode = kwargs[ATTR_PRESET_MODE]
464  end_datetime = kwargs[ATTR_END_DATETIME]
465  end_timestamp = int(dt_util.as_timestamp(end_datetime))
466 
467  await self.homehome.async_set_thermmode(
468  mode=PRESET_MAP_NETATMO[preset_mode], end_time=end_timestamp
469  )
470  _LOGGER.debug(
471  "Setting %s preset to %s with end datetime %s",
472  self.homehome.entity_id,
473  preset_mode,
474  end_timestamp,
475  )
476 
478  self, **kwargs: Any
479  ) -> None:
480  target_temperature = kwargs[ATTR_TARGET_TEMPERATURE]
481  end_datetime = kwargs[ATTR_END_DATETIME]
482  end_timestamp = int(dt_util.as_timestamp(end_datetime))
483 
484  _LOGGER.debug(
485  "Setting %s to target temperature %s with end datetime %s",
486  self.devicedevice.entity_id,
487  target_temperature,
488  end_timestamp,
489  )
490  await self.devicedevice.async_therm_manual(target_temperature, end_timestamp)
491 
493  self, **kwargs: Any
494  ) -> None:
495  target_temperature = kwargs[ATTR_TARGET_TEMPERATURE]
496  time_period = kwargs[ATTR_TIME_PERIOD]
497 
498  _LOGGER.debug(
499  "Setting %s to target temperature %s with time period %s",
500  self.devicedevice.entity_id,
501  target_temperature,
502  time_period,
503  )
504 
505  now_timestamp = dt_util.as_timestamp(dt_util.utcnow())
506  end_timestamp = int(now_timestamp + time_period.seconds)
507  await self.devicedevice.async_therm_manual(target_temperature, end_timestamp)
508 
509  async def _async_service_clear_temperature_setting(self, **kwargs: Any) -> None:
510  _LOGGER.debug("Clearing %s temperature setting", self.devicedevice.entity_id)
511  await self.devicedevice.async_therm_home()
None async_set_preset_mode(self, str preset_mode)
Definition: __init__.py:861
None _async_service_set_temperature_with_end_datetime(self, **Any kwargs)
Definition: climate.py:479
None async_set_hvac_mode(self, HVACMode hvac_mode)
Definition: climate.py:330
None _async_service_set_temperature_with_time_period(self, **Any kwargs)
Definition: climate.py:494
None _async_service_clear_temperature_setting(self, **Any kwargs)
Definition: climate.py:509
None _async_service_set_preset_mode_with_end_datetime(self, **Any kwargs)
Definition: climate.py:462
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: climate.py:122
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103