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