mirror of
https://github.com/home-assistant/core.git
synced 2026-09-19 16:23:06 +01:00
Expose UniFi Protect smart detection event source (#179508)
This commit is contained in:
@@ -13,6 +13,7 @@ AUTH_RETRIES = 2
|
||||
|
||||
ATTR_EVENT_SCORE = "event_score"
|
||||
ATTR_EVENT_ID = "event_id"
|
||||
ATTR_EVENT_SOURCE = "event_source"
|
||||
ATTR_SMART_DETECT_TYPES = "smart_detect_types"
|
||||
ATTR_WIDTH = "width"
|
||||
ATTR_HEIGHT = "height"
|
||||
|
||||
@@ -415,10 +415,11 @@ class ProtectData:
|
||||
|
||||
Each non-eviction change is dispatched — a detection type may surface at
|
||||
the event start, on a later update, or only as it ends — routed to the
|
||||
subscribers registered for this device and event type; entities fire each
|
||||
``(event, type)`` once. Subscriptions are keyed by ``device_id`` (the
|
||||
stable cross-API join key, shared by the private and public bootstraps),
|
||||
so the event routes directly without a bootstrap lookup.
|
||||
subscribers registered for this device and event type; entities dedupe
|
||||
each surfaced type by event id and Protect event type. Subscriptions are
|
||||
keyed by ``device_id`` (the stable cross-API join key, shared by the private
|
||||
and public bootstraps), so the event routes directly without a bootstrap
|
||||
lookup.
|
||||
"""
|
||||
if change is EventChange.REMOVED:
|
||||
return
|
||||
|
||||
@@ -21,6 +21,7 @@ from homeassistant.helpers.event import async_call_at
|
||||
from . import Bootstrap
|
||||
from .const import (
|
||||
ATTR_EVENT_ID,
|
||||
ATTR_EVENT_SOURCE,
|
||||
ATTR_SMART_DETECT_TYPES,
|
||||
EVENT_TYPE_FINGERPRINT_IDENTIFIED,
|
||||
EVENT_TYPE_FINGERPRINT_NOT_IDENTIFIED,
|
||||
@@ -98,6 +99,7 @@ class ProtectDetectionEventEntityDescription(ProtectEventEntityDescription):
|
||||
"""Describes a category detection event entity driven by the public events WS."""
|
||||
|
||||
ufp_public_event_types: tuple[EventType, ...]
|
||||
include_event_source: bool = False
|
||||
|
||||
|
||||
class ProtectDevicePublicEventEntity(
|
||||
@@ -107,7 +109,7 @@ class ProtectDevicePublicEventEntity(
|
||||
|
||||
A detection type can surface at the event start, on a later update, or only
|
||||
as the event ends, and every non-eviction change is dispatched — so firing is
|
||||
deduped per ``(event id, event type)``.
|
||||
deduped per ``(event id, object type, event source)``.
|
||||
|
||||
Availability follows the public API (device present and connected) plus the
|
||||
events websocket, which is the only channel these entities fire from.
|
||||
@@ -118,25 +120,28 @@ class ProtectDevicePublicEventEntity(
|
||||
|
||||
entity_description: ProtectEventEntityDescription
|
||||
# A camera can run two overlapping events of the same category whose
|
||||
# dispatches interleave, so dedup tracks fired types per recent event id
|
||||
# (bounded), not just the current one.
|
||||
_fired: dict[str, frozenset[str]] | None = None
|
||||
# dispatches interleave, so dedup tracks fired object/source pairs per
|
||||
# recent event id (bounded), not just the current one.
|
||||
_fired: dict[str, frozenset[tuple[str, EventType]]] | None = None
|
||||
|
||||
@callback
|
||||
def _fire_once(
|
||||
self, event: ProtectEvent, event_type: str, event_data: dict[str, Any]
|
||||
) -> None:
|
||||
"""Fire ``event_type`` once per event, ignoring repeat dispatches."""
|
||||
"""Fire once per event id, surfaced type, and Protect event type."""
|
||||
fired = self._fired
|
||||
if fired is None:
|
||||
fired = self._fired = {}
|
||||
# Pop-and-reinsert so any dispatch refreshes this event id's recency; a
|
||||
# long-running event that keeps updating is then not evicted below.
|
||||
types = fired.pop(event.id, frozenset())
|
||||
if event_type in types:
|
||||
# Protect can reuse an event id across overlapping smart-detect sources,
|
||||
# so omitting event.type would swallow a later line or loiter event.
|
||||
fired_type = (event_type, event.type)
|
||||
if fired_type in types:
|
||||
fired[event.id] = types
|
||||
return
|
||||
fired[event.id] = types | {event_type}
|
||||
fired[event.id] = types | {fired_type}
|
||||
if len(fired) > _MAX_TRACKED_EVENTS:
|
||||
del fired[next(iter(fired))] # evict the least-recently-seen event id
|
||||
self._trigger_event(event_type, event_data)
|
||||
@@ -433,8 +438,8 @@ class ProtectDeviceSmartDetectEventEntity(ProtectDevicePublicEventEntity):
|
||||
Used for object types that Protect models as discrete, point-in-time
|
||||
detections (e.g. package): the camera fires once with a cooldown and the
|
||||
smart-detect event is recorded already-ended, so a sustained binary sensor
|
||||
can never reflect it. The public events websocket delivers these as proper
|
||||
``smartDetectZone`` events (the private API only exposes the unhandled
|
||||
can never reflect it. The public events websocket delivers these as smart
|
||||
detection events (the private API only exposes the unhandled
|
||||
``smartDetectObject`` model), so we subscribe there and fire a momentary
|
||||
event when the description's object type matches.
|
||||
"""
|
||||
@@ -457,7 +462,14 @@ class ProtectDeviceSmartDetectEventEntity(ProtectDevicePublicEventEntity):
|
||||
description = self.entity_description
|
||||
event_types = description.event_types
|
||||
if event_types and description.ufp_obj_type in event.smart_detect_types:
|
||||
self._fire_once(event, event_types[0], {ATTR_EVENT_ID: event.id})
|
||||
self._fire_once(
|
||||
event,
|
||||
event_types[0],
|
||||
{
|
||||
ATTR_EVENT_ID: event.id,
|
||||
ATTR_EVENT_SOURCE: event.type.value,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
_CAMEL_BOUNDARY = re.compile(r"(?<!^)(?=[A-Z])")
|
||||
@@ -520,17 +532,27 @@ class ProtectDeviceDetectionEventEntity(ProtectDevicePublicEventEntity):
|
||||
|
||||
@callback
|
||||
def _async_detection_event(self, event: ProtectEvent) -> None:
|
||||
allowed = self.entity_description.event_types or ()
|
||||
description = self.entity_description
|
||||
allowed = description.event_types or ()
|
||||
# One fire per detected type so each stays independently automatable
|
||||
# (incl. types with no binary sensor); carries the co-detected set known
|
||||
# at fire time (types can still arrive on a later update).
|
||||
detected = [_event_type(t) for t in event.smart_detect_types]
|
||||
for event_type in detected:
|
||||
if event_type in allowed:
|
||||
event_data: dict[str, Any] = {
|
||||
ATTR_EVENT_ID: event.id,
|
||||
ATTR_SMART_DETECT_TYPES: detected,
|
||||
}
|
||||
if description.include_event_source:
|
||||
# Keep this raw: hassfest state translation keys reject
|
||||
# camelCase, so normalization belongs in uiprotect. It remains
|
||||
# recordable to distinguish overlapping sources in history.
|
||||
event_data[ATTR_EVENT_SOURCE] = event.type.value
|
||||
self._fire_once(
|
||||
event,
|
||||
event_type,
|
||||
{ATTR_EVENT_ID: event.id, ATTR_SMART_DETECT_TYPES: detected},
|
||||
event_data,
|
||||
)
|
||||
|
||||
|
||||
@@ -601,6 +623,7 @@ EVENT_DESCRIPTIONS: tuple[ProtectEventEntityDescription, ...] = (
|
||||
ufp_required_field="feature_flags.has_smart_detect",
|
||||
event_types=_SMART_OBJECT_EVENT_TYPES,
|
||||
ufp_public_event_types=_SMART_DETECT_EVENT_TYPES,
|
||||
include_event_source=True,
|
||||
entity_class=ProtectDeviceDetectionEventEntity,
|
||||
),
|
||||
ProtectDetectionEventEntityDescription(
|
||||
|
||||
@@ -388,6 +388,9 @@
|
||||
"package": {
|
||||
"name": "Package",
|
||||
"state_attributes": {
|
||||
"event_source": {
|
||||
"name": "[%key:component::unifiprotect::entity::event::smart_detection::state_attributes::event_source::name%]"
|
||||
},
|
||||
"event_type": {
|
||||
"state": {
|
||||
"detected": "Detected"
|
||||
@@ -398,6 +401,9 @@
|
||||
"smart_detection": {
|
||||
"name": "Smart detection",
|
||||
"state_attributes": {
|
||||
"event_source": {
|
||||
"name": "Event source"
|
||||
},
|
||||
"event_type": {
|
||||
"state": {
|
||||
"animal": "Animal",
|
||||
|
||||
@@ -21,6 +21,7 @@ from uiprotect.websocket import WebsocketState
|
||||
|
||||
from homeassistant.components.unifiprotect.const import (
|
||||
ATTR_EVENT_ID,
|
||||
ATTR_EVENT_SOURCE,
|
||||
ATTR_SMART_DETECT_TYPES,
|
||||
DEFAULT_ATTRIBUTION,
|
||||
EVENT_TYPE_PACKAGE_DETECTED,
|
||||
@@ -156,11 +157,13 @@ async def test_doorbell_ring(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"event_type",
|
||||
("event_type", "overlapping_event_type"),
|
||||
[
|
||||
pytest.param(EventType.SMART_DETECT, id="zone"),
|
||||
pytest.param(EventType.SMART_DETECT_LINE, id="line"),
|
||||
pytest.param(EventType.SMART_DETECT_LOITER, id="loiter"),
|
||||
pytest.param(EventType.SMART_DETECT, EventType.SMART_DETECT_LINE, id="zone"),
|
||||
pytest.param(EventType.SMART_DETECT_LINE, EventType.SMART_DETECT, id="line"),
|
||||
pytest.param(
|
||||
EventType.SMART_DETECT_LOITER, EventType.SMART_DETECT, id="loiter"
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_package_detected(
|
||||
@@ -170,6 +173,7 @@ async def test_package_detected(
|
||||
unadopted_camera: Camera,
|
||||
fixed_now: datetime,
|
||||
event_type: EventType,
|
||||
overlapping_event_type: EventType,
|
||||
) -> None:
|
||||
"""Test a package detection event fired from the public events websocket."""
|
||||
|
||||
@@ -216,6 +220,7 @@ async def test_package_detected(
|
||||
assert state
|
||||
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||
assert state.attributes[ATTR_EVENT_ID] == "test_package_event"
|
||||
assert state.attributes[ATTR_EVENT_SOURCE] == event_type.value
|
||||
assert state.attributes["event_type"] == EVENT_TYPE_PACKAGE_DETECTED
|
||||
|
||||
# A non-package detection must not fire the package entity.
|
||||
@@ -235,12 +240,12 @@ async def test_package_detected(
|
||||
await hass.async_block_till_done()
|
||||
assert len(events) == 1
|
||||
|
||||
# Updates are dispatched too, but the entity fires each (event id, type)
|
||||
# once, so a repeat dispatch of the same package event must be suppressed.
|
||||
# Updates are dispatched too, but the entity fires each (event id, object
|
||||
# type, event source) once, so the same source must be suppressed.
|
||||
ufp.events_msg(
|
||||
ProtectEvent(
|
||||
id="test_package_event",
|
||||
type=EventType.SMART_DETECT,
|
||||
type=event_type,
|
||||
channel=ProtectEventChannel.DETECTION,
|
||||
device_id=doorbell.id,
|
||||
device_mac=doorbell.mac,
|
||||
@@ -253,6 +258,27 @@ async def test_package_detected(
|
||||
await hass.async_block_till_done()
|
||||
assert len(events) == 1
|
||||
|
||||
# The same event and object can fire again for a distinct Protect source.
|
||||
ufp.events_msg(
|
||||
ProtectEvent(
|
||||
id="test_package_event",
|
||||
type=overlapping_event_type,
|
||||
channel=ProtectEventChannel.DETECTION,
|
||||
device_id=doorbell.id,
|
||||
device_mac=doorbell.mac,
|
||||
start=fixed_now - timedelta(seconds=1),
|
||||
end=fixed_now,
|
||||
smart_detect_types=(SmartDetectObjectType.PACKAGE,),
|
||||
),
|
||||
EventChange.STARTED,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(events) == 2
|
||||
assert (
|
||||
events[-1].data["new_state"].attributes[ATTR_EVENT_SOURCE]
|
||||
== overlapping_event_type.value
|
||||
)
|
||||
|
||||
# Subscriptions are keyed by device_id alone: an event still dispatches when
|
||||
# it carries no device_mac and the device is absent from the private
|
||||
# bootstrap, proving the public event path does not depend on it.
|
||||
@@ -271,8 +297,8 @@ async def test_package_detected(
|
||||
EventChange.STARTED,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(events) == 2
|
||||
assert events[1].data["new_state"].attributes[ATTR_EVENT_ID] == (
|
||||
assert len(events) == 3
|
||||
assert events[2].data["new_state"].attributes[ATTR_EVENT_ID] == (
|
||||
"test_package_event_no_private_device"
|
||||
)
|
||||
|
||||
@@ -291,7 +317,7 @@ async def test_package_detected(
|
||||
EventChange.STARTED,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(events) == 2
|
||||
assert len(events) == 3
|
||||
|
||||
# A non-smart-detect event that happens to carry a matching object type is
|
||||
# routed by type, so it must not reach the smart-detect entity.
|
||||
@@ -309,7 +335,7 @@ async def test_package_detected(
|
||||
EventChange.STARTED,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(events) == 2
|
||||
assert len(events) == 3
|
||||
|
||||
unsub()
|
||||
|
||||
@@ -1767,24 +1793,36 @@ async def test_motion_detection_event(
|
||||
hass, Platform.EVENT, doorbell, description
|
||||
)
|
||||
|
||||
events: list[HAEvent] = []
|
||||
|
||||
@callback
|
||||
def _capture(event: HAEvent) -> None:
|
||||
events.append(event)
|
||||
|
||||
unsub = async_track_state_change_event(hass, entity_id, _capture)
|
||||
motion_event = ProtectEvent(
|
||||
id="motion-1",
|
||||
type=EventType.MOTION,
|
||||
channel=ProtectEventChannel.DETECTION,
|
||||
device_id=doorbell.id,
|
||||
device_mac=doorbell.mac,
|
||||
start=fixed_now - timedelta(seconds=1),
|
||||
end=fixed_now,
|
||||
)
|
||||
ufp.events_msg(
|
||||
ProtectEvent(
|
||||
id="motion-1",
|
||||
type=EventType.MOTION,
|
||||
channel=ProtectEventChannel.DETECTION,
|
||||
device_id=doorbell.id,
|
||||
device_mac=doorbell.mac,
|
||||
start=fixed_now - timedelta(seconds=1),
|
||||
end=fixed_now,
|
||||
),
|
||||
motion_event,
|
||||
EventChange.STARTED,
|
||||
)
|
||||
ufp.events_msg(motion_event, EventChange.UPDATED)
|
||||
await hass.async_block_till_done()
|
||||
unsub()
|
||||
|
||||
assert len(events) == 1
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes["event_type"] == "motion"
|
||||
assert state.attributes[ATTR_EVENT_ID] == "motion-1"
|
||||
assert ATTR_EVENT_SOURCE not in state.attributes
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -1844,11 +1882,136 @@ async def test_smart_detection_event(
|
||||
unsub()
|
||||
|
||||
# One fire per surfaced type, each carrying the full co-detected set.
|
||||
fired = [event.data["new_state"].attributes["event_type"] for event in events]
|
||||
assert fired == ["person", "vehicle"]
|
||||
last = events[-1].data["new_state"]
|
||||
assert last.attributes[ATTR_EVENT_ID] == "smart-1"
|
||||
assert last.attributes[ATTR_SMART_DETECT_TYPES] == ["person", "vehicle"]
|
||||
states = [event.data["new_state"] for event in events]
|
||||
assert [state.attributes["event_type"] for state in states] == [
|
||||
"person",
|
||||
"vehicle",
|
||||
]
|
||||
assert [state.attributes[ATTR_EVENT_ID] for state in states] == ["smart-1"] * 2
|
||||
assert [state.attributes[ATTR_EVENT_SOURCE] for state in states] == [
|
||||
event_type.value
|
||||
] * 2
|
||||
assert [state.attributes[ATTR_SMART_DETECT_TYPES] for state in states] == [
|
||||
["person", "vehicle"]
|
||||
] * 2
|
||||
|
||||
|
||||
async def test_smart_detection_event_dedup_by_source(
|
||||
hass: HomeAssistant,
|
||||
ufp: MockUFPFixture,
|
||||
doorbell: Camera,
|
||||
unadopted_camera: Camera,
|
||||
fixed_now: datetime,
|
||||
) -> None:
|
||||
"""The same object and event id fire once for each raw Protect event source."""
|
||||
setup_public_camera(ufp)
|
||||
await init_entry(hass, ufp, [doorbell, unadopted_camera])
|
||||
|
||||
description = next(d for d in EVENT_DESCRIPTIONS if d.key == "smart_detection")
|
||||
_, entity_id = await ids_from_device_description(
|
||||
hass, Platform.EVENT, doorbell, description
|
||||
)
|
||||
|
||||
events: list[HAEvent] = []
|
||||
|
||||
@callback
|
||||
def _capture(event: HAEvent) -> None:
|
||||
events.append(event)
|
||||
|
||||
unsub = async_track_state_change_event(hass, entity_id, _capture)
|
||||
common = {
|
||||
"id": "smart-1",
|
||||
"channel": ProtectEventChannel.DETECTION,
|
||||
"device_id": doorbell.id,
|
||||
"device_mac": doorbell.mac,
|
||||
"start": fixed_now - timedelta(seconds=1),
|
||||
"end": fixed_now,
|
||||
"smart_detect_types": (SmartDetectObjectType.PERSON,),
|
||||
}
|
||||
for event_type, change in (
|
||||
(EventType.SMART_DETECT, EventChange.STARTED),
|
||||
(EventType.SMART_DETECT_LINE, EventChange.STARTED),
|
||||
(EventType.SMART_DETECT_LINE, EventChange.UPDATED),
|
||||
(EventType.SMART_DETECT_LOITER, EventChange.STARTED),
|
||||
):
|
||||
ufp.events_msg(ProtectEvent(type=event_type, **common), change)
|
||||
await hass.async_block_till_done()
|
||||
unsub()
|
||||
|
||||
states = [event.data["new_state"] for event in events]
|
||||
assert [state.attributes[ATTR_EVENT_SOURCE] for state in states] == [
|
||||
EventType.SMART_DETECT.value,
|
||||
EventType.SMART_DETECT_LINE.value,
|
||||
EventType.SMART_DETECT_LOITER.value,
|
||||
]
|
||||
assert [state.attributes["event_type"] for state in states] == ["person"] * 3
|
||||
assert [state.attributes[ATTR_EVENT_ID] for state in states] == ["smart-1"] * 3
|
||||
|
||||
|
||||
async def test_smart_detection_event_late_type(
|
||||
hass: HomeAssistant,
|
||||
ufp: MockUFPFixture,
|
||||
doorbell: Camera,
|
||||
unadopted_camera: Camera,
|
||||
fixed_now: datetime,
|
||||
) -> None:
|
||||
"""A late object type fires once without refiring an earlier type."""
|
||||
setup_public_camera(ufp)
|
||||
await init_entry(hass, ufp, [doorbell, unadopted_camera])
|
||||
|
||||
description = next(d for d in EVENT_DESCRIPTIONS if d.key == "smart_detection")
|
||||
_, entity_id = await ids_from_device_description(
|
||||
hass, Platform.EVENT, doorbell, description
|
||||
)
|
||||
|
||||
events: list[HAEvent] = []
|
||||
|
||||
@callback
|
||||
def _capture(event: HAEvent) -> None:
|
||||
events.append(event)
|
||||
|
||||
unsub = async_track_state_change_event(hass, entity_id, _capture)
|
||||
base = {
|
||||
"id": "smart-late",
|
||||
"type": EventType.SMART_DETECT_LINE,
|
||||
"channel": ProtectEventChannel.DETECTION,
|
||||
"device_id": doorbell.id,
|
||||
"device_mac": doorbell.mac,
|
||||
"start": fixed_now - timedelta(seconds=1),
|
||||
}
|
||||
ufp.events_msg(
|
||||
ProtectEvent(
|
||||
**base,
|
||||
end=None,
|
||||
smart_detect_types=(SmartDetectObjectType.PERSON,),
|
||||
),
|
||||
EventChange.STARTED,
|
||||
)
|
||||
for end in (None, fixed_now):
|
||||
ufp.events_msg(
|
||||
ProtectEvent(
|
||||
**base,
|
||||
end=end,
|
||||
smart_detect_types=(
|
||||
SmartDetectObjectType.PERSON,
|
||||
SmartDetectObjectType.VEHICLE,
|
||||
),
|
||||
),
|
||||
EventChange.UPDATED,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
unsub()
|
||||
|
||||
states = [event.data["new_state"] for event in events]
|
||||
assert [state.attributes["event_type"] for state in states] == [
|
||||
"person",
|
||||
"vehicle",
|
||||
]
|
||||
assert [state.attributes[ATTR_EVENT_SOURCE] for state in states] == [
|
||||
EventType.SMART_DETECT_LINE.value
|
||||
] * 2
|
||||
assert states[0].attributes[ATTR_SMART_DETECT_TYPES] == ["person"]
|
||||
assert states[1].attributes[ATTR_SMART_DETECT_TYPES] == ["person", "vehicle"]
|
||||
|
||||
|
||||
async def test_sound_detection_event(
|
||||
@@ -1887,6 +2050,7 @@ async def test_sound_detection_event(
|
||||
assert state
|
||||
assert state.attributes["event_type"] == "smoke"
|
||||
assert state.attributes[ATTR_EVENT_ID] == "audio-1"
|
||||
assert ATTR_EVENT_SOURCE not in state.attributes
|
||||
|
||||
|
||||
async def test_sound_detection_event_late_type(
|
||||
@@ -2086,7 +2250,7 @@ async def test_smart_detection_event_interleaved_dedup(
|
||||
unadopted_camera: Camera,
|
||||
fixed_now: datetime,
|
||||
) -> None:
|
||||
"""Two overlapping same-category events whose dispatches interleave don't re-fire."""
|
||||
"""Two overlapping same-source event ids remain independently deduplicated."""
|
||||
setup_public_camera(ufp)
|
||||
await init_entry(hass, ufp, [doorbell, unadopted_camera])
|
||||
|
||||
@@ -2119,7 +2283,7 @@ async def test_smart_detection_event_interleaved_dedup(
|
||||
)
|
||||
ufp.events_msg(
|
||||
ProtectEvent(
|
||||
id="evt-b", smart_detect_types=(SmartDetectObjectType.VEHICLE,), **common
|
||||
id="evt-b", smart_detect_types=(SmartDetectObjectType.PERSON,), **common
|
||||
),
|
||||
EventChange.STARTED,
|
||||
)
|
||||
@@ -2132,9 +2296,15 @@ async def test_smart_detection_event_interleaved_dedup(
|
||||
await hass.async_block_till_done()
|
||||
unsub()
|
||||
|
||||
# A's person is not re-fired when its update arrives after B's dispatch.
|
||||
fired = [event.data["new_state"].attributes["event_type"] for event in events]
|
||||
assert fired == ["person", "vehicle"]
|
||||
states = [event.data["new_state"] for event in events]
|
||||
assert [state.attributes[ATTR_EVENT_ID] for state in states] == [
|
||||
"evt-a",
|
||||
"evt-b",
|
||||
]
|
||||
assert [state.attributes["event_type"] for state in states] == ["person"] * 2
|
||||
assert [state.attributes[ATTR_EVENT_SOURCE] for state in states] == [
|
||||
EventType.SMART_DETECT.value
|
||||
] * 2
|
||||
|
||||
|
||||
async def test_detection_event_dedup_is_bounded(
|
||||
|
||||
@@ -9,6 +9,7 @@ from homeassistant.components.recorder import Recorder
|
||||
from homeassistant.components.recorder.history import get_significant_states
|
||||
from homeassistant.components.unifiprotect.const import (
|
||||
ATTR_EVENT_ID,
|
||||
ATTR_EVENT_SOURCE,
|
||||
ATTR_SMART_DETECT_TYPES,
|
||||
)
|
||||
from homeassistant.components.unifiprotect.event import EVENT_DESCRIPTIONS
|
||||
@@ -63,13 +64,18 @@ async def test_exclude_attributes(
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes[ATTR_EVENT_ID] == "test_event_id"
|
||||
assert state.attributes[ATTR_EVENT_SOURCE] == EventType.SMART_DETECT.value
|
||||
assert ATTR_SMART_DETECT_TYPES in state.attributes
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
states = await hass.async_add_executor_job(
|
||||
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||
)
|
||||
assert len(states) >= 1
|
||||
assert entity_id in states
|
||||
assert (
|
||||
states[entity_id][-1].attributes[ATTR_EVENT_SOURCE]
|
||||
== EventType.SMART_DETECT.value
|
||||
)
|
||||
for entity_states in states.values():
|
||||
for state in entity_states:
|
||||
assert ATTR_EVENT_ID not in state.attributes
|
||||
|
||||
Reference in New Issue
Block a user