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
+151
View File
@@ -0,0 +1,151 @@
"""Shared entity support for MB-Secure."""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import MBSecureCoordinator
from .models import BridgeObject
type EntityFactory = Callable[[str], MBSecureEntity]
async def _async_remove_entity(entity: MBSecureEntity) -> None:
"""Remove a topology entity from runtime and the entity registry."""
entity_id = entity.entity_id
await entity.async_remove(force_remove=True)
if entity_id is not None:
er.async_get(entity.coordinator.hass).async_remove(entity_id)
class MBSecureEntity(CoordinatorEntity[MBSecureCoordinator]):
"""Base class for an entity backed by one normalized bridge object."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: MBSecureCoordinator,
bridge_id: str,
collection_name: str,
object_id: str,
) -> None:
"""Initialize an entity with stable technical identifiers."""
super().__init__(coordinator)
self._bridge_id = bridge_id
self._collection_name = collection_name
self._object_id = object_id
self._attr_unique_id = f"{bridge_id}:{collection_name}:{object_id}"
@property
def bridge_object(self) -> BridgeObject | None:
"""Return the latest normalized object, if it still exists."""
return getattr(self.coordinator.data, self._collection_name).get(
self._object_id
)
@property
def available(self) -> bool:
"""Report availability from both transport and topology state."""
return super().available and self.bridge_object is not None
@property
def name(self) -> str | None:
"""Return a display name without using it for identity."""
item = self.bridge_object
if self._collection_name in {"areas", "controllers", "modules"}:
return None
return item.name if item is not None and item.name else self._object_id
@property
def device_info(self) -> DeviceInfo | None:
"""Attach the entity to the normalized device hierarchy."""
item = self.bridge_object
if item is None:
return None
return _device_info(self._bridge_id, self._collection_name, item)
def _identifier(bridge_id: str, object_type: str, object_id: str) -> tuple[str, str]:
return (DOMAIN, f"{bridge_id}:{object_type}:{object_id}")
def _device_info(
bridge_id: str,
collection_name: str,
item: BridgeObject,
) -> DeviceInfo:
object_type = collection_name.removesuffix("s")
if collection_name == "controllers":
return DeviceInfo(
identifiers={_identifier(bridge_id, object_type, item.id)},
name=item.name or item.id,
model="Controller",
)
if collection_name in {"areas", "modules"}:
return DeviceInfo(
identifiers={_identifier(bridge_id, object_type, item.id)},
name=item.name or item.id,
model=object_type.title(),
)
parent_type = "controller"
parent_id = item.controller_id
if item.module_id is not None:
parent_type = "module"
parent_id = item.module_id
elif item.area_id is not None:
parent_type = "area"
parent_id = item.area_id
assert parent_id is not None
return DeviceInfo(identifiers={_identifier(bridge_id, parent_type, parent_id)})
def async_setup_dynamic_entities(
entry: ConfigEntry[Any],
async_add_entities: AddEntitiesCallback,
*,
collection_name: str,
capability: str | frozenset[str],
factory: EntityFactory,
) -> None:
"""Keep a capability-filtered platform synchronized with topology changes."""
coordinator: MBSecureCoordinator = entry.runtime_data
entities: dict[str, MBSecureEntity] = {}
@callback
def sync_entities() -> None:
collection: dict[str, BridgeObject] = getattr(coordinator.data, collection_name)
required_capabilities = (
frozenset({capability}) if isinstance(capability, str) else capability
)
desired = {
object_id
for object_id, item in collection.items()
if required_capabilities & item.capabilities
}
added = [factory(object_id) for object_id in desired - entities.keys()]
for entity in added:
entities[entity._object_id] = entity
if added:
async_add_entities(added)
for object_id in entities.keys() - desired:
entity = entities.pop(object_id)
entry.async_create_background_task(
coordinator.hass,
_async_remove_entity(entity),
f"{DOMAIN}-remove-{entity.unique_id}",
)
sync_entities()
entry.async_on_unload(coordinator.async_add_listener(sync_entities))