core/homeassistant/components/overkiz/coordinator.py

191 lines
6.6 KiB
Python

"""Helpers to help coordinate updates."""
from __future__ import annotations
from collections.abc import Callable
from datetime import timedelta
import json
import logging
from typing import Any, cast
from aiohttp import ServerDisconnectedError
from pyoverkiz.client import OverkizClient
from pyoverkiz.enums import EventName, ExecutionState
from pyoverkiz.exceptions import (
BadCredentialsException,
MaintenanceException,
NotAuthenticatedException,
TooManyRequestsException,
)
from pyoverkiz.models import DataType, Device, Place, State
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, UPDATE_INTERVAL
DATA_TYPE_TO_PYTHON: dict[DataType, Callable[[DataType], Any]] = {
DataType.INTEGER: int,
DataType.DATE: int,
DataType.STRING: str,
DataType.FLOAT: float,
DataType.BOOLEAN: bool,
DataType.JSON_ARRAY: json.loads,
DataType.JSON_OBJECT: json.loads,
}
_LOGGER = logging.getLogger(__name__)
class OverkizDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from Overkiz platform."""
def __init__(
self,
hass: HomeAssistant,
logger: logging.Logger,
*,
name: str,
client: OverkizClient,
devices: list[Device],
places: Place,
update_interval: timedelta | None = None,
config_entry_id: str,
) -> None:
"""Initialize global data updater."""
super().__init__(
hass,
logger,
name=name,
update_interval=update_interval,
)
self.data = {}
self.client = client
self.devices: dict[str, Device] = {d.device_url: d for d in devices}
self.is_stateless = all(
device.device_url.startswith("rts://")
or device.device_url.startswith("internal://")
for device in devices
)
self.executions: dict[str, dict[str, str]] = {}
self.areas = self.places_to_area(places)
self._config_entry_id = config_entry_id
async def _async_update_data(self) -> dict[str, Device]:
"""Fetch Overkiz data via event listener."""
try:
events = await self.client.fetch_events()
except BadCredentialsException as exception:
raise UpdateFailed("Invalid authentication") from exception
except TooManyRequestsException as exception:
raise UpdateFailed("Too many requests, try again later.") from exception
except MaintenanceException as exception:
raise UpdateFailed("Server is down for maintenance.") from exception
except TimeoutError as exception:
raise UpdateFailed("Failed to connect.") from exception
except (ServerDisconnectedError, NotAuthenticatedException):
self.executions = {}
# During the relogin, similar exceptions can be thrown.
try:
await self.client.login()
self.devices = await self._get_devices()
except BadCredentialsException as exception:
raise UpdateFailed("Invalid authentication") from exception
except TooManyRequestsException as exception:
raise UpdateFailed("Too many requests, try again later.") from exception
return self.devices
for event in events:
_LOGGER.debug(event)
if event.name == EventName.DEVICE_AVAILABLE:
self.devices[event.device_url].available = True
elif event.name in [
EventName.DEVICE_UNAVAILABLE,
EventName.DEVICE_DISABLED,
]:
self.devices[event.device_url].available = False
elif event.name in [
EventName.DEVICE_CREATED,
EventName.DEVICE_UPDATED,
]:
self.hass.async_create_task(
self.hass.config_entries.async_reload(self._config_entry_id)
)
elif event.name == EventName.DEVICE_REMOVED:
base_device_url, *_ = event.device_url.split("#")
registry = dr.async_get(self.hass)
if registered_device := registry.async_get_device(
{(DOMAIN, base_device_url)}
):
registry.async_remove_device(registered_device.id)
del self.devices[event.device_url]
elif event.name == EventName.DEVICE_STATE_CHANGED:
for state in event.device_states:
device = self.devices[event.device_url]
if state.name not in device.states:
device.states[state.name] = state
device.states[state.name].value = self._get_state(state)
elif event.name == EventName.EXECUTION_REGISTERED:
if event.exec_id not in self.executions:
self.executions[event.exec_id] = {}
if not self.is_stateless:
self.update_interval = timedelta(seconds=1)
elif (
event.name == EventName.EXECUTION_STATE_CHANGED
and event.exec_id in self.executions
and event.new_state in [ExecutionState.COMPLETED, ExecutionState.FAILED]
):
del self.executions[event.exec_id]
if not self.executions:
self.update_interval = UPDATE_INTERVAL
return self.devices
async def _get_devices(self) -> dict[str, Device]:
"""Fetch devices."""
_LOGGER.debug("Fetching all devices and state via /setup/devices")
return {d.device_url: d for d in await self.client.get_devices(refresh=True)}
@staticmethod
def _get_state(
state: State,
) -> dict[Any, Any] | list[Any] | float | int | bool | str | None:
"""Cast string value to the right type."""
data_type = DataType(state.type)
if data_type == DataType.NONE:
return cast(None, state.value)
cast_to_python = DATA_TYPE_TO_PYTHON[data_type]
value = cast_to_python(state.value)
assert isinstance(value, (str, float, int, bool))
return value
def places_to_area(self, place: Place) -> dict[str, str]:
"""Convert places with sub_places to a flat dictionary."""
areas = {}
if isinstance(place, Place):
areas[place.oid] = place.label
if isinstance(place.sub_places, list):
for sub_place in place.sub_places:
areas.update(self.places_to_area(sub_place))
return areas