core/homeassistant/components/vallox/fan.py

242 lines
7.4 KiB
Python

"""Support for the Vallox ventilation unit fan."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any, NamedTuple
from vallox_websocket_api import (
PROFILE_TO_SET_FAN_SPEED_METRIC_MAP,
Vallox,
ValloxApiException,
ValloxInvalidInputException,
)
from homeassistant.components.fan import (
FanEntity,
FanEntityFeature,
NotValidPresetModeError,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import ValloxDataUpdateCoordinator, ValloxEntity
from .const import (
DOMAIN,
METRIC_KEY_MODE,
METRIC_KEY_PROFILE_FAN_SPEED_AWAY,
METRIC_KEY_PROFILE_FAN_SPEED_BOOST,
METRIC_KEY_PROFILE_FAN_SPEED_HOME,
MODE_OFF,
MODE_ON,
PRESET_MODE_TO_VALLOX_PROFILE_SETTABLE,
VALLOX_PROFILE_TO_PRESET_MODE_REPORTABLE,
)
class ExtraStateAttributeDetails(NamedTuple):
"""Extra state attribute details."""
description: str
metric_key: str
EXTRA_STATE_ATTRIBUTES = (
ExtraStateAttributeDetails(
description="fan_speed_home", metric_key=METRIC_KEY_PROFILE_FAN_SPEED_HOME
),
ExtraStateAttributeDetails(
description="fan_speed_away", metric_key=METRIC_KEY_PROFILE_FAN_SPEED_AWAY
),
ExtraStateAttributeDetails(
description="fan_speed_boost", metric_key=METRIC_KEY_PROFILE_FAN_SPEED_BOOST
),
)
def _convert_to_int(value: StateType) -> int | None:
if isinstance(value, (int, float)):
return int(value)
return None
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the fan device."""
data = hass.data[DOMAIN][entry.entry_id]
client = data["client"]
device = ValloxFanEntity(
data["name"],
client,
data["coordinator"],
)
async_add_entities([device])
class ValloxFanEntity(ValloxEntity, FanEntity):
"""Representation of the fan."""
_attr_has_entity_name = True
_attr_supported_features = FanEntityFeature.PRESET_MODE | FanEntityFeature.SET_SPEED
def __init__(
self,
name: str,
client: Vallox,
coordinator: ValloxDataUpdateCoordinator,
) -> None:
"""Initialize the fan."""
super().__init__(name, coordinator)
self._client = client
self._attr_unique_id = str(self._device_uuid)
self._attr_preset_modes = list(PRESET_MODE_TO_VALLOX_PROFILE_SETTABLE)
@property
def is_on(self) -> bool:
"""Return if device is on."""
return self.coordinator.data.get_metric(METRIC_KEY_MODE) == MODE_ON
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
vallox_profile = self.coordinator.data.profile
return VALLOX_PROFILE_TO_PRESET_MODE_REPORTABLE.get(vallox_profile)
@property
def percentage(self) -> int | None:
"""Return the current speed as a percentage."""
vallox_profile = self.coordinator.data.profile
metric_key = PROFILE_TO_SET_FAN_SPEED_METRIC_MAP.get(vallox_profile)
if not metric_key:
return None
return _convert_to_int(self.coordinator.data.get_metric(metric_key))
@property
def extra_state_attributes(self) -> Mapping[str, int | None]:
"""Return device specific state attributes."""
data = self.coordinator.data
return {
attr.description: _convert_to_int(data.get_metric(attr.metric_key))
for attr in EXTRA_STATE_ATTRIBUTES
}
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
update_needed = await self._async_set_preset_mode_internal(preset_mode)
if update_needed:
# This state change affects other entities like sensors. Force an immediate update that
# can be observed by all parties involved.
await self.coordinator.async_request_refresh()
async def async_turn_on(
self,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Turn the device on."""
update_needed = False
if not self.is_on:
update_needed |= await self._async_set_power(True)
if preset_mode:
update_needed |= await self._async_set_preset_mode_internal(preset_mode)
if percentage is not None:
update_needed |= await self._async_set_percentage_internal(percentage)
if update_needed:
# This state change affects other entities like sensors. Force an immediate update that
# can be observed by all parties involved.
await self.coordinator.async_request_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
if not self.is_on:
return
update_needed = await self._async_set_power(False)
if update_needed:
await self.coordinator.async_request_refresh()
async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage."""
if percentage == 0:
await self.async_turn_off()
return
update_needed = await self._async_set_percentage_internal(percentage)
if update_needed:
await self.coordinator.async_request_refresh()
async def _async_set_power(self, mode: bool) -> bool:
try:
await self._client.set_values(
{METRIC_KEY_MODE: MODE_ON if mode else MODE_OFF}
)
except ValloxApiException as err:
raise HomeAssistantError("Failed to set power mode") from err
return True
async def _async_set_preset_mode_internal(self, preset_mode: str) -> bool:
"""
Set new preset mode.
Returns true if the mode has been changed, false otherwise.
"""
try:
self._valid_preset_mode_or_raise(preset_mode)
except NotValidPresetModeError as err:
raise ValueError(f"Not valid preset mode: {preset_mode}") from err
if preset_mode == self.preset_mode:
return False
try:
profile = PRESET_MODE_TO_VALLOX_PROFILE_SETTABLE[preset_mode]
await self._client.set_profile(profile)
self.coordinator.data.profile = profile
except ValloxApiException as err:
raise HomeAssistantError(f"Failed to set profile: {preset_mode}") from err
return True
async def _async_set_percentage_internal(self, percentage: int) -> bool:
"""
Set fan speed percentage for current profile.
Returns true if speed has been changed, false otherwise.
"""
vallox_profile = self.coordinator.data.profile
try:
await self._client.set_fan_speed(vallox_profile, percentage)
except ValloxInvalidInputException as err:
# This can happen if current profile does not support setting the fan speed.
raise ValueError(
f"{vallox_profile} profile does not support setting the fan speed"
) from err
except ValloxApiException as err:
raise HomeAssistantError("Failed to set fan speed") from err
return True