Home Assistant Unofficial Reference 2024.12.1
climate.py
Go to the documentation of this file.
1 """Viessmann ViCare climate device."""
2 
3 from __future__ import annotations
4 
5 from contextlib import suppress
6 import logging
7 from typing import Any
8 
9 from PyViCare.PyViCareDevice import Device as PyViCareDevice
10 from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig
11 from PyViCare.PyViCareHeatingDevice import HeatingCircuit as PyViCareHeatingCircuit
12 from PyViCare.PyViCareUtils import (
13  PyViCareCommandError,
14  PyViCareInvalidDataError,
15  PyViCareNotSupportedFeatureError,
16  PyViCareRateLimitError,
17 )
18 import requests
19 import voluptuous as vol
20 
22  ClimateEntity,
23  ClimateEntityFeature,
24  HVACAction,
25  HVACMode,
26 )
27 from homeassistant.config_entries import ConfigEntry
28 from homeassistant.const import (
29  ATTR_TEMPERATURE,
30  PRECISION_TENTHS,
31  PRECISION_WHOLE,
32  UnitOfTemperature,
33 )
34 from homeassistant.core import HomeAssistant
35 from homeassistant.exceptions import ServiceValidationError
36 from homeassistant.helpers import entity_platform
38 from homeassistant.helpers.entity_platform import AddEntitiesCallback
39 
40 from .const import DEVICE_LIST, DOMAIN
41 from .entity import ViCareEntity
42 from .types import HeatingProgram, ViCareDevice
43 from .utils import get_burners, get_circuits, get_compressors, get_device_serial
44 
45 _LOGGER = logging.getLogger(__name__)
46 
47 SERVICE_SET_VICARE_MODE = "set_vicare_mode"
48 SERVICE_SET_VICARE_MODE_ATTR_MODE = "vicare_mode"
49 
50 VICARE_MODE_DHW = "dhw"
51 VICARE_MODE_HEATING = "heating"
52 VICARE_MODE_HEATINGCOOLING = "heatingCooling"
53 VICARE_MODE_DHWANDHEATING = "dhwAndHeating"
54 VICARE_MODE_DHWANDHEATINGCOOLING = "dhwAndHeatingCooling"
55 VICARE_MODE_FORCEDREDUCED = "forcedReduced"
56 VICARE_MODE_FORCEDNORMAL = "forcedNormal"
57 VICARE_MODE_OFF = "standby"
58 
59 VICARE_HOLD_MODE_AWAY = "away"
60 VICARE_HOLD_MODE_HOME = "home"
61 VICARE_HOLD_MODE_OFF = "off"
62 
63 VICARE_TEMP_HEATING_MIN = 3
64 VICARE_TEMP_HEATING_MAX = 37
65 
66 VICARE_TO_HA_HVAC_HEATING: dict[str, HVACMode] = {
67  VICARE_MODE_FORCEDREDUCED: HVACMode.OFF,
68  VICARE_MODE_OFF: HVACMode.OFF,
69  VICARE_MODE_DHW: HVACMode.OFF,
70  VICARE_MODE_DHWANDHEATINGCOOLING: HVACMode.AUTO,
71  VICARE_MODE_DHWANDHEATING: HVACMode.AUTO,
72  VICARE_MODE_HEATINGCOOLING: HVACMode.AUTO,
73  VICARE_MODE_HEATING: HVACMode.AUTO,
74  VICARE_MODE_FORCEDNORMAL: HVACMode.HEAT,
75 }
76 
77 CHANGABLE_HEATING_PROGRAMS = [
78  HeatingProgram.COMFORT,
79  HeatingProgram.COMFORT_HEATING,
80  HeatingProgram.ECO,
81 ]
82 
83 
85  device_list: list[ViCareDevice],
86 ) -> list[ViCareClimate]:
87  """Create ViCare climate entities for a device."""
88  return [
90  get_device_serial(device.api),
91  device.config,
92  device.api,
93  circuit,
94  )
95  for device in device_list
96  for circuit in get_circuits(device.api)
97  ]
98 
99 
101  hass: HomeAssistant,
102  config_entry: ConfigEntry,
103  async_add_entities: AddEntitiesCallback,
104 ) -> None:
105  """Set up the ViCare climate platform."""
106 
107  platform = entity_platform.async_get_current_platform()
108 
109  platform.async_register_entity_service(
110  SERVICE_SET_VICARE_MODE,
111  {vol.Required(SERVICE_SET_VICARE_MODE_ATTR_MODE): cv.string},
112  "set_vicare_mode",
113  )
114 
115  device_list = hass.data[DOMAIN][config_entry.entry_id][DEVICE_LIST]
116 
118  await hass.async_add_executor_job(
119  _build_entities,
120  device_list,
121  )
122  )
123 
124 
126  """Representation of the ViCare heating climate device."""
127 
128  _attr_precision = PRECISION_TENTHS
129  _attr_supported_features = (
130  ClimateEntityFeature.TARGET_TEMPERATURE
131  | ClimateEntityFeature.PRESET_MODE
132  | ClimateEntityFeature.TURN_OFF
133  | ClimateEntityFeature.TURN_ON
134  )
135  _attr_temperature_unit = UnitOfTemperature.CELSIUS
136  _attr_min_temp = VICARE_TEMP_HEATING_MIN
137  _attr_max_temp = VICARE_TEMP_HEATING_MAX
138  _attr_target_temperature_step = PRECISION_WHOLE
139  _attr_translation_key = "heating"
140  _current_action: bool | None = None
141  _current_mode: str | None = None
142  _current_program: str | None = None
143  _enable_turn_on_off_backwards_compatibility = False
144 
145  def __init__(
146  self,
147  device_serial: str | None,
148  device_config: PyViCareDeviceConfig,
149  device: PyViCareDevice,
150  circuit: PyViCareHeatingCircuit,
151  ) -> None:
152  """Initialize the climate device."""
153  super().__init__(
154  self._attr_translation_key_attr_translation_key, device_serial, device_config, device, circuit
155  )
156  self._device_device = device
157  self._attributes: dict[str, Any] = {}
158  self._attributes["vicare_programs"] = self._api.getPrograms()
159  self._attr_preset_modes_attr_preset_modes = [
160  preset
161  for heating_program in self._attributes["vicare_programs"]
162  if (preset := HeatingProgram.to_ha_preset(heating_program)) is not None
163  ]
164 
165  def update(self) -> None:
166  """Let HA know there has been an update from the ViCare API."""
167  try:
168  _room_temperature = None
169  with suppress(PyViCareNotSupportedFeatureError):
170  self._attributes["room_temperature"] = _room_temperature = (
171  self._api.getRoomTemperature()
172  )
173 
174  _supply_temperature = None
175  with suppress(PyViCareNotSupportedFeatureError):
176  _supply_temperature = self._api.getSupplyTemperature()
177 
178  if _room_temperature is not None:
179  self._attr_current_temperature_attr_current_temperature = _room_temperature
180  elif _supply_temperature is not None:
181  self._attr_current_temperature_attr_current_temperature = _supply_temperature
182  else:
183  self._attr_current_temperature_attr_current_temperature = None
184 
185  with suppress(PyViCareNotSupportedFeatureError):
186  self._attributes["active_vicare_program"] = self._current_program_current_program = (
187  self._api.getActiveProgram()
188  )
189 
190  with suppress(PyViCareNotSupportedFeatureError):
191  self._attr_target_temperature_attr_target_temperature = self._api.getCurrentDesiredTemperature()
192 
193  with suppress(PyViCareNotSupportedFeatureError):
194  self._attributes["active_vicare_mode"] = self._current_mode_current_mode = (
195  self._api.getActiveMode()
196  )
197 
198  with suppress(PyViCareNotSupportedFeatureError):
199  self._attributes["heating_curve_slope"] = (
200  self._api.getHeatingCurveSlope()
201  )
202 
203  with suppress(PyViCareNotSupportedFeatureError):
204  self._attributes["heating_curve_shift"] = (
205  self._api.getHeatingCurveShift()
206  )
207 
208  with suppress(PyViCareNotSupportedFeatureError):
209  self._attributes["vicare_modes"] = self._api.getModes()
210 
211  self._current_action_current_action = False
212  # Update the specific device attributes
213  with suppress(PyViCareNotSupportedFeatureError):
214  for burner in get_burners(self._device_device):
215  self._current_action_current_action = self._current_action_current_action or burner.getActive()
216 
217  with suppress(PyViCareNotSupportedFeatureError):
218  for compressor in get_compressors(self._device_device):
219  self._current_action_current_action = (
220  self._current_action_current_action or compressor.getActive()
221  )
222 
223  except requests.exceptions.ConnectionError:
224  _LOGGER.error("Unable to retrieve data from ViCare server")
225  except PyViCareRateLimitError as limit_exception:
226  _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception)
227  except ValueError:
228  _LOGGER.error("Unable to decode data from ViCare server")
229  except PyViCareInvalidDataError as invalid_data_exception:
230  _LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception)
231 
232  @property
233  def hvac_mode(self) -> HVACMode | None:
234  """Return current hvac mode."""
235  if self._current_mode_current_mode is None:
236  return None
237  return VICARE_TO_HA_HVAC_HEATING.get(self._current_mode_current_mode, None)
238 
239  def set_hvac_mode(self, hvac_mode: HVACMode) -> None:
240  """Set a new hvac mode on the ViCare API."""
241  if "vicare_modes" not in self._attributes:
242  raise ValueError("Cannot set hvac mode when vicare_modes are not known")
243 
244  vicare_mode = self.vicare_mode_from_hvac_modevicare_mode_from_hvac_mode(hvac_mode)
245  if vicare_mode is None:
246  raise ValueError(f"Cannot set invalid hvac mode: {hvac_mode}")
247 
248  _LOGGER.debug("Setting hvac mode to %s / %s", hvac_mode, vicare_mode)
249  self._api.setMode(vicare_mode)
250 
251  def vicare_mode_from_hvac_mode(self, hvac_mode) -> str | None:
252  """Return the corresponding vicare mode for an hvac_mode."""
253  if "vicare_modes" not in self._attributes:
254  return None
255 
256  supported_modes = self._attributes["vicare_modes"]
257  for key, value in VICARE_TO_HA_HVAC_HEATING.items():
258  if key in supported_modes and value == hvac_mode:
259  return key
260  return None
261 
262  @property
263  def hvac_modes(self) -> list[HVACMode]:
264  """Return the list of available hvac modes."""
265  if "vicare_modes" not in self._attributes:
266  return []
267 
268  supported_modes = self._attributes["vicare_modes"]
269  hvac_modes = []
270  for key, value in VICARE_TO_HA_HVAC_HEATING.items():
271  if value in hvac_modes:
272  continue
273  if key in supported_modes:
274  hvac_modes.append(value)
275  return hvac_modes
276 
277  @property
278  def hvac_action(self) -> HVACAction:
279  """Return the current hvac action."""
280  if self._current_action_current_action:
281  return HVACAction.HEATING
282  return HVACAction.IDLE
283 
284  def set_temperature(self, **kwargs: Any) -> None:
285  """Set new target temperatures."""
286  if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None:
287  self._api.setProgramTemperature(self._current_program_current_program, temp)
288  self._attr_target_temperature_attr_target_temperature = temp
289 
290  @property
291  def preset_mode(self):
292  """Return the current preset mode, e.g., home, away, temp."""
293  return HeatingProgram.to_ha_preset(self._current_program_current_program)
294 
295  def set_preset_mode(self, preset_mode: str) -> None:
296  """Set new preset mode and deactivate any existing programs."""
297  target_program = HeatingProgram.from_ha_preset(
298  preset_mode, self._attributes["vicare_programs"]
299  )
300  if target_program is None:
302  translation_domain=DOMAIN,
303  translation_key="program_unknown",
304  translation_placeholders={
305  "preset": preset_mode,
306  },
307  )
308 
309  _LOGGER.debug("Current preset %s", self._current_program_current_program)
310  if (
311  self._current_program_current_program
312  and self._current_program_current_program in CHANGABLE_HEATING_PROGRAMS
313  ):
314  _LOGGER.debug("deactivating %s", self._current_program_current_program)
315  try:
316  self._api.deactivateProgram(self._current_program_current_program)
317  except PyViCareCommandError as err:
319  translation_domain=DOMAIN,
320  translation_key="program_not_deactivated",
321  translation_placeholders={
322  "program": self._current_program_current_program,
323  },
324  ) from err
325 
326  _LOGGER.debug("Setting preset to %s / %s", preset_mode, target_program)
327  if target_program in CHANGABLE_HEATING_PROGRAMS:
328  _LOGGER.debug("activating %s", target_program)
329  try:
330  self._api.activateProgram(target_program)
331  except PyViCareCommandError as err:
333  translation_domain=DOMAIN,
334  translation_key="program_not_activated",
335  translation_placeholders={
336  "program": target_program,
337  },
338  ) from err
339 
340  @property
341  def extra_state_attributes(self) -> dict[str, Any]:
342  """Show Device Attributes."""
343  return self._attributes
344 
345  def set_vicare_mode(self, vicare_mode) -> None:
346  """Service function to set vicare modes directly."""
347  if vicare_mode not in self._attributes["vicare_modes"]:
348  raise ValueError(f"Cannot set invalid vicare mode: {vicare_mode}.")
349 
350  self._api.setMode(vicare_mode)
None __init__(self, str|None device_serial, PyViCareDeviceConfig device_config, PyViCareDevice device, PyViCareHeatingCircuit circuit)
Definition: climate.py:151
None set_hvac_mode(self, HVACMode hvac_mode)
Definition: climate.py:239
str|None vicare_mode_from_hvac_mode(self, hvac_mode)
Definition: climate.py:251
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: climate.py:104
list[ViCareClimate] _build_entities(list[ViCareDevice] device_list)
Definition: climate.py:86
str|None get_device_serial(PyViCareDevice device)
Definition: utils.py:35
list[PyViCareHeatingDeviceComponent] get_compressors(PyViCareDevice device)
Definition: utils.py:92
list[PyViCareHeatingDeviceComponent] get_circuits(PyViCareDevice device)
Definition: utils.py:81
list[PyViCareHeatingDeviceComponent] get_burners(PyViCareDevice device)
Definition: utils.py:70