"""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