1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-17 23:53:49 +01:00

Add dynamic device support for UniFi Access door platforms (#166793)

This commit is contained in:
Raphael Hehl
2026-03-30 19:51:05 +02:00
committed by GitHub
parent 52af74c3b6
commit fd54e45aeb
6 changed files with 111 additions and 21 deletions

View File

@@ -8,7 +8,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import UnifiAccessConfigEntry, UnifiAccessCoordinator from .coordinator import UnifiAccessConfigEntry, UnifiAccessCoordinator
@@ -24,10 +24,23 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up UniFi Access binary sensor entities.""" """Set up UniFi Access binary sensor entities."""
coordinator = entry.runtime_data coordinator = entry.runtime_data
async_add_entities( added_doors: set[str] = set()
UnifiAccessDoorPositionBinarySensor(coordinator, door)
for door in coordinator.data.doors.values() @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): class UnifiAccessDoorPositionBinarySensor(UnifiAccessEntity, BinarySensorEntity):

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from unifi_access_api import Door, UnifiAccessError from unifi_access_api import Door, UnifiAccessError
from homeassistant.components.button import ButtonEntity from homeassistant.components.button import ButtonEntity
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -23,10 +23,21 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up UniFi Access button entities.""" """Set up UniFi Access button entities."""
coordinator = entry.runtime_data coordinator = entry.runtime_data
async_add_entities( added_doors: set[str] = set()
UnifiAccessUnlockButton(coordinator, door)
for door in coordinator.data.doors.values() @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): class UnifiAccessUnlockButton(UnifiAccessEntity, ButtonEntity):

View File

@@ -55,11 +55,24 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up UniFi Access event entities.""" """Set up UniFi Access event entities."""
coordinator = entry.runtime_data coordinator = entry.runtime_data
async_add_entities( added_doors: set[str] = set()
UnifiAccessEventEntity(coordinator, door, description)
for door in coordinator.data.doors.values() @callback
for description in EVENT_DESCRIPTIONS 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): class UnifiAccessEventEntity(UnifiAccessEntity, EventEntity):

View File

@@ -8,7 +8,7 @@ from unifi_access_api import Door
from homeassistant.components.image import ImageEntity from homeassistant.components.image import ImageEntity
from homeassistant.const import CONF_VERIFY_SSL 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 homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import UnifiAccessConfigEntry, UnifiAccessCoordinator from .coordinator import UnifiAccessConfigEntry, UnifiAccessCoordinator
@@ -24,10 +24,26 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up image entities for UniFi Access doors.""" """Set up image entities for UniFi Access doors."""
coordinator = entry.runtime_data coordinator = entry.runtime_data
async_add_entities( added_doors: set[str] = set()
UnifiAccessDoorImageEntity(coordinator, hass, entry.data[CONF_VERIFY_SSL], door)
for door in coordinator.data.doors.values() @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): class UnifiAccessDoorImageEntity(UnifiAccessEntity, ImageEntity):

View File

@@ -51,7 +51,7 @@ rules:
docs-supported-functions: todo docs-supported-functions: todo
docs-troubleshooting: todo docs-troubleshooting: todo
docs-use-cases: todo docs-use-cases: todo
dynamic-devices: todo dynamic-devices: done
entity-category: done entity-category: done
entity-device-class: done entity-device-class: done
entity-disabled-by-default: entity-disabled-by-default:

View File

@@ -28,6 +28,8 @@ from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from .conftest import _make_door
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
FRONT_DOOR_BINARY_SENSOR = "binary_sensor.front_door" 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 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( async def test_stale_device_removed_on_refresh(
hass: HomeAssistant, hass: HomeAssistant,
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,