-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use DataUpdateCoordinator in Vallox #56966
Changes from all commits
b272d9c
6a3575e
562fc99
49848f8
a7e0473
4defc8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
"""Support for Vallox ventilation units.""" | ||
from __future__ import annotations | ||
|
||
from datetime import datetime | ||
from dataclasses import dataclass, field | ||
import ipaddress | ||
import logging | ||
from typing import Any | ||
|
@@ -11,13 +11,12 @@ | |
from vallox_websocket_api.exceptions import ValloxApiException | ||
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.helpers import config_validation as cv | ||
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.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||
|
||
from .const import ( | ||
DEFAULT_FAN_SPEED_AWAY, | ||
|
@@ -28,8 +27,7 @@ | |
METRIC_KEY_PROFILE_FAN_SPEED_AWAY, | ||
METRIC_KEY_PROFILE_FAN_SPEED_BOOST, | ||
METRIC_KEY_PROFILE_FAN_SPEED_HOME, | ||
SIGNAL_VALLOX_STATE_UPDATE, | ||
STATE_PROXY_SCAN_INTERVAL, | ||
STATE_SCAN_INTERVAL, | ||
STR_TO_VALLOX_PROFILE_SETTABLE, | ||
) | ||
|
||
|
@@ -91,109 +89,115 @@ | |
} | ||
|
||
|
||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: | ||
"""Set up the client and boot the platforms.""" | ||
conf = config[DOMAIN] | ||
host = conf.get(CONF_HOST) | ||
name = conf.get(CONF_NAME) | ||
|
||
client = Vallox(host) | ||
state_proxy = ValloxStateProxy(hass, client) | ||
service_handler = ValloxServiceHandler(client, state_proxy) | ||
@dataclass | ||
class ValloxState: | ||
"""Describes the current state of the unit.""" | ||
|
||
hass.data[DOMAIN] = {"client": client, "state_proxy": state_proxy, "name": name} | ||
metric_cache: dict[str, Any] = field(default_factory=dict) | ||
profile: VALLOX_PROFILE = VALLOX_PROFILE.NONE | ||
|
||
for vallox_service, method in SERVICE_TO_METHOD.items(): | ||
schema = method["schema"] | ||
hass.services.async_register( | ||
DOMAIN, vallox_service, service_handler.async_handle, schema=schema | ||
) | ||
|
||
# The vallox hardware expects quite strict timings for websocket requests. Timings that machines | ||
# 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 | ||
# called with update_before_add=False to intentionally delay the first request, increasing | ||
# 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, "fan", DOMAIN, {}, config)) | ||
|
||
async_track_time_interval(hass, state_proxy.async_update, STATE_PROXY_SCAN_INTERVAL) | ||
|
||
return True | ||
|
||
|
||
class ValloxStateProxy: | ||
"""Helper class to reduce websocket API calls.""" | ||
|
||
def __init__(self, hass: HomeAssistant, client: Vallox) -> None: | ||
"""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: | ||
def get_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}") | ||
_LOGGER.debug("Metric key invalid: %s", metric_key) | ||
|
||
if (value := self._metric_cache[metric_key]) is None: | ||
if (value := self.metric_cache.get(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)}" | ||
_LOGGER.debug( | ||
"Return value of metric %s has unexpected type %s", | ||
metric_key, | ||
type(value), | ||
) | ||
return None | ||
|
||
return value | ||
|
||
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.") | ||
class ValloxDataUpdateCoordinator(DataUpdateCoordinator): | ||
"""The DataUpdateCoordinator for Vallox.""" | ||
|
||
data: ValloxState | ||
|
||
|
||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: | ||
"""Set up the client and boot the platforms.""" | ||
conf = config[DOMAIN] | ||
host = conf.get(CONF_HOST) | ||
name = conf.get(CONF_NAME) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Host is required and name has a default value. Please use |
||
|
||
return self._profile | ||
client = Vallox(host) | ||
|
||
async def async_update(self, time: datetime | None = None) -> None: | ||
async def async_update_data() -> ValloxState: | ||
"""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() | ||
metric_cache = await client.fetch_metrics() | ||
profile = await client.get_profile() | ||
|
||
except (OSError, ValloxApiException) as err: | ||
self._valid = False | ||
_LOGGER.error("Error during state cache update: %s", err) | ||
return | ||
raise UpdateFailed("Error during state cache update") from err | ||
|
||
return ValloxState(metric_cache, profile) | ||
|
||
self._valid = True | ||
async_dispatcher_send(self._hass, SIGNAL_VALLOX_STATE_UPDATE) | ||
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(): | ||
schema = method["schema"] | ||
hass.services.async_register( | ||
DOMAIN, vallox_service, service_handler.async_handle, schema=schema | ||
) | ||
|
||
hass.data[DOMAIN] = {"client": client, "coordinator": coordinator, "name": name} | ||
|
||
async def _async_load_platform_delayed(*_: Any) -> None: | ||
await coordinator.async_refresh() | ||
hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) | ||
hass.async_create_task(async_load_platform(hass, "fan", DOMAIN, {}, config)) | ||
|
||
# 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 | ||
# of Home Asssistant. | ||
# | ||
# Hence, wait for the started event before doing a first data refresh and loading the platforms, | ||
# because it usually means the system is less busy after the event and can now meet the | ||
# websocket timing requirements. | ||
hass.bus.async_listen_once( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the future by converting this to a config entry, you can do |
||
EVENT_HOMEASSISTANT_STARTED, _async_load_platform_delayed | ||
) | ||
|
||
return True | ||
|
||
|
||
class ValloxServiceHandler: | ||
"""Services implementation.""" | ||
|
||
def __init__(self, client: Vallox, state_proxy: ValloxStateProxy) -> None: | ||
def __init__( | ||
self, client: Vallox, coordinator: DataUpdateCoordinator[ValloxState] | ||
bdraco marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) -> None: | ||
"""Initialize the proxy.""" | ||
self._client = client | ||
self._state_proxy = state_proxy | ||
self._coordinator = coordinator | ||
|
||
async def async_set_profile(self, profile: str = "Home") -> bool: | ||
"""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" | ||
"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: | ||
|
@@ -269,4 +273,4 @@ async def async_handle(self, call: ServiceCall) -> None: | |
# 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() | ||
await self._coordinator.async_request_refresh() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't want side effects like logging in entity state properties.
get_metric
is called fromis_on
eg.