Implement bridge-backed Home Assistant integration
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"""Data coordinator for MB-Secure Bridge API v1."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, override
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .api import (
|
||||
BridgeAuthenticationError,
|
||||
BridgeClient,
|
||||
BridgeConnectionError,
|
||||
BridgeResponseError,
|
||||
BridgeVersionError,
|
||||
)
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
from .models import BridgeDataError, BridgeInfo, BridgeSnapshot
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MBSecureCoordinator(DataUpdateCoordinator[BridgeSnapshot]):
|
||||
"""Coordinate snapshots and push events from one local bridge."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry[MBSecureCoordinator],
|
||||
client: BridgeClient,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger=_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
always_update=False,
|
||||
)
|
||||
self.client = client
|
||||
self._entry: ConfigEntry[Any] = config_entry
|
||||
self.info: BridgeInfo | None = None
|
||||
self._event_task: asyncio.Task[None] | None = None
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> BridgeSnapshot:
|
||||
"""Fetch a full snapshot for startup and periodic reconciliation."""
|
||||
try:
|
||||
info = await self.client.async_get_info()
|
||||
if info.bridge_id is None:
|
||||
raise BridgeDataError("bridge installation ID is missing")
|
||||
if info.bridge_id != self._entry.unique_id:
|
||||
raise BridgeDataError("bridge installation ID has changed")
|
||||
self.info = info
|
||||
return await self.client.async_get_snapshot()
|
||||
except BridgeAuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed("Bridge authentication failed") from err
|
||||
except (
|
||||
BridgeConnectionError,
|
||||
BridgeResponseError,
|
||||
BridgeVersionError,
|
||||
BridgeDataError,
|
||||
) as err:
|
||||
raise UpdateFailed(f"Bridge update failed: {err}") from err
|
||||
|
||||
def async_start_event_listener(self) -> None:
|
||||
"""Start the config-entry-managed event listener."""
|
||||
if self._event_task is not None:
|
||||
return
|
||||
self._event_task = self._entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._async_event_loop(),
|
||||
f"{DOMAIN}-events-{self._entry.entry_id}",
|
||||
)
|
||||
|
||||
async def async_shutdown(self) -> None:
|
||||
"""Stop background event handling."""
|
||||
if self._event_task is None:
|
||||
return
|
||||
self._event_task.cancel()
|
||||
await asyncio.gather(self._event_task, return_exceptions=True)
|
||||
self._event_task = None
|
||||
|
||||
async def _async_event_loop(self) -> None:
|
||||
"""Reconnect to events and reconcile gaps with a snapshot."""
|
||||
reconnect_delay = 1
|
||||
while True:
|
||||
try:
|
||||
async for event in self.client.async_events():
|
||||
reconnect_delay = 1
|
||||
updated = self.data.apply_event(event)
|
||||
if updated is None:
|
||||
await self.async_request_refresh()
|
||||
else:
|
||||
self.async_set_updated_data(updated)
|
||||
except BridgeAuthenticationError:
|
||||
self._entry.async_start_reauth(self.hass)
|
||||
return
|
||||
except (BridgeConnectionError, BridgeResponseError, BridgeDataError):
|
||||
_LOGGER.debug(
|
||||
"Bridge event stream disconnected; retrying in %s seconds",
|
||||
reconnect_delay,
|
||||
)
|
||||
await asyncio.sleep(reconnect_delay)
|
||||
reconnect_delay = min(reconnect_delay * 2, 60)
|
||||
Reference in New Issue
Block a user