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:
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user