"""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