193 lines
6.1 KiB
Python
193 lines
6.1 KiB
Python
"""Tests for MB-Secure devices and entities."""
|
|
|
|
from copy import deepcopy
|
|
from unittest.mock import patch
|
|
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import device_registry as dr
|
|
from homeassistant.helpers import entity_registry as er
|
|
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
|
|
|
from custom_components.mb_secure.const import CONF_TOKEN, DOMAIN
|
|
from custom_components.mb_secure.coordinator import MBSecureCoordinator
|
|
from custom_components.mb_secure.models import BridgeInfo, BridgeSnapshot
|
|
|
|
ENTRY_DATA = {
|
|
"host": "bridge.local",
|
|
"port": 8099,
|
|
CONF_TOKEN: "x" * 32,
|
|
}
|
|
|
|
|
|
def snapshot_payload() -> dict[str, object]:
|
|
"""Return a capability-complete normalized test snapshot."""
|
|
return {
|
|
"revision": 1,
|
|
"controllers": [
|
|
{
|
|
"id": "controller-1",
|
|
"name": "Main Controller",
|
|
"state": "online",
|
|
"capabilities": ["snapshot"],
|
|
}
|
|
],
|
|
"areas": [
|
|
{
|
|
"id": "area-1",
|
|
"controller_id": "controller-1",
|
|
"name": "Ground Floor",
|
|
"state": "disarmed",
|
|
"capabilities": ["arm_away", "disarm"],
|
|
}
|
|
],
|
|
"points": [
|
|
{
|
|
"id": "point-1",
|
|
"controller_id": "controller-1",
|
|
"area_id": "area-1",
|
|
"module_id": None,
|
|
"name": "Front Door",
|
|
"state": "closed",
|
|
"capabilities": ["binary_state"],
|
|
},
|
|
{
|
|
"id": "point-without-capability",
|
|
"controller_id": "controller-1",
|
|
"area_id": "area-1",
|
|
"module_id": None,
|
|
"name": "Unsupported Point",
|
|
"state": "unknown",
|
|
"capabilities": [],
|
|
},
|
|
],
|
|
"modules": [
|
|
{
|
|
"id": "module-1",
|
|
"controller_id": "controller-1",
|
|
"name": "Expansion Module",
|
|
"state": "online",
|
|
"capabilities": ["status"],
|
|
}
|
|
],
|
|
"outputs": [
|
|
{
|
|
"id": "output-1",
|
|
"controller_id": "controller-1",
|
|
"module_id": "module-1",
|
|
"name": "Indicator",
|
|
"state": "off",
|
|
"capabilities": ["set"],
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
async def setup_entry(hass: HomeAssistant) -> MockConfigEntry:
|
|
"""Set up an entry against mocked neutral Bridge API responses."""
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data=ENTRY_DATA,
|
|
unique_id="bridge-installation-1",
|
|
)
|
|
entry.add_to_hass(hass)
|
|
info = BridgeInfo(
|
|
bridge_id="bridge-installation-1",
|
|
bridge_version="0.1.0",
|
|
api_version=1,
|
|
connected=True,
|
|
capabilities=frozenset({"snapshot", "events", "commands"}),
|
|
)
|
|
snapshot = BridgeSnapshot.from_dict(snapshot_payload())
|
|
with (
|
|
patch(
|
|
"custom_components.mb_secure.api.BridgeClient.async_get_info",
|
|
return_value=info,
|
|
),
|
|
patch(
|
|
"custom_components.mb_secure.api.BridgeClient.async_get_snapshot",
|
|
return_value=snapshot,
|
|
),
|
|
patch.object(MBSecureCoordinator, "async_start_event_listener"),
|
|
):
|
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
return entry
|
|
|
|
|
|
async def test_setup_creates_capability_gated_entities_and_hierarchy(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Only reported capabilities create stable entities and linked devices."""
|
|
entry = await setup_entry(hass)
|
|
registry = er.async_get(hass)
|
|
unique_ids = {
|
|
entity.unique_id
|
|
for entity in er.async_entries_for_config_entry(registry, entry.entry_id)
|
|
}
|
|
assert unique_ids == {
|
|
"bridge-installation-1:areas:area-1",
|
|
"bridge-installation-1:controllers:controller-1",
|
|
"bridge-installation-1:modules:module-1",
|
|
"bridge-installation-1:outputs:output-1",
|
|
"bridge-installation-1:points:point-1",
|
|
}
|
|
|
|
device_registry = dr.async_get(hass)
|
|
controller = device_registry.async_get_device_by_identifier(
|
|
(DOMAIN, "bridge-installation-1:controller:controller-1"),
|
|
entry.entry_id,
|
|
)
|
|
area = device_registry.async_get_device_by_identifier(
|
|
(DOMAIN, "bridge-installation-1:area:area-1"),
|
|
entry.entry_id,
|
|
)
|
|
module = device_registry.async_get_device_by_identifier(
|
|
(DOMAIN, "bridge-installation-1:module:module-1"),
|
|
entry.entry_id,
|
|
)
|
|
assert controller is not None
|
|
assert area is not None and area.via_device_id == controller.id
|
|
assert module is not None and module.via_device_id == controller.id
|
|
|
|
|
|
async def test_snapshot_topology_changes_add_and_remove_entities(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Snapshot reconciliation updates the runtime entity topology."""
|
|
entry = await setup_entry(hass)
|
|
coordinator = entry.runtime_data
|
|
registry = er.async_get(hass)
|
|
old_entry = registry.async_get_entity_id(
|
|
"binary_sensor",
|
|
DOMAIN,
|
|
"bridge-installation-1:points:point-1",
|
|
)
|
|
assert old_entry is not None
|
|
payload = deepcopy(snapshot_payload())
|
|
payload["revision"] = 2
|
|
points = payload["points"]
|
|
assert isinstance(points, list)
|
|
points.pop(0)
|
|
points.append(
|
|
{
|
|
"id": "point-2",
|
|
"controller_id": "controller-1",
|
|
"area_id": "area-1",
|
|
"module_id": None,
|
|
"name": "Back Door",
|
|
"state": "open",
|
|
"capabilities": ["binary_state"],
|
|
}
|
|
)
|
|
|
|
coordinator.async_set_updated_data(BridgeSnapshot.from_dict(payload))
|
|
await hass.async_block_till_done()
|
|
|
|
unique_ids = {
|
|
entity.unique_id
|
|
for entity in er.async_entries_for_config_entry(registry, entry.entry_id)
|
|
}
|
|
assert "bridge-installation-1:points:point-1" not in unique_ids
|
|
assert "bridge-installation-1:points:point-2" in unique_ids
|
|
assert hass.states.get(old_entry) is None
|