diff --git a/homeassistant/components/unifi_access/coordinator.py b/homeassistant/components/unifi_access/coordinator.py index 8809f62f554..5124acdb9e7 100644 --- a/homeassistant/components/unifi_access/coordinator.py +++ b/homeassistant/components/unifi_access/coordinator.py @@ -212,9 +212,6 @@ class UnifiAccessCoordinator(DataUpdateCoordinator[UnifiAccessData]): async def _handle_insights_add(self, msg: WebsocketMessage) -> None: """Handle access insights events (entry/exit).""" insights = cast(InsightsAdd, msg) - door = insights.data.metadata.door - if not door.id: - return event_type = ( "access_granted" if insights.data.result == "ACCESS" else "access_denied" ) @@ -225,7 +222,9 @@ class UnifiAccessCoordinator(DataUpdateCoordinator[UnifiAccessData]): attrs["authentication"] = insights.data.metadata.authentication.display_name if insights.data.result: attrs["result"] = insights.data.result - self._dispatch_door_event(door.id, "access", event_type, attrs) + for door in insights.data.metadata.door: + if door.id: + self._dispatch_door_event(door.id, "access", event_type, attrs) @callback def _dispatch_door_event( diff --git a/homeassistant/components/unifi_access/manifest.json b/homeassistant/components/unifi_access/manifest.json index d04b99962ff..e72174e1869 100644 --- a/homeassistant/components/unifi_access/manifest.json +++ b/homeassistant/components/unifi_access/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["unifi_access_api"], "quality_scale": "bronze", - "requirements": ["py-unifi-access==1.1.0"] + "requirements": ["py-unifi-access==1.1.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index bbe6d00014f..6549d50e2df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1892,7 +1892,7 @@ py-sucks==0.9.11 py-synologydsm-api==2.7.3 # homeassistant.components.unifi_access -py-unifi-access==1.1.0 +py-unifi-access==1.1.3 # homeassistant.components.atome pyAtome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1eeca19b25..0069bbbe024 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1641,7 +1641,7 @@ py-sucks==0.9.11 py-synologydsm-api==2.7.3 # homeassistant.components.unifi_access -py-unifi-access==1.1.0 +py-unifi-access==1.1.3 # homeassistant.components.hdmi_cec pyCEC==0.5.2 diff --git a/tests/components/unifi_access/test_event.py b/tests/components/unifi_access/test_event.py index 86869a9b40b..1ab2d93d83a 100644 --- a/tests/components/unifi_access/test_event.py +++ b/tests/components/unifi_access/test_event.py @@ -154,10 +154,12 @@ async def test_access_event( event_type="access.door.unlock", result=result, metadata=InsightsMetadata( - door=InsightsMetadataEntry( - id=door_id, - display_name="Door", - ), + door=[ + InsightsMetadataEntry( + id=door_id, + display_name="Door", + ) + ], actor=InsightsMetadataEntry( display_name=actor, ), @@ -194,7 +196,7 @@ async def test_insights_no_door_id_ignored( event_type="access.door.unlock", result="ACCESS", metadata=InsightsMetadata( - door=InsightsMetadataEntry(id="", display_name=""), + door=[InsightsMetadataEntry(id="", display_name="")], ), ), ) @@ -234,10 +236,12 @@ async def test_access_event_result_mapping( event_type="access.door.unlock", result=result, metadata=InsightsMetadata( - door=InsightsMetadataEntry( - id="door-001", - display_name="Front Door", - ), + door=[ + InsightsMetadataEntry( + id="door-001", + display_name="Front Door", + ) + ], ), ), ) @@ -254,6 +258,72 @@ async def test_access_event_result_mapping( assert state.state == "2025-01-01T00:00:00.000+00:00" +async def test_insights_empty_door_list_ignored( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test insights event with empty door list is ignored.""" + handlers = _get_ws_handlers(mock_client) + + insights_msg = InsightsAdd( + event="access.logs.insights.add", + data=InsightsAddData( + event_type="access.door.unlock", + result="ACCESS", + metadata=InsightsMetadata(door=[]), + ), + ) + + await handlers["access.logs.insights.add"](insights_msg) + await hass.async_block_till_done() + + state = hass.states.get(FRONT_DOOR_ACCESS_ENTITY) + assert state is not None + assert state.state == "unknown" + + +@pytest.mark.freeze_time("2025-01-01 00:00:00+00:00") +async def test_insights_multiple_doors( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test insights event with multiple doors dispatches events for each.""" + handlers = _get_ws_handlers(mock_client) + + insights_msg = InsightsAdd( + event="access.logs.insights.add", + data=InsightsAddData( + event_type="access.door.unlock", + result="ACCESS", + metadata=InsightsMetadata( + door=[ + InsightsMetadataEntry(id="door-001", display_name="Front Door"), + InsightsMetadataEntry(id="door-002", display_name="Back Door"), + ], + actor=InsightsMetadataEntry(display_name="John Doe"), + authentication=InsightsMetadataEntry(display_name="NFC"), + ), + ), + ) + + await handlers["access.logs.insights.add"](insights_msg) + await hass.async_block_till_done() + + front_state = hass.states.get(FRONT_DOOR_ACCESS_ENTITY) + assert front_state is not None + assert front_state.attributes["event_type"] == "access_granted" + assert front_state.attributes["actor"] == "John Doe" + assert front_state.state == "2025-01-01T00:00:00.000+00:00" + + back_state = hass.states.get(BACK_DOOR_ACCESS_ENTITY) + assert back_state is not None + assert back_state.attributes["event_type"] == "access_granted" + assert back_state.attributes["actor"] == "John Doe" + assert back_state.state == "2025-01-01T00:00:00.000+00:00" + + async def test_unload_entry_removes_listeners( hass: HomeAssistant, init_integration: MockConfigEntry,