core/homeassistant/components/vallox/fan.py

214 lines
6.1 KiB
Python

"""Support for the Vallox ventilation unit fan."""
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any, NamedTuple
from vallox_websocket_api import Vallox
from vallox_websocket_api.exceptions import ValloxApiException
from homeassistant.components.fan import (
SUPPORT_PRESET_MODE,
FanEntity,
NotValidPresetModeError,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import ValloxDataUpdateCoordinator
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,
STR_TO_VALLOX_PROFILE_SETTABLE,
VALLOX_PROFILE_TO_STR_SETTABLE,
)
_LOGGER = logging.getLogger(__name__)
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_fan_speed_value(value: StateType) -> int | None:
if isinstance(value, (int, float)):
return int(value)
return None
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the fan device."""
if discovery_info is None:
return
client = hass.data[DOMAIN]["client"]
client.set_settable_address(METRIC_KEY_MODE, int)
device = ValloxFan(
hass.data[DOMAIN]["name"], client, hass.data[DOMAIN]["coordinator"]
)
async_add_entities([device])
class ValloxFan(CoordinatorEntity, FanEntity):
"""Representation of the fan."""
coordinator: ValloxDataUpdateCoordinator
def __init__(
self,
name: str,
client: Vallox,
coordinator: ValloxDataUpdateCoordinator,
) -> None:
"""Initialize the fan."""
super().__init__(coordinator)
self._client = client
self._attr_name = name
self._attr_unique_id = str(self.coordinator.data.get_uuid())
@property
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_PRESET_MODE
@property
def preset_modes(self) -> list[str]:
"""Return a list of available preset modes."""
# Use the Vallox profile names for the preset names.
return list(STR_TO_VALLOX_PROFILE_SETTABLE.keys())
@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_STR_SETTABLE.get(vallox_profile)
@property
def extra_state_attributes(self) -> Mapping[str, int | None]:
"""Return device specific state attributes."""
data = self.coordinator.data
return {
attr.description: _convert_fan_speed_value(data.get_metric(attr.metric_key))
for attr in EXTRA_STATE_ATTRIBUTES
}
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) # type: ignore[no-untyped-call]
except NotValidPresetModeError as err:
_LOGGER.error(err)
return False
if preset_mode == self.preset_mode:
return False
try:
await self._client.set_profile(STR_TO_VALLOX_PROFILE_SETTABLE[preset_mode])
except (OSError, ValloxApiException) as err:
_LOGGER.error("Error setting preset: %s", err)
return False
return True
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,
speed: str | None = None,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Turn the device on."""
_LOGGER.debug("Turn on")
update_needed = False
if preset_mode:
update_needed = await self._async_set_preset_mode_internal(preset_mode)
if not self.is_on:
try:
await self._client.set_values({METRIC_KEY_MODE: MODE_ON})
except OSError as err:
_LOGGER.error("Error turning on: %s", err)
else:
update_needed = True
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
try:
await self._client.set_values({METRIC_KEY_MODE: MODE_OFF})
except OSError as err:
_LOGGER.error("Error turning off: %s", err)
return
# Same as for turn_on method.
await self.coordinator.async_request_refresh()