Home Assistant Unofficial Reference 2024.12.1
fan.py
Go to the documentation of this file.
1 """Viessmann ViCare ventilation device."""
2 
3 from __future__ import annotations
4 
5 from contextlib import suppress
6 import enum
7 import logging
8 
9 from PyViCare.PyViCareDevice import Device as PyViCareDevice
10 from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig
11 from PyViCare.PyViCareUtils import (
12  PyViCareInvalidDataError,
13  PyViCareNotSupportedFeatureError,
14  PyViCareRateLimitError,
15 )
16 from PyViCare.PyViCareVentilationDevice import (
17  VentilationDevice as PyViCareVentilationDevice,
18 )
19 from requests.exceptions import ConnectionError as RequestConnectionError
20 
21 from homeassistant.components.fan import FanEntity, FanEntityFeature
22 from homeassistant.config_entries import ConfigEntry
23 from homeassistant.core import HomeAssistant
24 from homeassistant.helpers.entity_platform import AddEntitiesCallback
26  ordered_list_item_to_percentage,
27  percentage_to_ordered_list_item,
28 )
29 
30 from .const import DEVICE_LIST, DOMAIN
31 from .entity import ViCareEntity
32 from .types import ViCareDevice
33 from .utils import get_device_serial
34 
35 _LOGGER = logging.getLogger(__name__)
36 
37 
38 class VentilationProgram(enum.StrEnum):
39  """ViCare preset ventilation programs.
40 
41  As listed in https://github.com/somm15/PyViCare/blob/6c5b023ca6c8bb2d38141dd1746dc1705ec84ce8/PyViCare/PyViCareVentilationDevice.py#L37
42  """
43 
44  LEVEL_ONE = "levelOne"
45  LEVEL_TWO = "levelTwo"
46  LEVEL_THREE = "levelThree"
47  LEVEL_FOUR = "levelFour"
48 
49 
50 class VentilationMode(enum.StrEnum):
51  """ViCare ventilation modes."""
52 
53  PERMANENT = "permanent" # on, speed controlled by program (levelOne-levelFour)
54  VENTILATION = "ventilation" # activated by schedule
55  SENSOR_DRIVEN = "sensor_driven" # activated by schedule, override by sensor
56  SENSOR_OVERRIDE = "sensor_override" # activated by sensor
57 
58  @staticmethod
59  def to_vicare_mode(mode: str | None) -> str | None:
60  """Return the mapped ViCare ventilation mode for the Home Assistant mode."""
61  if mode:
62  try:
63  ventilation_mode = VentilationMode(mode)
64  except ValueError:
65  # ignore unsupported / unmapped modes
66  return None
67  return HA_TO_VICARE_MODE_VENTILATION.get(ventilation_mode) if mode else None
68  return None
69 
70  @staticmethod
71  def from_vicare_mode(vicare_mode: str | None) -> str | None:
72  """Return the mapped Home Assistant mode for the ViCare ventilation mode."""
73  for mode in VentilationMode:
74  if HA_TO_VICARE_MODE_VENTILATION.get(VentilationMode(mode)) == vicare_mode:
75  return mode
76  return None
77 
78 
79 HA_TO_VICARE_MODE_VENTILATION = {
80  VentilationMode.PERMANENT: "permanent",
81  VentilationMode.VENTILATION: "ventilation",
82  VentilationMode.SENSOR_DRIVEN: "sensorDriven",
83  VentilationMode.SENSOR_OVERRIDE: "sensorOverride",
84 }
85 
86 ORDERED_NAMED_FAN_SPEEDS = [
87  VentilationProgram.LEVEL_ONE,
88  VentilationProgram.LEVEL_TWO,
89  VentilationProgram.LEVEL_THREE,
90  VentilationProgram.LEVEL_FOUR,
91 ]
92 
93 
95  device_list: list[ViCareDevice],
96 ) -> list[ViCareFan]:
97  """Create ViCare climate entities for a device."""
98  return [
99  ViCareFan(get_device_serial(device.api), device.config, device.api)
100  for device in device_list
101  if isinstance(device.api, PyViCareVentilationDevice)
102  ]
103 
104 
106  hass: HomeAssistant,
107  config_entry: ConfigEntry,
108  async_add_entities: AddEntitiesCallback,
109 ) -> None:
110  """Set up the ViCare fan platform."""
111 
112  device_list = hass.data[DOMAIN][config_entry.entry_id][DEVICE_LIST]
113 
115  await hass.async_add_executor_job(
116  _build_entities,
117  device_list,
118  )
119  )
120 
121 
123  """Representation of the ViCare ventilation device."""
124 
125  _attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS)
126  _attr_supported_features = FanEntityFeature.SET_SPEED
127  _attr_translation_key = "ventilation"
128  _enable_turn_on_off_backwards_compatibility = False
129 
130  def __init__(
131  self,
132  device_serial: str | None,
133  device_config: PyViCareDeviceConfig,
134  device: PyViCareDevice,
135  ) -> None:
136  """Initialize the fan entity."""
137  super().__init__(
138  self._attr_translation_key_attr_translation_key, device_serial, device_config, device
139  )
140  # init presets
141  supported_modes = list[str](self._api.getAvailableModes())
142  self._attr_preset_modes_attr_preset_modes = [
143  mode
144  for mode in VentilationMode
145  if VentilationMode.to_vicare_mode(mode) in supported_modes
146  ]
147  if len(self._attr_preset_modes_attr_preset_modes) > 0:
148  self._attr_supported_features_attr_supported_features |= FanEntityFeature.PRESET_MODE
149 
150  def update(self) -> None:
151  """Update state of fan."""
152  try:
153  with suppress(PyViCareNotSupportedFeatureError):
154  self._attr_preset_mode_attr_preset_mode = VentilationMode.from_vicare_mode(
155  self._api.getActiveMode()
156  )
157  with suppress(PyViCareNotSupportedFeatureError):
158  self._attr_percentage_attr_percentage = ordered_list_item_to_percentage(
159  ORDERED_NAMED_FAN_SPEEDS, self._api.getActiveProgram()
160  )
161  except RequestConnectionError:
162  _LOGGER.error("Unable to retrieve data from ViCare server")
163  except ValueError:
164  _LOGGER.error("Unable to decode data from ViCare server")
165  except PyViCareRateLimitError as limit_exception:
166  _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception)
167  except PyViCareInvalidDataError as invalid_data_exception:
168  _LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception)
169 
170  @property
171  def is_on(self) -> bool | None:
172  """Return true if the entity is on."""
173  # Viessmann ventilation unit cannot be turned off
174  return True
175 
176  @property
177  def icon(self) -> str | None:
178  """Return the icon to use in the frontend."""
179  if hasattr(self, "_attr_preset_mode"):
180  if self._attr_preset_mode_attr_preset_mode == VentilationMode.VENTILATION:
181  return "mdi:fan-clock"
182  if self._attr_preset_mode_attr_preset_mode in [
183  VentilationMode.SENSOR_DRIVEN,
184  VentilationMode.SENSOR_OVERRIDE,
185  ]:
186  return "mdi:fan-auto"
187  if self._attr_preset_mode_attr_preset_mode == VentilationMode.PERMANENT:
188  if self._attr_percentage_attr_percentage == 0:
189  return "mdi:fan-off"
190  if self._attr_percentage_attr_percentage is not None:
191  level = 1 + ORDERED_NAMED_FAN_SPEEDS.index(
192  percentage_to_ordered_list_item(
193  ORDERED_NAMED_FAN_SPEEDS, self._attr_percentage_attr_percentage
194  )
195  )
196  if level < 4: # fan-speed- only supports 1-3
197  return f"mdi:fan-speed-{level}"
198  return "mdi:fan"
199 
200  def set_percentage(self, percentage: int) -> None:
201  """Set the speed of the fan, as a percentage."""
202  if self._attr_preset_mode_attr_preset_mode != str(VentilationMode.PERMANENT):
203  self.set_preset_modeset_preset_modeset_preset_mode(VentilationMode.PERMANENT)
204 
205  level = percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage)
206  _LOGGER.debug("changing ventilation level to %s", level)
207  self._api.setPermanentLevel(level)
208 
209  def set_preset_mode(self, preset_mode: str) -> None:
210  """Set new preset mode."""
211  target_mode = VentilationMode.to_vicare_mode(preset_mode)
212  _LOGGER.debug("changing ventilation mode to %s", target_mode)
213  self._api.setActiveMode(target_mode)
None set_preset_mode(self, str preset_mode)
Definition: __init__.py:375
str|None to_vicare_mode(str|None mode)
Definition: fan.py:59
str|None from_vicare_mode(str|None vicare_mode)
Definition: fan.py:71
None set_percentage(self, int percentage)
Definition: fan.py:200
None set_preset_mode(self, str preset_mode)
Definition: fan.py:209
None __init__(self, str|None device_serial, PyViCareDeviceConfig device_config, PyViCareDevice device)
Definition: fan.py:135
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: fan.py:109
list[ViCareFan] _build_entities(list[ViCareDevice] device_list)
Definition: fan.py:96
str|None get_device_serial(PyViCareDevice device)
Definition: utils.py:35