Migrate Vallox to new fan entity model (#56663)

* Migrate Vallox to new fan entity model

* Review comments 1

* Minor corrections

* Review comments 2
This commit is contained in:
Andre Richter 2021-09-29 17:14:41 +02:00 committed by GitHub
parent 00651a4055
commit d13c3e3917
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 126 additions and 64 deletions

View File

@ -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()

View File

@ -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()
}

View File

@ -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)
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
# This state change affects other entities like sensors. Force an immediate update that can
# be observed by all parties involved.
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()

View File

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

View File

@ -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: