"""Datenkoordinator fuer genau ein ninstrip-Geraet.""" from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable from dataclasses import dataclass from datetime import timedelta import logging from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .api import ( NinstripAuthError, NinstripBusyError, NinstripClient, NinstripError, NinstripRequestError, ) from .const import CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, DOMAIN, MAX_POLL_FAILURES _LOGGER = logging.getLogger(__name__) type NinstripConfigEntry = ConfigEntry[NinstripCoordinator] @dataclass(frozen=True, slots=True) class NinstripData: """Letzter bekannter Stand eines Geraets.""" state: dict[str, Any] info: dict[str, Any] presets: dict[str, Any] class NinstripCoordinator(DataUpdateCoordinator[NinstripData]): """Fragt ein Geraet ab; jede Anfrage dorthin laeuft durch `lock`.""" config_entry: NinstripConfigEntry def __init__( self, hass: HomeAssistant, entry: NinstripConfigEntry, client: NinstripClient ) -> None: super().__init__( hass, _LOGGER, config_entry=entry, name=f"{DOMAIN} {client.host}", update_interval=timedelta( seconds=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) ), ) self.client = client # Der ESP32-C3 hat einen Kern, den sich WLAN-Stack und LED-Ausgabe # teilen. Mehrere gleichzeitige Anfragen bringen nichts ausser Last. self.lock = asyncio.Lock() self.ota_in_progress = False self._consecutive_poll_failures = 0 async def _async_update_data(self) -> NinstripData: if self.ota_in_progress and self.data is not None: return self.data try: async with self.lock: state = await self.client.get_state() info = await self.client.get_info() presets = await self.client.get_presets() except NinstripAuthError as err: raise ConfigEntryAuthFailed(f"{self.client.host}: Token abgelehnt") from err except NinstripError as err: self._consecutive_poll_failures += 1 if self.data is not None and self._consecutive_poll_failures < MAX_POLL_FAILURES: _LOGGER.debug( "%s: Abfrage fehlgeschlagen (%d/%d), behalte den letzten Stand: %s", self.client.host, self._consecutive_poll_failures, MAX_POLL_FAILURES, err, ) return self.data raise UpdateFailed(f"{self.client.host}: {err}") from err self._consecutive_poll_failures = 0 return NinstripData(state, info, presets) async def async_call[T](self, action: Callable[[NinstripClient], Awaitable[T]]) -> T: """Fuehrt eine Anfrage unter der Sperre aus und uebersetzt Fehler fuer die Oberflaeche.""" try: async with self.lock: return await action(self.client) except NinstripAuthError as err: self.config_entry.async_start_reauth(self.hass) raise HomeAssistantError( translation_domain=DOMAIN, translation_key="auth_failed" ) from err except NinstripBusyError as err: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="device_busy" ) from err except NinstripRequestError as err: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="request_failed", translation_placeholders={"message": err.message}, ) from err except NinstripError as err: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="cannot_connect" ) from err async def async_set_state(self, **fields: Any) -> None: """Schreibt und uebernimmt die vollstaendige Antwort des Geraets direkt.""" state = await self.async_call(lambda client: client.set_state(**fields)) self._consecutive_poll_failures = 0 self.async_set_updated_data(NinstripData(state, self.data.info, self.data.presets)) async def async_after_change(self) -> None: """Nach einem Eingriff, der den ganzen Stand aendern kann (Szene, Segmente).""" await self.async_request_refresh()