118 lines
4.4 KiB
Python
118 lines
4.4 KiB
Python
"""Data coordinator for one ninlight lamp."""
|
|
|
|
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 (
|
|
NinlightAuthError,
|
|
NinlightBusyError,
|
|
NinlightClient,
|
|
NinlightError,
|
|
NinlightRequestError,
|
|
)
|
|
from .const import CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, DOMAIN, MAX_POLL_FAILURES
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
type NinlightConfigEntry = ConfigEntry[NinlightCoordinator]
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class NinlightData:
|
|
"""Last known lamp state and system info."""
|
|
|
|
state: dict[str, Any]
|
|
info: dict[str, Any]
|
|
|
|
|
|
class NinlightCoordinator(DataUpdateCoordinator[NinlightData]):
|
|
"""Polls one lamp; every request to the lamp goes through `lock`."""
|
|
|
|
config_entry: NinlightConfigEntry
|
|
|
|
def __init__(
|
|
self, hass: HomeAssistant, entry: NinlightConfigEntry, client: NinlightClient
|
|
) -> 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
|
|
# The W600 web server has a listen backlog of 4: never talk to one lamp concurrently.
|
|
self.lock = asyncio.Lock()
|
|
# While a firmware update runs, the update entity watches the lamp itself.
|
|
self.ota_in_progress = False
|
|
# Consecutive poll failures; a short outage should not flip the lamp unavailable.
|
|
self._consecutive_poll_failures = 0
|
|
|
|
async def _async_update_data(self) -> NinlightData:
|
|
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()
|
|
except NinlightAuthError as err:
|
|
raise ConfigEntryAuthFailed(f"{self.client.host}: token rejected") from err
|
|
except NinlightError as err:
|
|
self._consecutive_poll_failures += 1
|
|
if self.data is not None and self._consecutive_poll_failures < MAX_POLL_FAILURES:
|
|
_LOGGER.debug(
|
|
"%s: poll failed (%d/%d), keeping last known state: %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 NinlightData(state, info)
|
|
|
|
async def async_call[T](self, action: Callable[[NinlightClient], Awaitable[T]]) -> T:
|
|
"""Run one request under the lock and translate lamp errors for the UI."""
|
|
try:
|
|
async with self.lock:
|
|
return await action(self.client)
|
|
except NinlightAuthError as err:
|
|
self.config_entry.async_start_reauth(self.hass)
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN, translation_key="auth_failed"
|
|
) from err
|
|
except NinlightBusyError as err:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN, translation_key="update_busy"
|
|
) from err
|
|
except NinlightRequestError as err:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="request_failed",
|
|
translation_placeholders={"message": err.message},
|
|
) from err
|
|
except NinlightError as err:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN, translation_key="cannot_connect"
|
|
) from err
|
|
|
|
async def async_set_state(self, **fields: Any) -> None:
|
|
"""Write and adopt the lamp's complete answer directly - no debounced refresh."""
|
|
state = await self.async_call(lambda client: client.set_state(**fields))
|
|
self._consecutive_poll_failures = 0
|
|
self.async_set_updated_data(NinlightData(state, self.data.info))
|