diff --git a/homeassistant/components/unifi_access/binary_sensor.py b/homeassistant/components/unifi_access/binary_sensor.py index a59dc4d2b1c..f8bf2b59065 100644 --- a/homeassistant/components/unifi_access/binary_sensor.py +++ b/homeassistant/components/unifi_access/binary_sensor.py @@ -8,7 +8,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import UnifiAccessConfigEntry, UnifiAccessCoordinator @@ -24,10 +24,23 @@ async def async_setup_entry( ) -> None: """Set up UniFi Access binary sensor entities.""" coordinator = entry.runtime_data - async_add_entities( - UnifiAccessDoorPositionBinarySensor(coordinator, door) - for door in coordinator.data.doors.values() - ) + added_doors: set[str] = set() + + @callback + def _async_add_new_doors() -> None: + new_door_ids = sorted(set(coordinator.data.doors) - added_doors) + if not new_door_ids: + return + async_add_entities( + UnifiAccessDoorPositionBinarySensor( + coordinator, coordinator.data.doors[door_id] + ) + for door_id in new_door_ids + ) + added_doors.update(new_door_ids) + + _async_add_new_doors() + entry.async_on_unload(coordinator.async_add_listener(_async_add_new_doors)) class UnifiAccessDoorPositionBinarySensor(UnifiAccessEntity, BinarySensorEntity): diff --git a/homeassistant/components/unifi_access/button.py b/homeassistant/components/unifi_access/button.py index d1c795006cf..4527dfb048a 100644 --- a/homeassistant/components/unifi_access/button.py +++ b/homeassistant/components/unifi_access/button.py @@ -5,7 +5,7 @@ from __future__ import annotations from unifi_access_api import Door, UnifiAccessError from homeassistant.components.button import ButtonEntity -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -23,10 +23,21 @@ async def async_setup_entry( ) -> None: """Set up UniFi Access button entities.""" coordinator = entry.runtime_data - async_add_entities( - UnifiAccessUnlockButton(coordinator, door) - for door in coordinator.data.doors.values() - ) + added_doors: set[str] = set() + + @callback + def _async_add_new_doors() -> None: + new_door_ids = sorted(set(coordinator.data.doors) - added_doors) + if not new_door_ids: + return + async_add_entities( + UnifiAccessUnlockButton(coordinator, coordinator.data.doors[door_id]) + for door_id in new_door_ids + ) + added_doors.update(new_door_ids) + + _async_add_new_doors() + entry.async_on_unload(coordinator.async_add_listener(_async_add_new_doors)) class UnifiAccessUnlockButton(UnifiAccessEntity, ButtonEntity): diff --git a/homeassistant/components/unifi_access/event.py b/homeassistant/components/unifi_access/event.py index b13bdce869e..99a8b9b55ef 100644 --- a/homeassistant/components/unifi_access/event.py +++ b/homeassistant/components/unifi_access/event.py @@ -55,11 +55,24 @@ async def async_setup_entry( ) -> None: """Set up UniFi Access event entities.""" coordinator = entry.runtime_data - async_add_entities( - UnifiAccessEventEntity(coordinator, door, description) - for door in coordinator.data.doors.values() - for description in EVENT_DESCRIPTIONS - ) + added_doors: set[str] = set() + + @callback + def _async_add_new_doors() -> None: + new_door_ids = sorted(set(coordinator.data.doors) - added_doors) + if not new_door_ids: + return + async_add_entities( + UnifiAccessEventEntity( + coordinator, coordinator.data.doors[door_id], description + ) + for door_id in new_door_ids + for description in EVENT_DESCRIPTIONS + ) + added_doors.update(new_door_ids) + + _async_add_new_doors() + entry.async_on_unload(coordinator.async_add_listener(_async_add_new_doors)) class UnifiAccessEventEntity(UnifiAccessEntity, EventEntity): diff --git a/homeassistant/components/unifi_access/image.py b/homeassistant/components/unifi_access/image.py index ccb45ede0c0..b2ca2b5d242 100644 --- a/homeassistant/components/unifi_access/image.py +++ b/homeassistant/components/unifi_access/image.py @@ -8,7 +8,7 @@ from unifi_access_api import Door from homeassistant.components.image import ImageEntity from homeassistant.const import CONF_VERIFY_SSL -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .coordinator import UnifiAccessConfigEntry, UnifiAccessCoordinator @@ -24,10 +24,26 @@ async def async_setup_entry( ) -> None: """Set up image entities for UniFi Access doors.""" coordinator = entry.runtime_data - async_add_entities( - UnifiAccessDoorImageEntity(coordinator, hass, entry.data[CONF_VERIFY_SSL], door) - for door in coordinator.data.doors.values() - ) + added_doors: set[str] = set() + + @callback + def _async_add_new_doors() -> None: + new_door_ids = sorted(set(coordinator.data.doors) - added_doors) + if not new_door_ids: + return + async_add_entities( + UnifiAccessDoorImageEntity( + coordinator, + hass, + entry.data[CONF_VERIFY_SSL], + coordinator.data.doors[door_id], + ) + for door_id in new_door_ids + ) + added_doors.update(new_door_ids) + + _async_add_new_doors() + entry.async_on_unload(coordinator.async_add_listener(_async_add_new_doors)) class UnifiAccessDoorImageEntity(UnifiAccessEntity, ImageEntity): diff --git a/homeassistant/components/unifi_access/quality_scale.yaml b/homeassistant/components/unifi_access/quality_scale.yaml index 66dc50f69d2..72d0bb8590d 100644 --- a/homeassistant/components/unifi_access/quality_scale.yaml +++ b/homeassistant/components/unifi_access/quality_scale.yaml @@ -51,7 +51,7 @@ rules: docs-supported-functions: todo docs-troubleshooting: todo docs-use-cases: todo - dynamic-devices: todo + dynamic-devices: done entity-category: done entity-device-class: done entity-disabled-by-default: diff --git a/tests/components/unifi_access/test_init.py b/tests/components/unifi_access/test_init.py index 733a72be5f9..6d1664c36de 100644 --- a/tests/components/unifi_access/test_init.py +++ b/tests/components/unifi_access/test_init.py @@ -28,6 +28,8 @@ from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from .conftest import _make_door + from tests.common import MockConfigEntry FRONT_DOOR_BINARY_SENSOR = "binary_sensor.front_door" @@ -357,6 +359,41 @@ async def test_ws_location_update_thumbnail_only_no_state( assert hass.states.get(FRONT_DOOR_IMAGE).state != image_state_before +async def test_new_door_entities_created_on_refresh( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test that new door entities are added dynamically via coordinator listener.""" + # Verify new door entities do not exist yet + assert not hass.states.get("binary_sensor.garage_door") + assert not hass.states.get("button.garage_door_unlock") + assert not hass.states.get("event.garage_door_doorbell") + assert not hass.states.get("event.garage_door_access") + assert not hass.states.get("image.garage_door_thumbnail") + + # Add a new door to the API response + mock_client.get_doors.return_value = [ + *mock_client.get_doors.return_value, + _make_door("door-003", "Garage Door"), + ] + + # Trigger natural refresh via WebSocket reconnect + on_disconnect = mock_client.start_websocket.call_args[1]["on_disconnect"] + on_connect = mock_client.start_websocket.call_args[1]["on_connect"] + on_disconnect() + await hass.async_block_till_done() + on_connect() + await hass.async_block_till_done() + + # Entities for the new door should now exist + assert hass.states.get("binary_sensor.garage_door") + assert hass.states.get("button.garage_door_unlock") + assert hass.states.get("event.garage_door_doorbell") + assert hass.states.get("event.garage_door_access") + assert hass.states.get("image.garage_door_thumbnail") + + async def test_stale_device_removed_on_refresh( hass: HomeAssistant, device_registry: dr.DeviceRegistry,