From d13c3e3917c7f9c4c84335b5b53bef91618a1457 Mon Sep 17 00:00:00 2001 From: Andre Richter Date: Wed, 29 Sep 2021 17:14:41 +0200 Subject: [PATCH] Migrate Vallox to new fan entity model (#56663) * Migrate Vallox to new fan entity model * Review comments 1 * Minor corrections * Review comments 2 --- homeassistant/components/vallox/__init__.py | 35 +++-- homeassistant/components/vallox/const.py | 18 +++ homeassistant/components/vallox/fan.py | 124 ++++++++++++------ homeassistant/components/vallox/sensor.py | 11 +- homeassistant/components/vallox/services.yaml | 2 +- 5 files changed, 126 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 96c83c82c36..bdd7242a76a 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -30,6 +30,7 @@ from .const import ( METRIC_KEY_PROFILE_FAN_SPEED_HOME, SIGNAL_VALLOX_STATE_UPDATE, STATE_PROXY_SCAN_INTERVAL, + STR_TO_VALLOX_PROFILE_SETTABLE, ) _LOGGER = logging.getLogger(__name__) @@ -46,25 +47,15 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PROFILE_TO_STR_SETTABLE = { - VALLOX_PROFILE.HOME: "Home", - VALLOX_PROFILE.AWAY: "Away", - VALLOX_PROFILE.BOOST: "Boost", - VALLOX_PROFILE.FIREPLACE: "Fireplace", -} - -STR_TO_PROFILE = {v: k for (k, v) in PROFILE_TO_STR_SETTABLE.items()} - -PROFILE_TO_STR_REPORTABLE = { - **{VALLOX_PROFILE.NONE: "None", VALLOX_PROFILE.EXTRA: "Extra"}, - **PROFILE_TO_STR_SETTABLE, -} - ATTR_PROFILE = "profile" ATTR_PROFILE_FAN_SPEED = "fan_speed" SERVICE_SCHEMA_SET_PROFILE = vol.Schema( - {vol.Required(ATTR_PROFILE): vol.All(cv.string, vol.In(STR_TO_PROFILE))} + { + vol.Required(ATTR_PROFILE): vol.All( + cv.string, vol.In(STR_TO_VALLOX_PROFILE_SETTABLE) + ) + } ) SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED = vol.Schema( @@ -163,14 +154,14 @@ class ValloxStateProxy: return value - def get_profile(self) -> str: + def get_profile(self) -> VALLOX_PROFILE: """Return cached profile value.""" _LOGGER.debug("Returning profile") if not self._valid: raise OSError("Device state out of sync.") - return PROFILE_TO_STR_REPORTABLE[self._profile] + return self._profile async def async_update(self, time: datetime | None = None) -> None: """Fetch state update.""" @@ -201,8 +192,13 @@ class ValloxServiceHandler: """Set the ventilation profile.""" _LOGGER.debug("Setting ventilation profile to: %s", profile) + _LOGGER.warning( + "Attention: The service 'vallox.set_profile' is superseded by the 'fan.set_preset_mode' service." + "It will be removed in the future, please migrate to 'fan.set_preset_mode' to prevent breakage" + ) + try: - await self._client.set_profile(STR_TO_PROFILE[profile]) + await self._client.set_profile(STR_TO_VALLOX_PROFILE_SETTABLE[profile]) return True except (OSError, ValloxApiException) as err: @@ -271,6 +267,7 @@ class ValloxServiceHandler: result = await getattr(self, method["method"])(**params) - # Force state_proxy to refresh device state, so that updates are propagated to platforms. + # This state change affects other entities like sensors. Force an immediate update that can + # be observed by all parties involved. if result: await self._state_proxy.async_update() diff --git a/homeassistant/components/vallox/const.py b/homeassistant/components/vallox/const.py index 038e46043da..6a9c4ddc5f4 100644 --- a/homeassistant/components/vallox/const.py +++ b/homeassistant/components/vallox/const.py @@ -2,6 +2,8 @@ from datetime import timedelta +from vallox_websocket_api import PROFILE as VALLOX_PROFILE + DOMAIN = "vallox" DEFAULT_NAME = "Vallox" @@ -20,3 +22,19 @@ MODE_OFF = 5 DEFAULT_FAN_SPEED_HOME = 50 DEFAULT_FAN_SPEED_AWAY = 25 DEFAULT_FAN_SPEED_BOOST = 65 + +VALLOX_PROFILE_TO_STR_SETTABLE = { + VALLOX_PROFILE.HOME: "Home", + VALLOX_PROFILE.AWAY: "Away", + VALLOX_PROFILE.BOOST: "Boost", + VALLOX_PROFILE.FIREPLACE: "Fireplace", +} + +VALLOX_PROFILE_TO_STR_REPORTABLE = { + VALLOX_PROFILE.EXTRA: "Extra", + **VALLOX_PROFILE_TO_STR_SETTABLE, +} + +STR_TO_VALLOX_PROFILE_SETTABLE = { + value: key for (key, value) in VALLOX_PROFILE_TO_STR_SETTABLE.items() +} diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index b8d320a7e7e..8ee1b8b471f 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -6,8 +6,13 @@ import logging from typing import Any from vallox_websocket_api import Vallox +from vallox_websocket_api.exceptions import ValloxApiException -from homeassistant.components.fan import FanEntity +from homeassistant.components.fan import ( + SUPPORT_PRESET_MODE, + FanEntity, + NotValidPresetModeError, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -23,11 +28,12 @@ from .const import ( MODE_OFF, MODE_ON, SIGNAL_VALLOX_STATE_UPDATE, + STR_TO_VALLOX_PROFILE_SETTABLE, + VALLOX_PROFILE_TO_STR_SETTABLE, ) _LOGGER = logging.getLogger(__name__) -# Device attributes ATTR_PROFILE_FAN_SPEED_HOME = { "description": "fan_speed_home", "metric_key": METRIC_KEY_PROFILE_FAN_SPEED_HOME, @@ -65,39 +71,44 @@ async def async_setup_platform( class ValloxFan(FanEntity): """Representation of the fan.""" + _attr_should_poll = False + def __init__( self, name: str, client: Vallox, state_proxy: ValloxStateProxy ) -> None: """Initialize the fan.""" - self._name = name self._client = client self._state_proxy = state_proxy - self._available = False self._is_on = False + self._preset_mode: str | None = None self._fan_speed_home: int | None = None self._fan_speed_away: int | None = None self._fan_speed_boost: int | None = None - @property - def should_poll(self) -> bool: - """Do not poll the device.""" - return False + self._attr_name = name + self._attr_available = False @property - def name(self) -> str: - """Return the name of the device.""" - return self._name + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_PRESET_MODE @property - def available(self) -> bool: - """Return if state is known.""" - return self._available + 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._is_on + @property + def preset_mode(self) -> str | None: + """Return the current preset mode.""" + return self._preset_mode + @property def extra_state_attributes(self) -> Mapping[str, int | None]: """Return device specific state attributes.""" @@ -126,6 +137,8 @@ class ValloxFan(FanEntity): # Fetch if the whole device is in regular operation state. self._is_on = self._state_proxy.fetch_metric(METRIC_KEY_MODE) == MODE_ON + vallox_profile = self._state_proxy.get_profile() + # Fetch the profile fan speeds. fan_speed_home = self._state_proxy.fetch_metric( ATTR_PROFILE_FAN_SPEED_HOME["metric_key"] @@ -138,10 +151,12 @@ class ValloxFan(FanEntity): ) except (OSError, KeyError, TypeError) as err: - self._available = False + self._attr_available = False _LOGGER.error("Error updating fan: %s", err) return + self._preset_mode = VALLOX_PROFILE_TO_STR_SETTABLE.get(vallox_profile) + self._fan_speed_home = ( int(fan_speed_home) if isinstance(fan_speed_home, (int, float)) else None ) @@ -152,15 +167,42 @@ class ValloxFan(FanEntity): int(fan_speed_boost) if isinstance(fan_speed_boost, (int, float)) else None ) - self._available = True + self._attr_available = 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) # 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._state_proxy.async_update() - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # async def async_turn_on( self, speed: str | None = None, @@ -171,39 +213,37 @@ class ValloxFan(FanEntity): """Turn the device on.""" _LOGGER.debug("Turn on: %s", speed) - # Only the case speed == None equals the GUI toggle switch being activated. - if speed is not None: - return + update_needed = False - if self._is_on: - _LOGGER.error("Already on") - return + if preset_mode: + update_needed = await self._async_set_preset_mode_internal(preset_mode) - try: - await self._client.set_values({METRIC_KEY_MODE: MODE_ON}) + if not self.is_on: + try: + await self._client.set_values({METRIC_KEY_MODE: MODE_ON}) - except OSError as err: - self._available = False - _LOGGER.error("Error turning on: %s", err) - return + except OSError as err: + _LOGGER.error("Error turning on: %s", err) - # This state change affects other entities like sensors. Force an immediate update that can - # be observed by all parties involved. - await self._state_proxy.async_update() + 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._state_proxy.async_update() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - if not self._is_on: - _LOGGER.error("Already off") + if not self.is_on: return try: await self._client.set_values({METRIC_KEY_MODE: MODE_OFF}) except OSError as err: - self._available = False _LOGGER.error("Error turning off: %s", err) return # Same as for turn_on method. - await self._state_proxy.async_update(None) + await self._state_proxy.async_update() diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 74920853eb6..ff22c317bc1 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -25,7 +25,13 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import ValloxStateProxy -from .const import DOMAIN, METRIC_KEY_MODE, MODE_ON, SIGNAL_VALLOX_STATE_UPDATE +from .const import ( + DOMAIN, + METRIC_KEY_MODE, + MODE_ON, + SIGNAL_VALLOX_STATE_UPDATE, + VALLOX_PROFILE_TO_STR_REPORTABLE, +) _LOGGER = logging.getLogger(__name__) @@ -89,13 +95,14 @@ class ValloxProfileSensor(ValloxSensor): async def async_update(self) -> None: """Fetch state from the ventilation unit.""" try: - self._attr_native_value = self._state_proxy.get_profile() + vallox_profile = self._state_proxy.get_profile() except OSError as err: self._attr_available = False _LOGGER.error("Error updating sensor: %s", err) return + self._attr_native_value = VALLOX_PROFILE_TO_STR_REPORTABLE.get(vallox_profile) self._attr_available = True diff --git a/homeassistant/components/vallox/services.yaml b/homeassistant/components/vallox/services.yaml index 98d7abac249..5cfa1dae4b5 100644 --- a/homeassistant/components/vallox/services.yaml +++ b/homeassistant/components/vallox/services.yaml @@ -15,7 +15,7 @@ set_profile: - 'Home' set_profile_fan_speed_home: - name: Set profile fan speed hom + name: Set profile fan speed home description: Set the fan speed of the Home profile. fields: fan_speed: