Fix unifiprotect smart detection when end is set (#120027)

This commit is contained in:
J. Nick Koston 2024-06-20 22:03:07 -05:00 committed by GitHub
parent ecadaf314d
commit 68462b014c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 372 additions and 66 deletions

View File

@ -14,6 +14,7 @@ from uiprotect.data import (
ProtectAdoptableDeviceModel,
ProtectModelWithId,
Sensor,
SmartDetectObjectType,
)
from uiprotect.data.nvr import UOSDisk
@ -436,11 +437,13 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
ufp_enabled="is_motion_detection_on",
ufp_event_obj="last_motion_event",
),
)
SMART_EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
ProtectBinaryEventEntityDescription(
key="smart_obj_any",
name="Object detected",
icon="mdi:eye",
ufp_value="is_smart_currently_detected",
ufp_required_field="feature_flags.has_smart_detect",
ufp_event_obj="last_smart_detect_event",
),
@ -448,7 +451,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
key="smart_obj_person",
name="Person detected",
icon="mdi:walk",
ufp_value="is_person_currently_detected",
ufp_obj_type=SmartDetectObjectType.PERSON,
ufp_required_field="can_detect_person",
ufp_enabled="is_person_detection_on",
ufp_event_obj="last_person_detect_event",
@ -457,7 +460,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
key="smart_obj_vehicle",
name="Vehicle detected",
icon="mdi:car",
ufp_value="is_vehicle_currently_detected",
ufp_obj_type=SmartDetectObjectType.VEHICLE,
ufp_required_field="can_detect_vehicle",
ufp_enabled="is_vehicle_detection_on",
ufp_event_obj="last_vehicle_detect_event",
@ -466,7 +469,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
key="smart_obj_animal",
name="Animal detected",
icon="mdi:paw",
ufp_value="is_animal_currently_detected",
ufp_obj_type=SmartDetectObjectType.ANIMAL,
ufp_required_field="can_detect_animal",
ufp_enabled="is_animal_detection_on",
ufp_event_obj="last_animal_detect_event",
@ -475,8 +478,8 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
key="smart_obj_package",
name="Package detected",
icon="mdi:package-variant-closed",
ufp_value="is_package_currently_detected",
entity_registry_enabled_default=False,
ufp_obj_type=SmartDetectObjectType.PACKAGE,
ufp_required_field="can_detect_package",
ufp_enabled="is_package_detection_on",
ufp_event_obj="last_package_detect_event",
@ -485,7 +488,6 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
key="smart_audio_any",
name="Audio object detected",
icon="mdi:eye",
ufp_value="is_audio_currently_detected",
ufp_required_field="feature_flags.has_smart_detect",
ufp_event_obj="last_smart_audio_detect_event",
),
@ -493,7 +495,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
key="smart_audio_smoke",
name="Smoke alarm detected",
icon="mdi:fire",
ufp_value="is_smoke_currently_detected",
ufp_obj_type=SmartDetectObjectType.SMOKE,
ufp_required_field="can_detect_smoke",
ufp_enabled="is_smoke_detection_on",
ufp_event_obj="last_smoke_detect_event",
@ -502,16 +504,16 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
key="smart_audio_cmonx",
name="CO alarm detected",
icon="mdi:molecule-co",
ufp_value="is_cmonx_currently_detected",
ufp_required_field="can_detect_co",
ufp_enabled="is_co_detection_on",
ufp_event_obj="last_cmonx_detect_event",
ufp_obj_type=SmartDetectObjectType.CMONX,
),
ProtectBinaryEventEntityDescription(
key="smart_audio_siren",
name="Siren detected",
icon="mdi:alarm-bell",
ufp_value="is_siren_currently_detected",
ufp_obj_type=SmartDetectObjectType.SIREN,
ufp_required_field="can_detect_siren",
ufp_enabled="is_siren_detection_on",
ufp_event_obj="last_siren_detect_event",
@ -520,7 +522,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
key="smart_audio_baby_cry",
name="Baby cry detected",
icon="mdi:cradle",
ufp_value="is_baby_cry_currently_detected",
ufp_obj_type=SmartDetectObjectType.BABY_CRY,
ufp_required_field="can_detect_baby_cry",
ufp_enabled="is_baby_cry_detection_on",
ufp_event_obj="last_baby_cry_detect_event",
@ -529,7 +531,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
key="smart_audio_speak",
name="Speaking detected",
icon="mdi:account-voice",
ufp_value="is_speaking_currently_detected",
ufp_obj_type=SmartDetectObjectType.SPEAK,
ufp_required_field="can_detect_speaking",
ufp_enabled="is_speaking_detection_on",
ufp_event_obj="last_speaking_detect_event",
@ -538,7 +540,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
key="smart_audio_bark",
name="Barking detected",
icon="mdi:dog",
ufp_value="is_bark_currently_detected",
ufp_obj_type=SmartDetectObjectType.BARK,
ufp_required_field="can_detect_bark",
ufp_enabled="is_bark_detection_on",
ufp_event_obj="last_bark_detect_event",
@ -547,7 +549,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
key="smart_audio_car_alarm",
name="Car alarm detected",
icon="mdi:car",
ufp_value="is_car_alarm_currently_detected",
ufp_obj_type=SmartDetectObjectType.BURGLAR,
ufp_required_field="can_detect_car_alarm",
ufp_enabled="is_car_alarm_detection_on",
ufp_event_obj="last_car_alarm_detect_event",
@ -556,7 +558,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
key="smart_audio_car_horn",
name="Car horn detected",
icon="mdi:bugle",
ufp_value="is_car_horn_currently_detected",
ufp_obj_type=SmartDetectObjectType.CAR_HORN,
ufp_required_field="can_detect_car_horn",
ufp_enabled="is_car_horn_detection_on",
ufp_event_obj="last_car_horn_detect_event",
@ -565,7 +567,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
key="smart_audio_glass_break",
name="Glass break detected",
icon="mdi:glass-fragile",
ufp_value="last_glass_break_detect",
ufp_obj_type=SmartDetectObjectType.GLASS_BREAK,
ufp_required_field="can_detect_glass_break",
ufp_enabled="is_glass_break_detection_on",
ufp_event_obj="last_glass_break_detect_event",
@ -709,11 +711,50 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity):
@callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device)
is_on = self.entity_description.get_is_on(self.device, self._event)
self._attr_is_on = is_on
if not is_on:
self._event = None
description = self.entity_description
event = self._event = self.entity_description.get_event_obj(device)
if is_on := bool(description.get_ufp_value(device)):
if event:
self._set_event_attrs(event)
else:
self._attr_extra_state_attributes = {}
self._attr_is_on = is_on
class ProtectSmartEventBinarySensor(EventEntityMixin, BinarySensorEntity):
"""A UniFi Protect Device Binary Sensor for smart events."""
device: Camera
entity_description: ProtectBinaryEventEntityDescription
_state_attrs = ("_attr_available", "_attr_is_on", "_attr_extra_state_attributes")
@callback
def _set_event_done(self) -> None:
self._attr_is_on = False
self._attr_extra_state_attributes = {}
@callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
prev_event = self._event
super()._async_update_device_from_protect(device)
description = self.entity_description
self._event = description.get_event_obj(device)
if not (
(event := self._event)
and not self._event_already_ended(prev_event)
and description.has_matching_smart(event)
and ((is_end := event.end) or self.device.is_smart_detected)
):
self._set_event_done()
return
was_on = self._attr_is_on
self._attr_is_on = True
self._set_event_attrs(event)
if is_end and not was_on:
self._async_event_with_immediate_end()
MODEL_DESCRIPTIONS_WITH_CLASS = (
@ -727,12 +768,19 @@ def _async_event_entities(
data: ProtectData,
ufp_device: ProtectAdoptableDeviceModel | None = None,
) -> list[ProtectDeviceEntity]:
return [
ProtectEventBinarySensor(data, device, description)
for device in (data.get_cameras() if ufp_device is None else [ufp_device])
for description in EVENT_SENSORS
if description.has_required(device)
]
entities: list[ProtectDeviceEntity] = []
for device in data.get_cameras() if ufp_device is None else [ufp_device]:
entities.extend(
ProtectSmartEventBinarySensor(data, device, description)
for description in SMART_EVENT_SENSORS
if description.has_required(device)
)
entities.extend(
ProtectEventBinarySensor(data, device, description)
for description in EVENT_SENSORS
if description.has_required(device)
)
return entities
@callback

View File

@ -305,13 +305,27 @@ class EventEntityMixin(ProtectDeviceEntity):
_event: Event | None = None
@callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
if (event := self.entity_description.get_event_obj(device)) is None:
self._attr_extra_state_attributes = {}
else:
self._attr_extra_state_attributes = {
ATTR_EVENT_ID: event.id,
ATTR_EVENT_SCORE: event.score,
}
self._event = event
super()._async_update_device_from_protect(device)
def _set_event_done(self) -> None:
"""Clear the event and state."""
@callback
def _set_event_attrs(self, event: Event) -> None:
"""Set event attrs."""
self._attr_extra_state_attributes = {
ATTR_EVENT_ID: event.id,
ATTR_EVENT_SCORE: event.score,
}
@callback
def _async_event_with_immediate_end(self) -> None:
# If the event is so short that the detection is received
# in the same message as the end of the event we need to write
# state and than clear the event and write state again.
self.async_write_ha_state()
self._set_event_done()
self.async_write_ha_state()
@callback
def _event_already_ended(self, prev_event: Event | None) -> bool:
event = self._event
return bool(event and event.end and prev_event and prev_event.id == event.id)

View File

@ -10,7 +10,12 @@ import logging
from operator import attrgetter
from typing import Any, Generic, TypeVar
from uiprotect.data import NVR, Event, ProtectAdoptableDeviceModel
from uiprotect.data import (
NVR,
Event,
ProtectAdoptableDeviceModel,
SmartDetectObjectType,
)
from homeassistant.helpers.entity import EntityDescription
@ -79,21 +84,24 @@ class ProtectEventMixin(ProtectEntityDescription[T]):
"""Mixin for events."""
ufp_event_obj: str | None = None
ufp_obj_type: SmartDetectObjectType | None = None
def get_event_obj(self, obj: T) -> Event | None:
"""Return value from UniFi Protect device."""
return None
def has_matching_smart(self, event: Event) -> bool:
"""Determine if the detection type is a match."""
return (
not (obj_type := self.ufp_obj_type) or obj_type in event.smart_detect_types
)
def __post_init__(self) -> None:
"""Override get_event_obj if ufp_event_obj is set."""
if (_ufp_event_obj := self.ufp_event_obj) is not None:
object.__setattr__(self, "get_event_obj", attrgetter(_ufp_event_obj))
super().__post_init__()
def get_is_on(self, obj: T, event: Event | None) -> bool:
"""Return value if event is active."""
return event is not None and self.get_ufp_value(obj)
@dataclass(frozen=True, kw_only=True)
class ProtectSetableKeysMixin(ProtectEntityDescription[T]):

View File

@ -18,6 +18,7 @@ from uiprotect.data import (
ProtectDeviceModel,
ProtectModelWithId,
Sensor,
SmartDetectObjectType,
)
from homeassistant.components.sensor import (
@ -542,7 +543,7 @@ LICENSE_PLATE_EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = (
name="License plate detected",
icon="mdi:car",
translation_key="license_plate",
ufp_value="is_license_plate_currently_detected",
ufp_obj_type=SmartDetectObjectType.LICENSE_PLATE,
ufp_required_field="can_detect_license_plate",
ufp_event_obj="last_license_plate_detect_event",
),
@ -747,19 +748,34 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity):
class ProtectLicensePlateEventSensor(ProtectEventSensor):
"""A UniFi Protect license plate sensor."""
device: Camera
@callback
def _set_event_done(self) -> None:
self._attr_native_value = OBJECT_TYPE_NONE
self._attr_extra_state_attributes = {}
@callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
prev_event = self._event
super()._async_update_device_from_protect(device)
event = self._event
entity_description = self.entity_description
if (
event is None
or (event.metadata is None or event.metadata.license_plate is None)
or not entity_description.get_is_on(self.device, event)
description = self.entity_description
self._event = description.get_event_obj(device)
if not (
(event := self._event)
and not self._event_already_ended(prev_event)
and description.has_matching_smart(event)
and ((is_end := event.end) or self.device.is_smart_detected)
and (metadata := event.metadata)
and (license_plate := metadata.license_plate)
):
self._attr_native_value = OBJECT_TYPE_NONE
self._event = None
self._attr_extra_state_attributes = {}
self._set_event_done()
return
self._attr_native_value = event.metadata.license_plate.name
previous_plate = self._attr_native_value
self._attr_native_value = license_plate.name
self._set_event_attrs(event)
if is_end and previous_plate != license_plate.name:
self._async_event_with_immediate_end()

View File

@ -217,6 +217,7 @@ def doorbell_fixture(camera: Camera, fixed_now: datetime):
SmartDetectObjectType.PERSON,
SmartDetectObjectType.VEHICLE,
SmartDetectObjectType.ANIMAL,
SmartDetectObjectType.PACKAGE,
]
doorbell.has_speaker = True
doorbell.feature_flags.has_hdr = True

View File

@ -5,7 +5,17 @@ from __future__ import annotations
from datetime import datetime, timedelta
from unittest.mock import Mock
from uiprotect.data import Camera, Event, EventType, Light, ModelType, MountType, Sensor
import pytest
from uiprotect.data import (
Camera,
Event,
EventType,
Light,
ModelType,
MountType,
Sensor,
SmartDetectObjectType,
)
from uiprotect.data.nvr import EventMetadata
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
@ -15,6 +25,7 @@ from homeassistant.components.unifiprotect.binary_sensor import (
LIGHT_SENSORS,
MOUNTABLE_SENSE_SENSORS,
SENSE_SENSORS,
SMART_EVENT_SENSORS,
)
from homeassistant.components.unifiprotect.const import (
ATTR_EVENT_SCORE,
@ -23,12 +34,13 @@ from homeassistant.components.unifiprotect.const import (
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
EVENT_STATE_CHANGED,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import Event as HAEvent, EventStateChangedData, HomeAssistant
from homeassistant.helpers import entity_registry as er
from .utils import (
@ -40,6 +52,8 @@ from .utils import (
remove_entities,
)
from tests.common import async_capture_events
LIGHT_SENSOR_WRITE = LIGHT_SENSORS[:2]
SENSE_SENSORS_WRITE = SENSE_SENSORS[:3]
@ -51,11 +65,11 @@ async def test_binary_sensor_camera_remove(
ufp.api.bootstrap.nvr.system_info.ustorage = None
await init_entry(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8)
assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 8)
await remove_entities(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 0, 0)
await adopt_devices(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8)
assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 8)
async def test_binary_sensor_light_remove(
@ -123,7 +137,7 @@ async def test_binary_sensor_setup_camera_all(
ufp.api.bootstrap.nvr.system_info.ustorage = None
await init_entry(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8)
assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 8)
description = EVENT_SENSORS[0]
unique_id, entity_id = ids_from_device_description(
@ -273,7 +287,7 @@ async def test_binary_sensor_update_motion(
"""Test binary_sensor motion entity."""
await init_entry(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 14, 14)
assert_entity_counts(hass, Platform.BINARY_SENSOR, 15, 14)
_, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, doorbell, EVENT_SENSORS[1]
@ -421,3 +435,144 @@ async def test_binary_sensor_update_mount_type_garage(
assert (
state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.GARAGE_DOOR.value
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_binary_sensor_package_detected(
hass: HomeAssistant,
ufp: MockUFPFixture,
doorbell: Camera,
unadopted_camera: Camera,
fixed_now: datetime,
) -> None:
"""Test binary_sensor package detection entity."""
await init_entry(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 15, 15)
doorbell.smart_detect_settings.object_types.append(SmartDetectObjectType.PACKAGE)
_, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, doorbell, SMART_EVENT_SENSORS[4]
)
event = Event(
model=ModelType.EVENT,
id="test_event_id",
type=EventType.SMART_DETECT,
start=fixed_now - timedelta(seconds=1),
end=None,
score=100,
smart_detect_types=[SmartDetectObjectType.PACKAGE],
smart_detect_event_ids=[],
camera_id=doorbell.id,
api=ufp.api,
)
new_camera = doorbell.copy()
new_camera.is_smart_detected = True
new_camera.last_smart_detect_event_ids[SmartDetectObjectType.PACKAGE] = event.id
ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
ufp.api.bootstrap.events = {event.id: event}
mock_msg = Mock()
mock_msg.changed_data = {}
mock_msg.new_obj = event
ufp.ws_msg(mock_msg)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
assert state.attributes[ATTR_EVENT_SCORE] == 100
event = Event(
model=ModelType.EVENT,
id="test_event_id",
type=EventType.SMART_DETECT,
start=fixed_now - timedelta(seconds=1),
end=fixed_now + timedelta(seconds=1),
score=50,
smart_detect_types=[SmartDetectObjectType.PACKAGE],
smart_detect_event_ids=[],
camera_id=doorbell.id,
api=ufp.api,
)
new_camera = doorbell.copy()
new_camera.is_smart_detected = True
new_camera.last_smart_detect_event_ids[SmartDetectObjectType.PACKAGE] = event.id
ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
ufp.api.bootstrap.events = {event.id: event}
mock_msg = Mock()
mock_msg.changed_data = {}
mock_msg.new_obj = event
ufp.ws_msg(mock_msg)
await hass.async_block_till_done()
# Event is already seen and has end, should now be off
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_OFF
# Now send an event that has an end right away
event = Event(
model=ModelType.EVENT,
id="new_event_id",
type=EventType.SMART_DETECT,
start=fixed_now - timedelta(seconds=1),
end=fixed_now + timedelta(seconds=1),
score=80,
smart_detect_types=[SmartDetectObjectType.PACKAGE],
smart_detect_event_ids=[],
camera_id=doorbell.id,
api=ufp.api,
)
new_camera = doorbell.copy()
new_camera.is_smart_detected = True
new_camera.last_smart_detect_event_ids[SmartDetectObjectType.PACKAGE] = event.id
ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
ufp.api.bootstrap.events = {event.id: event}
mock_msg = Mock()
mock_msg.changed_data = {}
mock_msg.new_obj = event
state_changes: list[HAEvent[EventStateChangedData]] = async_capture_events(
hass, EVENT_STATE_CHANGED
)
ufp.ws_msg(mock_msg)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state == STATE_OFF
assert len(state_changes) == 2
on_event = state_changes[0]
state = on_event.data["new_state"]
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
assert state.attributes[ATTR_EVENT_SCORE] == 80
off_event = state_changes[1]
state = off_event.data["new_state"]
assert state
assert state.state == STATE_OFF
assert ATTR_EVENT_SCORE not in state.attributes
# replay and ensure ignored
ufp.ws_msg(mock_msg)
await hass.async_block_till_done()
assert len(state_changes) == 2

View File

@ -30,11 +30,12 @@ from homeassistant.components.unifiprotect.sensor import (
)
from homeassistant.const import (
ATTR_ATTRIBUTION,
EVENT_STATE_CHANGED,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import Event as HAEvent, EventStateChangedData, HomeAssistant
from homeassistant.helpers import entity_registry as er
from .utils import (
@ -49,6 +50,8 @@ from .utils import (
time_changed,
)
from tests.common import async_capture_events
CAMERA_SENSORS_WRITE = CAMERA_SENSORS[:5]
SENSE_SENSORS_WRITE = SENSE_SENSORS[:8]
@ -554,6 +557,10 @@ async def test_camera_update_license_plate(
ufp.api.bootstrap.cameras = {new_camera.id: new_camera}
ufp.api.bootstrap.events = {event.id: event}
state_changes: list[HAEvent[EventStateChangedData]] = async_capture_events(
hass, EVENT_STATE_CHANGED
)
ufp.ws_msg(mock_msg)
await hass.async_block_till_done()
@ -561,6 +568,63 @@ async def test_camera_update_license_plate(
assert state
assert state.state == "ABCD1234"
assert len(state_changes) == 1
# ensure reply is ignored
ufp.ws_msg(mock_msg)
await hass.async_block_till_done()
assert len(state_changes) == 1
event = Event(
model=ModelType.EVENT,
id="test_event_id",
type=EventType.SMART_DETECT,
start=fixed_now - timedelta(seconds=1),
end=fixed_now + timedelta(seconds=1),
score=100,
smart_detect_types=[SmartDetectObjectType.LICENSE_PLATE],
smart_detect_event_ids=[],
metadata=event_metadata,
api=ufp.api,
)
ufp.api.bootstrap.events = {event.id: event}
new_camera.last_smart_detect_event_ids[SmartDetectObjectType.LICENSE_PLATE] = (
event.id
)
ufp.ws_msg(mock_msg)
await hass.async_block_till_done()
assert len(state_changes) == 2
state = hass.states.get(entity_id)
assert state
assert state.state == "none"
# Now send a new event with end already set
event = Event(
model=ModelType.EVENT,
id="new_event",
type=EventType.SMART_DETECT,
start=fixed_now - timedelta(seconds=1),
end=fixed_now + timedelta(seconds=1),
score=100,
smart_detect_types=[SmartDetectObjectType.LICENSE_PLATE],
smart_detect_event_ids=[],
metadata=event_metadata,
api=ufp.api,
)
ufp.api.bootstrap.events = {event.id: event}
new_camera.last_smart_detect_event_ids[SmartDetectObjectType.LICENSE_PLATE] = (
event.id
)
ufp.ws_msg(mock_msg)
await hass.async_block_till_done()
assert len(state_changes) == 4
assert state_changes[2].data["new_state"].state == "ABCD1234"
state = hass.states.get(entity_id)
assert state
assert state.state == "none"
async def test_sensor_precision(
hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor, fixed_now: datetime

View File

@ -59,11 +59,11 @@ async def test_switch_camera_remove(
ufp.api.bootstrap.nvr.system_info.ustorage = None
await init_entry(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.SWITCH, 16, 14)
assert_entity_counts(hass, Platform.SWITCH, 17, 15)
await remove_entities(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.SWITCH, 2, 2)
await adopt_devices(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.SWITCH, 16, 14)
assert_entity_counts(hass, Platform.SWITCH, 17, 15)
async def test_switch_light_remove(
@ -175,7 +175,7 @@ async def test_switch_setup_camera_all(
"""Test switch entity setup for camera devices (all enabled feature flags)."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 16, 14)
assert_entity_counts(hass, Platform.SWITCH, 17, 15)
for description in CAMERA_SWITCHES_BASIC:
unique_id, entity_id = ids_from_device_description(
@ -295,7 +295,7 @@ async def test_switch_camera_ssh(
"""Tests SSH switch for cameras."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 16, 14)
assert_entity_counts(hass, Platform.SWITCH, 17, 15)
description = CAMERA_SWITCHES[0]
@ -328,7 +328,7 @@ async def test_switch_camera_simple(
"""Tests all simple switches for cameras."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 16, 14)
assert_entity_counts(hass, Platform.SWITCH, 17, 15)
assert description.ufp_set_method is not None
@ -357,7 +357,7 @@ async def test_switch_camera_highfps(
"""Tests High FPS switch for cameras."""
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 16, 14)
assert_entity_counts(hass, Platform.SWITCH, 17, 15)
description = CAMERA_SWITCHES[3]
@ -388,7 +388,7 @@ async def test_switch_camera_privacy(
previous_record = doorbell.recording_settings.mode = RecordingMode.DETECTIONS
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 16, 14)
assert_entity_counts(hass, Platform.SWITCH, 17, 15)
description = PRIVACY_MODE_SWITCH
@ -440,7 +440,7 @@ async def test_switch_camera_privacy_already_on(
doorbell.add_privacy_zone()
await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 16, 14)
assert_entity_counts(hass, Platform.SWITCH, 17, 15)
description = PRIVACY_MODE_SWITCH