1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 00:20:30 +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,
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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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):

View File

@@ -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:

View File

@@ -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,