Implement bridge-backed Home Assistant integration

This commit is contained in:
2026-09-04 12:57:32 +02:00
parent 42050010b2
commit 9766f7f9b2
25 changed files with 2154 additions and 7 deletions
+110
View File
@@ -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)