213 lines
7.6 KiB
Python
213 lines
7.6 KiB
Python
"""Viessmann ViCare ventilation device."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from contextlib import suppress
|
|
import enum
|
|
import logging
|
|
|
|
from PyViCare.PyViCareDevice import Device as PyViCareDevice
|
|
from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig
|
|
from PyViCare.PyViCareUtils import (
|
|
PyViCareInvalidDataError,
|
|
PyViCareNotSupportedFeatureError,
|
|
PyViCareRateLimitError,
|
|
)
|
|
from PyViCare.PyViCareVentilationDevice import (
|
|
VentilationDevice as PyViCareVentilationDevice,
|
|
)
|
|
from requests.exceptions import ConnectionError as RequestConnectionError
|
|
|
|
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
from homeassistant.util.percentage import (
|
|
ordered_list_item_to_percentage,
|
|
percentage_to_ordered_list_item,
|
|
)
|
|
|
|
from .entity import ViCareEntity
|
|
from .types import ViCareConfigEntry, ViCareDevice
|
|
from .utils import filter_state, get_device_serial
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class VentilationProgram(enum.StrEnum):
|
|
"""ViCare preset ventilation programs.
|
|
|
|
As listed in https://github.com/somm15/PyViCare/blob/6c5b023ca6c8bb2d38141dd1746dc1705ec84ce8/PyViCare/PyViCareVentilationDevice.py#L37
|
|
"""
|
|
|
|
LEVEL_ONE = "levelOne"
|
|
LEVEL_TWO = "levelTwo"
|
|
LEVEL_THREE = "levelThree"
|
|
LEVEL_FOUR = "levelFour"
|
|
|
|
|
|
class VentilationMode(enum.StrEnum):
|
|
"""ViCare ventilation modes."""
|
|
|
|
PERMANENT = "permanent" # on, speed controlled by program (levelOne-levelFour)
|
|
VENTILATION = "ventilation" # activated by schedule
|
|
SENSOR_DRIVEN = "sensor_driven" # activated by schedule, override by sensor
|
|
SENSOR_OVERRIDE = "sensor_override" # activated by sensor
|
|
|
|
@staticmethod
|
|
def to_vicare_mode(mode: str | None) -> str | None:
|
|
"""Return the mapped ViCare ventilation mode for the Home Assistant mode."""
|
|
if mode:
|
|
try:
|
|
ventilation_mode = VentilationMode(mode)
|
|
except ValueError:
|
|
# ignore unsupported / unmapped modes
|
|
return None
|
|
return HA_TO_VICARE_MODE_VENTILATION.get(ventilation_mode) if mode else None
|
|
return None
|
|
|
|
@staticmethod
|
|
def from_vicare_mode(vicare_mode: str | None) -> str | None:
|
|
"""Return the mapped Home Assistant mode for the ViCare ventilation mode."""
|
|
for mode in VentilationMode:
|
|
if HA_TO_VICARE_MODE_VENTILATION.get(VentilationMode(mode)) == vicare_mode:
|
|
return mode
|
|
return None
|
|
|
|
|
|
HA_TO_VICARE_MODE_VENTILATION = {
|
|
VentilationMode.PERMANENT: "permanent",
|
|
VentilationMode.VENTILATION: "ventilation",
|
|
VentilationMode.SENSOR_DRIVEN: "sensorDriven",
|
|
VentilationMode.SENSOR_OVERRIDE: "sensorOverride",
|
|
}
|
|
|
|
ORDERED_NAMED_FAN_SPEEDS = [
|
|
VentilationProgram.LEVEL_ONE,
|
|
VentilationProgram.LEVEL_TWO,
|
|
VentilationProgram.LEVEL_THREE,
|
|
VentilationProgram.LEVEL_FOUR,
|
|
]
|
|
|
|
|
|
def _build_entities(
|
|
device_list: list[ViCareDevice],
|
|
) -> list[ViCareFan]:
|
|
"""Create ViCare climate entities for a device."""
|
|
return [
|
|
ViCareFan(get_device_serial(device.api), device.config, device.api)
|
|
for device in device_list
|
|
if isinstance(device.api, PyViCareVentilationDevice)
|
|
]
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
config_entry: ViCareConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up the ViCare fan platform."""
|
|
async_add_entities(
|
|
await hass.async_add_executor_job(
|
|
_build_entities,
|
|
config_entry.runtime_data.devices,
|
|
)
|
|
)
|
|
|
|
|
|
class ViCareFan(ViCareEntity, FanEntity):
|
|
"""Representation of the ViCare ventilation device."""
|
|
|
|
_attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS)
|
|
_attr_supported_features = FanEntityFeature.SET_SPEED
|
|
_attr_translation_key = "ventilation"
|
|
|
|
def __init__(
|
|
self,
|
|
device_serial: str | None,
|
|
device_config: PyViCareDeviceConfig,
|
|
device: PyViCareDevice,
|
|
) -> None:
|
|
"""Initialize the fan entity."""
|
|
super().__init__(
|
|
self._attr_translation_key, device_serial, device_config, device
|
|
)
|
|
# init presets
|
|
supported_modes = list[str](self._api.getAvailableModes())
|
|
self._attr_preset_modes = [
|
|
mode
|
|
for mode in VentilationMode
|
|
if VentilationMode.to_vicare_mode(mode) in supported_modes
|
|
]
|
|
if len(self._attr_preset_modes) > 0:
|
|
self._attr_supported_features |= FanEntityFeature.PRESET_MODE
|
|
|
|
def update(self) -> None:
|
|
"""Update state of fan."""
|
|
level: str | None = None
|
|
try:
|
|
with suppress(PyViCareNotSupportedFeatureError):
|
|
self._attr_preset_mode = VentilationMode.from_vicare_mode(
|
|
self._api.getActiveMode()
|
|
)
|
|
with suppress(PyViCareNotSupportedFeatureError):
|
|
level = filter_state(self._api.getVentilationLevel())
|
|
if level is not None and level in ORDERED_NAMED_FAN_SPEEDS:
|
|
self._attr_percentage = ordered_list_item_to_percentage(
|
|
ORDERED_NAMED_FAN_SPEEDS, VentilationProgram(level)
|
|
)
|
|
else:
|
|
self._attr_percentage = 0
|
|
except RequestConnectionError:
|
|
_LOGGER.error("Unable to retrieve data from ViCare server")
|
|
except ValueError:
|
|
_LOGGER.error("Unable to decode data from ViCare server")
|
|
except PyViCareRateLimitError as limit_exception:
|
|
_LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception)
|
|
except PyViCareInvalidDataError as invalid_data_exception:
|
|
_LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception)
|
|
|
|
@property
|
|
def is_on(self) -> bool | None:
|
|
"""Return true if the entity is on."""
|
|
# Viessmann ventilation unit cannot be turned off
|
|
return True
|
|
|
|
@property
|
|
def icon(self) -> str | None:
|
|
"""Return the icon to use in the frontend."""
|
|
if hasattr(self, "_attr_preset_mode"):
|
|
if self._attr_preset_mode == VentilationMode.VENTILATION:
|
|
return "mdi:fan-clock"
|
|
if self._attr_preset_mode in [
|
|
VentilationMode.SENSOR_DRIVEN,
|
|
VentilationMode.SENSOR_OVERRIDE,
|
|
]:
|
|
return "mdi:fan-auto"
|
|
if self._attr_preset_mode == VentilationMode.PERMANENT:
|
|
if self._attr_percentage == 0:
|
|
return "mdi:fan-off"
|
|
if self._attr_percentage is not None:
|
|
level = 1 + ORDERED_NAMED_FAN_SPEEDS.index(
|
|
percentage_to_ordered_list_item(
|
|
ORDERED_NAMED_FAN_SPEEDS, self._attr_percentage
|
|
)
|
|
)
|
|
if level < 4: # fan-speed- only supports 1-3
|
|
return f"mdi:fan-speed-{level}"
|
|
return "mdi:fan"
|
|
|
|
def set_percentage(self, percentage: int) -> None:
|
|
"""Set the speed of the fan, as a percentage."""
|
|
if self._attr_preset_mode != str(VentilationMode.PERMANENT):
|
|
self.set_preset_mode(VentilationMode.PERMANENT)
|
|
|
|
level = percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage)
|
|
_LOGGER.debug("changing ventilation level to %s", level)
|
|
self._api.setPermanentLevel(level)
|
|
|
|
def set_preset_mode(self, preset_mode: str) -> None:
|
|
"""Set new preset mode."""
|
|
target_mode = VentilationMode.to_vicare_mode(preset_mode)
|
|
_LOGGER.debug("changing ventilation mode to %s", target_mode)
|
|
self._api.setActiveMode(target_mode)
|