mirror of
https://github.com/home-assistant/core.git
synced 2025-05-01 04:37:52 +00:00
Use DataUpdateCoordinator in Vallox (#56966)
This commit is contained in:
parent
02372cd65a
commit
6c01ed8d97
@ -1,7 +1,7 @@
|
|||||||
"""Support for Vallox ventilation units."""
|
"""Support for Vallox ventilation units."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from dataclasses import dataclass, field
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -11,13 +11,12 @@ from vallox_websocket_api.constants import vlxDevConstants
|
|||||||
from vallox_websocket_api.exceptions import ValloxApiException
|
from vallox_websocket_api.exceptions import ValloxApiException
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.discovery import async_load_platform
|
from homeassistant.helpers.discovery import async_load_platform
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
|
||||||
from homeassistant.helpers.typing import ConfigType, StateType
|
from homeassistant.helpers.typing import ConfigType, StateType
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DEFAULT_FAN_SPEED_AWAY,
|
DEFAULT_FAN_SPEED_AWAY,
|
||||||
@ -28,8 +27,7 @@ from .const import (
|
|||||||
METRIC_KEY_PROFILE_FAN_SPEED_AWAY,
|
METRIC_KEY_PROFILE_FAN_SPEED_AWAY,
|
||||||
METRIC_KEY_PROFILE_FAN_SPEED_BOOST,
|
METRIC_KEY_PROFILE_FAN_SPEED_BOOST,
|
||||||
METRIC_KEY_PROFILE_FAN_SPEED_HOME,
|
METRIC_KEY_PROFILE_FAN_SPEED_HOME,
|
||||||
SIGNAL_VALLOX_STATE_UPDATE,
|
STATE_SCAN_INTERVAL,
|
||||||
STATE_PROXY_SCAN_INTERVAL,
|
|
||||||
STR_TO_VALLOX_PROFILE_SETTABLE,
|
STR_TO_VALLOX_PROFILE_SETTABLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -91,6 +89,40 @@ SERVICE_TO_METHOD = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ValloxState:
|
||||||
|
"""Describes the current state of the unit."""
|
||||||
|
|
||||||
|
metric_cache: dict[str, Any] = field(default_factory=dict)
|
||||||
|
profile: VALLOX_PROFILE = VALLOX_PROFILE.NONE
|
||||||
|
|
||||||
|
def get_metric(self, metric_key: str) -> StateType:
|
||||||
|
"""Return cached state value."""
|
||||||
|
_LOGGER.debug("Fetching metric key: %s", metric_key)
|
||||||
|
|
||||||
|
if metric_key not in vlxDevConstants.__dict__:
|
||||||
|
_LOGGER.debug("Metric key invalid: %s", metric_key)
|
||||||
|
|
||||||
|
if (value := self.metric_cache.get(metric_key)) is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(value, (str, int, float)):
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Return value of metric %s has unexpected type %s",
|
||||||
|
metric_key,
|
||||||
|
type(value),
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class ValloxDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
|
"""The DataUpdateCoordinator for Vallox."""
|
||||||
|
|
||||||
|
data: ValloxState
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the client and boot the platforms."""
|
"""Set up the client and boot the platforms."""
|
||||||
conf = config[DOMAIN]
|
conf = config[DOMAIN]
|
||||||
@ -98,102 +130,74 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
name = conf.get(CONF_NAME)
|
name = conf.get(CONF_NAME)
|
||||||
|
|
||||||
client = Vallox(host)
|
client = Vallox(host)
|
||||||
state_proxy = ValloxStateProxy(hass, client)
|
|
||||||
service_handler = ValloxServiceHandler(client, state_proxy)
|
|
||||||
|
|
||||||
hass.data[DOMAIN] = {"client": client, "state_proxy": state_proxy, "name": name}
|
async def async_update_data() -> ValloxState:
|
||||||
|
"""Fetch state update."""
|
||||||
|
_LOGGER.debug("Updating Vallox state cache")
|
||||||
|
|
||||||
|
try:
|
||||||
|
metric_cache = await client.fetch_metrics()
|
||||||
|
profile = await client.get_profile()
|
||||||
|
|
||||||
|
except (OSError, ValloxApiException) as err:
|
||||||
|
raise UpdateFailed("Error during state cache update") from err
|
||||||
|
|
||||||
|
return ValloxState(metric_cache, profile)
|
||||||
|
|
||||||
|
coordinator = ValloxDataUpdateCoordinator(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name=f"{name} DataUpdateCoordinator",
|
||||||
|
update_interval=STATE_SCAN_INTERVAL,
|
||||||
|
update_method=async_update_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
service_handler = ValloxServiceHandler(client, coordinator)
|
||||||
for vallox_service, method in SERVICE_TO_METHOD.items():
|
for vallox_service, method in SERVICE_TO_METHOD.items():
|
||||||
schema = method["schema"]
|
schema = method["schema"]
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, vallox_service, service_handler.async_handle, schema=schema
|
DOMAIN, vallox_service, service_handler.async_handle, schema=schema
|
||||||
)
|
)
|
||||||
|
|
||||||
# The vallox hardware expects quite strict timings for websocket requests. Timings that machines
|
hass.data[DOMAIN] = {"client": client, "coordinator": coordinator, "name": name}
|
||||||
# with less processing power, like Raspberries, cannot live up to during the busy start phase of
|
|
||||||
# Home Asssistant. Hence, async_add_entities() for fan and sensor in respective code will be
|
async def _async_load_platform_delayed(*_: Any) -> None:
|
||||||
# called with update_before_add=False to intentionally delay the first request, increasing
|
await coordinator.async_refresh()
|
||||||
# chance that it is issued only when the machine is less busy again.
|
|
||||||
hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config))
|
hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config))
|
||||||
hass.async_create_task(async_load_platform(hass, "fan", DOMAIN, {}, config))
|
hass.async_create_task(async_load_platform(hass, "fan", DOMAIN, {}, config))
|
||||||
|
|
||||||
async_track_time_interval(hass, state_proxy.async_update, STATE_PROXY_SCAN_INTERVAL)
|
# The Vallox hardware expects quite strict timings for websocket requests. Timings that machines
|
||||||
|
# with less processing power, like a Raspberry Pi, cannot live up to during the busy start phase
|
||||||
return True
|
# of Home Asssistant.
|
||||||
|
#
|
||||||
|
# Hence, wait for the started event before doing a first data refresh and loading the platforms,
|
||||||
class ValloxStateProxy:
|
# because it usually means the system is less busy after the event and can now meet the
|
||||||
"""Helper class to reduce websocket API calls."""
|
# websocket timing requirements.
|
||||||
|
hass.bus.async_listen_once(
|
||||||
def __init__(self, hass: HomeAssistant, client: Vallox) -> None:
|
EVENT_HOMEASSISTANT_STARTED, _async_load_platform_delayed
|
||||||
"""Initialize the proxy."""
|
|
||||||
self._hass = hass
|
|
||||||
self._client = client
|
|
||||||
self._metric_cache: dict[str, Any] = {}
|
|
||||||
self._profile = VALLOX_PROFILE.NONE
|
|
||||||
self._valid = False
|
|
||||||
|
|
||||||
def fetch_metric(self, metric_key: str) -> StateType:
|
|
||||||
"""Return cached state value."""
|
|
||||||
_LOGGER.debug("Fetching metric key: %s", metric_key)
|
|
||||||
|
|
||||||
if not self._valid:
|
|
||||||
raise OSError("Device state out of sync.")
|
|
||||||
|
|
||||||
if metric_key not in vlxDevConstants.__dict__:
|
|
||||||
raise KeyError(f"Unknown metric key: {metric_key}")
|
|
||||||
|
|
||||||
if (value := self._metric_cache[metric_key]) is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not isinstance(value, (str, int, float)):
|
|
||||||
raise TypeError(
|
|
||||||
f"Return value of metric {metric_key} has unexpected type {type(value)}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return value
|
return True
|
||||||
|
|
||||||
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 self._profile
|
|
||||||
|
|
||||||
async def async_update(self, time: datetime | None = None) -> None:
|
|
||||||
"""Fetch state update."""
|
|
||||||
_LOGGER.debug("Updating Vallox state cache")
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._metric_cache = await self._client.fetch_metrics()
|
|
||||||
self._profile = await self._client.get_profile()
|
|
||||||
|
|
||||||
except (OSError, ValloxApiException) as err:
|
|
||||||
self._valid = False
|
|
||||||
_LOGGER.error("Error during state cache update: %s", err)
|
|
||||||
return
|
|
||||||
|
|
||||||
self._valid = True
|
|
||||||
async_dispatcher_send(self._hass, SIGNAL_VALLOX_STATE_UPDATE)
|
|
||||||
|
|
||||||
|
|
||||||
class ValloxServiceHandler:
|
class ValloxServiceHandler:
|
||||||
"""Services implementation."""
|
"""Services implementation."""
|
||||||
|
|
||||||
def __init__(self, client: Vallox, state_proxy: ValloxStateProxy) -> None:
|
def __init__(
|
||||||
|
self, client: Vallox, coordinator: DataUpdateCoordinator[ValloxState]
|
||||||
|
) -> None:
|
||||||
"""Initialize the proxy."""
|
"""Initialize the proxy."""
|
||||||
self._client = client
|
self._client = client
|
||||||
self._state_proxy = state_proxy
|
self._coordinator = coordinator
|
||||||
|
|
||||||
async def async_set_profile(self, profile: str = "Home") -> bool:
|
async def async_set_profile(self, profile: str = "Home") -> bool:
|
||||||
"""Set the ventilation profile."""
|
"""Set the ventilation profile."""
|
||||||
_LOGGER.debug("Setting ventilation profile to: %s", profile)
|
_LOGGER.debug("Setting ventilation profile to: %s", profile)
|
||||||
|
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Attention: The service 'vallox.set_profile' is superseded by the 'fan.set_preset_mode' service."
|
"Attention: The service 'vallox.set_profile' is superseded by the "
|
||||||
"It will be removed in the future, please migrate to 'fan.set_preset_mode' to prevent breakage"
|
"'fan.set_preset_mode' service. It will be removed in the future, please migrate to "
|
||||||
|
"'fan.set_preset_mode' to prevent breakage"
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -269,4 +273,4 @@ class ValloxServiceHandler:
|
|||||||
# This state change affects other entities like sensors. Force an immediate update that can
|
# This state change affects other entities like sensors. Force an immediate update that can
|
||||||
# be observed by all parties involved.
|
# be observed by all parties involved.
|
||||||
if result:
|
if result:
|
||||||
await self._state_proxy.async_update()
|
await self._coordinator.async_request_refresh()
|
||||||
|
@ -7,8 +7,7 @@ from vallox_websocket_api import PROFILE as VALLOX_PROFILE
|
|||||||
DOMAIN = "vallox"
|
DOMAIN = "vallox"
|
||||||
DEFAULT_NAME = "Vallox"
|
DEFAULT_NAME = "Vallox"
|
||||||
|
|
||||||
SIGNAL_VALLOX_STATE_UPDATE = "vallox_state_update"
|
STATE_SCAN_INTERVAL = timedelta(seconds=60)
|
||||||
STATE_PROXY_SCAN_INTERVAL = timedelta(seconds=60)
|
|
||||||
|
|
||||||
# Common metric keys and (default) values.
|
# Common metric keys and (default) values.
|
||||||
METRIC_KEY_MODE = "A_CYC_MODE"
|
METRIC_KEY_MODE = "A_CYC_MODE"
|
||||||
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any, NamedTuple
|
||||||
|
|
||||||
from vallox_websocket_api import Vallox
|
from vallox_websocket_api import Vallox
|
||||||
from vallox_websocket_api.exceptions import ValloxApiException
|
from vallox_websocket_api.exceptions import ValloxApiException
|
||||||
@ -13,12 +13,12 @@ from homeassistant.components.fan import (
|
|||||||
FanEntity,
|
FanEntity,
|
||||||
NotValidPresetModeError,
|
NotValidPresetModeError,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import ValloxStateProxy
|
from . import ValloxDataUpdateCoordinator
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
METRIC_KEY_MODE,
|
METRIC_KEY_MODE,
|
||||||
@ -27,25 +27,38 @@ from .const import (
|
|||||||
METRIC_KEY_PROFILE_FAN_SPEED_HOME,
|
METRIC_KEY_PROFILE_FAN_SPEED_HOME,
|
||||||
MODE_OFF,
|
MODE_OFF,
|
||||||
MODE_ON,
|
MODE_ON,
|
||||||
SIGNAL_VALLOX_STATE_UPDATE,
|
|
||||||
STR_TO_VALLOX_PROFILE_SETTABLE,
|
STR_TO_VALLOX_PROFILE_SETTABLE,
|
||||||
VALLOX_PROFILE_TO_STR_SETTABLE,
|
VALLOX_PROFILE_TO_STR_SETTABLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_PROFILE_FAN_SPEED_HOME = {
|
|
||||||
"description": "fan_speed_home",
|
class ExtraStateAttributeDetails(NamedTuple):
|
||||||
"metric_key": METRIC_KEY_PROFILE_FAN_SPEED_HOME,
|
"""Extra state attribute properties."""
|
||||||
}
|
|
||||||
ATTR_PROFILE_FAN_SPEED_AWAY = {
|
description: str
|
||||||
"description": "fan_speed_away",
|
metric_key: str
|
||||||
"metric_key": METRIC_KEY_PROFILE_FAN_SPEED_AWAY,
|
|
||||||
}
|
|
||||||
ATTR_PROFILE_FAN_SPEED_BOOST = {
|
EXTRA_STATE_ATTRIBUTES = (
|
||||||
"description": "fan_speed_boost",
|
ExtraStateAttributeDetails(
|
||||||
"metric_key": METRIC_KEY_PROFILE_FAN_SPEED_BOOST,
|
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(
|
async def async_setup_platform(
|
||||||
@ -62,31 +75,29 @@ async def async_setup_platform(
|
|||||||
client.set_settable_address(METRIC_KEY_MODE, int)
|
client.set_settable_address(METRIC_KEY_MODE, int)
|
||||||
|
|
||||||
device = ValloxFan(
|
device = ValloxFan(
|
||||||
hass.data[DOMAIN]["name"], client, hass.data[DOMAIN]["state_proxy"]
|
hass.data[DOMAIN]["name"], client, hass.data[DOMAIN]["coordinator"]
|
||||||
)
|
)
|
||||||
|
|
||||||
async_add_entities([device], update_before_add=False)
|
async_add_entities([device])
|
||||||
|
|
||||||
|
|
||||||
class ValloxFan(FanEntity):
|
class ValloxFan(CoordinatorEntity, FanEntity):
|
||||||
"""Representation of the fan."""
|
"""Representation of the fan."""
|
||||||
|
|
||||||
_attr_should_poll = False
|
coordinator: ValloxDataUpdateCoordinator
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, name: str, client: Vallox, state_proxy: ValloxStateProxy
|
self,
|
||||||
|
name: str,
|
||||||
|
client: Vallox,
|
||||||
|
coordinator: ValloxDataUpdateCoordinator,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the fan."""
|
"""Initialize the fan."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
|
||||||
self._client = client
|
self._client = client
|
||||||
self._state_proxy = state_proxy
|
|
||||||
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
|
|
||||||
|
|
||||||
self._attr_name = name
|
self._attr_name = name
|
||||||
self._attr_available = False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self) -> int:
|
def supported_features(self) -> int:
|
||||||
@ -102,73 +113,24 @@ class ValloxFan(FanEntity):
|
|||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return if device is on."""
|
"""Return if device is on."""
|
||||||
return self._is_on
|
return self.coordinator.data.get_metric(METRIC_KEY_MODE) == MODE_ON
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def preset_mode(self) -> str | None:
|
def preset_mode(self) -> str | None:
|
||||||
"""Return the current preset mode."""
|
"""Return the current preset mode."""
|
||||||
return self._preset_mode
|
vallox_profile = self.coordinator.data.profile
|
||||||
|
return VALLOX_PROFILE_TO_STR_SETTABLE.get(vallox_profile)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self) -> Mapping[str, int | None]:
|
def extra_state_attributes(self) -> Mapping[str, int | None]:
|
||||||
"""Return device specific state attributes."""
|
"""Return device specific state attributes."""
|
||||||
|
data = self.coordinator.data
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ATTR_PROFILE_FAN_SPEED_HOME["description"]: self._fan_speed_home,
|
attr.description: _convert_fan_speed_value(data.get_metric(attr.metric_key))
|
||||||
ATTR_PROFILE_FAN_SPEED_AWAY["description"]: self._fan_speed_away,
|
for attr in EXTRA_STATE_ATTRIBUTES
|
||||||
ATTR_PROFILE_FAN_SPEED_BOOST["description"]: self._fan_speed_boost,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
|
||||||
"""Call to update."""
|
|
||||||
self.async_on_remove(
|
|
||||||
async_dispatcher_connect(
|
|
||||||
self.hass, SIGNAL_VALLOX_STATE_UPDATE, self._update_callback
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _update_callback(self) -> None:
|
|
||||||
"""Call update method."""
|
|
||||||
self.async_schedule_update_ha_state(True)
|
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
|
||||||
"""Fetch state from the device."""
|
|
||||||
try:
|
|
||||||
# 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"]
|
|
||||||
)
|
|
||||||
fan_speed_away = self._state_proxy.fetch_metric(
|
|
||||||
ATTR_PROFILE_FAN_SPEED_AWAY["metric_key"]
|
|
||||||
)
|
|
||||||
fan_speed_boost = self._state_proxy.fetch_metric(
|
|
||||||
ATTR_PROFILE_FAN_SPEED_BOOST["metric_key"]
|
|
||||||
)
|
|
||||||
|
|
||||||
except (OSError, KeyError, TypeError) as err:
|
|
||||||
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
|
|
||||||
)
|
|
||||||
self._fan_speed_away = (
|
|
||||||
int(fan_speed_away) if isinstance(fan_speed_away, (int, float)) else None
|
|
||||||
)
|
|
||||||
self._fan_speed_boost = (
|
|
||||||
int(fan_speed_boost) if isinstance(fan_speed_boost, (int, float)) else None
|
|
||||||
)
|
|
||||||
|
|
||||||
self._attr_available = True
|
|
||||||
|
|
||||||
async def _async_set_preset_mode_internal(self, preset_mode: str) -> bool:
|
async def _async_set_preset_mode_internal(self, preset_mode: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Set new preset mode.
|
Set new preset mode.
|
||||||
@ -201,7 +163,7 @@ class ValloxFan(FanEntity):
|
|||||||
if update_needed:
|
if update_needed:
|
||||||
# This state change affects other entities like sensors. Force an immediate update that
|
# This state change affects other entities like sensors. Force an immediate update that
|
||||||
# can be observed by all parties involved.
|
# can be observed by all parties involved.
|
||||||
await self._state_proxy.async_update()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
async def async_turn_on(
|
async def async_turn_on(
|
||||||
self,
|
self,
|
||||||
@ -211,7 +173,7 @@ class ValloxFan(FanEntity):
|
|||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
_LOGGER.debug("Turn on: %s", speed)
|
_LOGGER.debug("Turn on")
|
||||||
|
|
||||||
update_needed = False
|
update_needed = False
|
||||||
|
|
||||||
@ -231,7 +193,7 @@ class ValloxFan(FanEntity):
|
|||||||
if update_needed:
|
if update_needed:
|
||||||
# This state change affects other entities like sensors. Force an immediate update that
|
# This state change affects other entities like sensors. Force an immediate update that
|
||||||
# can be observed by all parties involved.
|
# can be observed by all parties involved.
|
||||||
await self._state_proxy.async_update()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
@ -246,4 +208,4 @@ class ValloxFan(FanEntity):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Same as for turn_on method.
|
# Same as for turn_on method.
|
||||||
await self._state_proxy.async_update()
|
await self.coordinator.async_request_refresh()
|
||||||
|
@ -19,143 +19,91 @@ from homeassistant.const import (
|
|||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import ValloxStateProxy
|
from . import ValloxDataUpdateCoordinator
|
||||||
from .const import (
|
from .const import DOMAIN, METRIC_KEY_MODE, MODE_ON, VALLOX_PROFILE_TO_STR_REPORTABLE
|
||||||
DOMAIN,
|
|
||||||
METRIC_KEY_MODE,
|
|
||||||
MODE_ON,
|
|
||||||
SIGNAL_VALLOX_STATE_UPDATE,
|
|
||||||
VALLOX_PROFILE_TO_STR_REPORTABLE,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ValloxSensor(SensorEntity):
|
class ValloxSensor(CoordinatorEntity, SensorEntity):
|
||||||
"""Representation of a Vallox sensor."""
|
"""Representation of a Vallox sensor."""
|
||||||
|
|
||||||
_attr_should_poll = False
|
|
||||||
entity_description: ValloxSensorEntityDescription
|
entity_description: ValloxSensorEntityDescription
|
||||||
|
coordinator: ValloxDataUpdateCoordinator
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
state_proxy: ValloxStateProxy,
|
coordinator: ValloxDataUpdateCoordinator,
|
||||||
description: ValloxSensorEntityDescription,
|
description: ValloxSensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Vallox sensor."""
|
"""Initialize the Vallox sensor."""
|
||||||
self._state_proxy = state_proxy
|
super().__init__(coordinator)
|
||||||
|
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
|
||||||
self._attr_name = f"{name} {description.name}"
|
self._attr_name = f"{name} {description.name}"
|
||||||
self._attr_available = False
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
@property
|
||||||
"""Call to update."""
|
def native_value(self) -> StateType:
|
||||||
self.async_on_remove(
|
"""Return the value reported by the sensor."""
|
||||||
async_dispatcher_connect(
|
|
||||||
self.hass, SIGNAL_VALLOX_STATE_UPDATE, self._update_callback
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _update_callback(self) -> None:
|
|
||||||
"""Call update method."""
|
|
||||||
self.async_schedule_update_ha_state(True)
|
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
|
||||||
"""Fetch state from the ventilation unit."""
|
|
||||||
if (metric_key := self.entity_description.metric_key) is None:
|
if (metric_key := self.entity_description.metric_key) is None:
|
||||||
self._attr_available = False
|
_LOGGER.debug("Error updating sensor. Empty metric key")
|
||||||
_LOGGER.error("Error updating sensor. Empty metric key")
|
return None
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
return self.coordinator.data.get_metric(metric_key)
|
||||||
self._attr_native_value = self._state_proxy.fetch_metric(metric_key)
|
|
||||||
|
|
||||||
except (OSError, KeyError, TypeError) as err:
|
|
||||||
self._attr_available = False
|
|
||||||
_LOGGER.error("Error updating sensor: %s", err)
|
|
||||||
return
|
|
||||||
|
|
||||||
self._attr_available = True
|
|
||||||
|
|
||||||
|
|
||||||
class ValloxProfileSensor(ValloxSensor):
|
class ValloxProfileSensor(ValloxSensor):
|
||||||
"""Child class for profile reporting."""
|
"""Child class for profile reporting."""
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
@property
|
||||||
"""Fetch state from the ventilation unit."""
|
def native_value(self) -> StateType:
|
||||||
try:
|
"""Return the value reported by the sensor."""
|
||||||
vallox_profile = self._state_proxy.get_profile()
|
vallox_profile = self.coordinator.data.profile
|
||||||
|
return VALLOX_PROFILE_TO_STR_REPORTABLE.get(vallox_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
|
|
||||||
|
|
||||||
|
|
||||||
# There seems to be a quirk with respect to the fan speed reporting. The device keeps on reporting
|
# There is a quirk with respect to the fan speed reporting. The device keeps on reporting the last
|
||||||
# the last valid fan speed from when the device was in regular operation mode, even if it left that
|
# valid fan speed from when the device was in regular operation mode, even if it left that state and
|
||||||
# state and has been shut off in the meantime.
|
# has been shut off in the meantime.
|
||||||
#
|
#
|
||||||
# Therefore, first query the overall state of the device, and report zero percent fan speed in case
|
# Therefore, first query the overall state of the device, and report zero percent fan speed in case
|
||||||
# it is not in regular operation mode.
|
# it is not in regular operation mode.
|
||||||
class ValloxFanSpeedSensor(ValloxSensor):
|
class ValloxFanSpeedSensor(ValloxSensor):
|
||||||
"""Child class for fan speed reporting."""
|
"""Child class for fan speed reporting."""
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
@property
|
||||||
"""Fetch state from the ventilation unit."""
|
def native_value(self) -> StateType:
|
||||||
try:
|
"""Return the value reported by the sensor."""
|
||||||
fan_on = self._state_proxy.fetch_metric(METRIC_KEY_MODE) == MODE_ON
|
fan_is_on = self.coordinator.data.get_metric(METRIC_KEY_MODE) == MODE_ON
|
||||||
|
return super().native_value if fan_is_on else 0
|
||||||
except (OSError, KeyError, TypeError) as err:
|
|
||||||
self._attr_available = False
|
|
||||||
_LOGGER.error("Error updating sensor: %s", err)
|
|
||||||
return
|
|
||||||
|
|
||||||
if fan_on:
|
|
||||||
await super().async_update()
|
|
||||||
else:
|
|
||||||
# Report zero percent otherwise.
|
|
||||||
self._attr_native_value = 0
|
|
||||||
self._attr_available = True
|
|
||||||
|
|
||||||
|
|
||||||
class ValloxFilterRemainingSensor(ValloxSensor):
|
class ValloxFilterRemainingSensor(ValloxSensor):
|
||||||
"""Child class for filter remaining time reporting."""
|
"""Child class for filter remaining time reporting."""
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
@property
|
||||||
"""Fetch state from the ventilation unit."""
|
def native_value(self) -> StateType:
|
||||||
await super().async_update()
|
"""Return the value reported by the sensor."""
|
||||||
|
super_native_value = super().native_value
|
||||||
|
|
||||||
# Check if the update in the super call was a success.
|
if not isinstance(super_native_value, (int, float)):
|
||||||
if not self._attr_available:
|
_LOGGER.debug("Value has unexpected type: %s", type(super_native_value))
|
||||||
return
|
return None
|
||||||
|
|
||||||
if not isinstance(self._attr_native_value, (int, float)):
|
|
||||||
self._attr_available = False
|
|
||||||
_LOGGER.error(
|
|
||||||
"Value has unexpected type: %s", type(self._attr_native_value)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Since only a delta of days is received from the device, fix the time so the timestamp does
|
# Since only a delta of days is received from the device, fix the time so the timestamp does
|
||||||
# not change with every update.
|
# not change with every update.
|
||||||
days_remaining = float(self._attr_native_value)
|
days_remaining = float(super_native_value)
|
||||||
days_remaining_delta = timedelta(days=days_remaining)
|
days_remaining_delta = timedelta(days=days_remaining)
|
||||||
now = datetime.utcnow().replace(hour=13, minute=0, second=0, microsecond=0)
|
now = datetime.utcnow().replace(hour=13, minute=0, second=0, microsecond=0)
|
||||||
|
|
||||||
self._attr_native_value = (now + days_remaining_delta).isoformat()
|
return (now + days_remaining_delta).isoformat()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -259,12 +207,11 @@ async def async_setup_platform(
|
|||||||
return
|
return
|
||||||
|
|
||||||
name = hass.data[DOMAIN]["name"]
|
name = hass.data[DOMAIN]["name"]
|
||||||
state_proxy = hass.data[DOMAIN]["state_proxy"]
|
coordinator = hass.data[DOMAIN]["coordinator"]
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[
|
||||||
description.sensor_type(name, state_proxy, description)
|
description.sensor_type(name, coordinator, description)
|
||||||
for description in SENSORS
|
for description in SENSORS
|
||||||
],
|
]
|
||||||
update_before_add=False,
|
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user