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

View File

@ -305,13 +305,27 @@ class EventEntityMixin(ProtectDeviceEntity):
_event: Event | None = None _event: Event | None = None
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _set_event_done(self) -> None:
if (event := self.entity_description.get_event_obj(device)) is None: """Clear the event and state."""
self._attr_extra_state_attributes = {}
else: @callback
self._attr_extra_state_attributes = { def _set_event_attrs(self, event: Event) -> None:
ATTR_EVENT_ID: event.id, """Set event attrs."""
ATTR_EVENT_SCORE: event.score, self._attr_extra_state_attributes = {
} ATTR_EVENT_ID: event.id,
self._event = event ATTR_EVENT_SCORE: event.score,
super()._async_update_device_from_protect(device) }
@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 operator import attrgetter
from typing import Any, Generic, TypeVar 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 from homeassistant.helpers.entity import EntityDescription
@ -79,21 +84,24 @@ class ProtectEventMixin(ProtectEntityDescription[T]):
"""Mixin for events.""" """Mixin for events."""
ufp_event_obj: str | None = None ufp_event_obj: str | None = None
ufp_obj_type: SmartDetectObjectType | None = None
def get_event_obj(self, obj: T) -> Event | None: def get_event_obj(self, obj: T) -> Event | None:
"""Return value from UniFi Protect device.""" """Return value from UniFi Protect device."""
return None 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: def __post_init__(self) -> None:
"""Override get_event_obj if ufp_event_obj is set.""" """Override get_event_obj if ufp_event_obj is set."""
if (_ufp_event_obj := self.ufp_event_obj) is not None: if (_ufp_event_obj := self.ufp_event_obj) is not None:
object.__setattr__(self, "get_event_obj", attrgetter(_ufp_event_obj)) object.__setattr__(self, "get_event_obj", attrgetter(_ufp_event_obj))
super().__post_init__() 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) @dataclass(frozen=True, kw_only=True)
class ProtectSetableKeysMixin(ProtectEntityDescription[T]): class ProtectSetableKeysMixin(ProtectEntityDescription[T]):

View File

@ -18,6 +18,7 @@ from uiprotect.data import (
ProtectDeviceModel, ProtectDeviceModel,
ProtectModelWithId, ProtectModelWithId,
Sensor, Sensor,
SmartDetectObjectType,
) )
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@ -542,7 +543,7 @@ LICENSE_PLATE_EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = (
name="License plate detected", name="License plate detected",
icon="mdi:car", icon="mdi:car",
translation_key="license_plate", 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_required_field="can_detect_license_plate",
ufp_event_obj="last_license_plate_detect_event", ufp_event_obj="last_license_plate_detect_event",
), ),
@ -747,19 +748,34 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity):
class ProtectLicensePlateEventSensor(ProtectEventSensor): class ProtectLicensePlateEventSensor(ProtectEventSensor):
"""A UniFi Protect license plate sensor.""" """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 @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
prev_event = self._event
super()._async_update_device_from_protect(device) super()._async_update_device_from_protect(device)
event = self._event description = self.entity_description
entity_description = self.entity_description self._event = description.get_event_obj(device)
if (
event is None if not (
or (event.metadata is None or event.metadata.license_plate is None) (event := self._event)
or not entity_description.get_is_on(self.device, 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._set_event_done()
self._event = None
self._attr_extra_state_attributes = {}
return 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.PERSON,
SmartDetectObjectType.VEHICLE, SmartDetectObjectType.VEHICLE,
SmartDetectObjectType.ANIMAL, SmartDetectObjectType.ANIMAL,
SmartDetectObjectType.PACKAGE,
] ]
doorbell.has_speaker = True doorbell.has_speaker = True
doorbell.feature_flags.has_hdr = True doorbell.feature_flags.has_hdr = True

View File

@ -5,7 +5,17 @@ from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import Mock 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 uiprotect.data.nvr import EventMetadata
from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.binary_sensor import BinarySensorDeviceClass
@ -15,6 +25,7 @@ from homeassistant.components.unifiprotect.binary_sensor import (
LIGHT_SENSORS, LIGHT_SENSORS,
MOUNTABLE_SENSE_SENSORS, MOUNTABLE_SENSE_SENSORS,
SENSE_SENSORS, SENSE_SENSORS,
SMART_EVENT_SENSORS,
) )
from homeassistant.components.unifiprotect.const import ( from homeassistant.components.unifiprotect.const import (
ATTR_EVENT_SCORE, ATTR_EVENT_SCORE,
@ -23,12 +34,13 @@ from homeassistant.components.unifiprotect.const import (
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS, ATTR_DEVICE_CLASS,
EVENT_STATE_CHANGED,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
Platform, Platform,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import Event as HAEvent, EventStateChangedData, HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .utils import ( from .utils import (
@ -40,6 +52,8 @@ from .utils import (
remove_entities, remove_entities,
) )
from tests.common import async_capture_events
LIGHT_SENSOR_WRITE = LIGHT_SENSORS[:2] LIGHT_SENSOR_WRITE = LIGHT_SENSORS[:2]
SENSE_SENSORS_WRITE = SENSE_SENSORS[:3] 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 ufp.api.bootstrap.nvr.system_info.ustorage = None
await init_entry(hass, ufp, [doorbell, unadopted_camera]) 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]) await remove_entities(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.BINARY_SENSOR, 0, 0) assert_entity_counts(hass, Platform.BINARY_SENSOR, 0, 0)
await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) 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( 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 ufp.api.bootstrap.nvr.system_info.ustorage = None
await init_entry(hass, ufp, [doorbell, unadopted_camera]) 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] description = EVENT_SENSORS[0]
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
@ -273,7 +287,7 @@ async def test_binary_sensor_update_motion(
"""Test binary_sensor motion entity.""" """Test binary_sensor motion entity."""
await init_entry(hass, ufp, [doorbell, unadopted_camera]) 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( _, entity_id = ids_from_device_description(
Platform.BINARY_SENSOR, doorbell, EVENT_SENSORS[1] Platform.BINARY_SENSOR, doorbell, EVENT_SENSORS[1]
@ -421,3 +435,144 @@ async def test_binary_sensor_update_mount_type_garage(
assert ( assert (
state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.GARAGE_DOOR.value 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 ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
EVENT_STATE_CHANGED,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
Platform, Platform,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import Event as HAEvent, EventStateChangedData, HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .utils import ( from .utils import (
@ -49,6 +50,8 @@ from .utils import (
time_changed, time_changed,
) )
from tests.common import async_capture_events
CAMERA_SENSORS_WRITE = CAMERA_SENSORS[:5] CAMERA_SENSORS_WRITE = CAMERA_SENSORS[:5]
SENSE_SENSORS_WRITE = SENSE_SENSORS[:8] 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.cameras = {new_camera.id: new_camera}
ufp.api.bootstrap.events = {event.id: event} ufp.api.bootstrap.events = {event.id: event}
state_changes: list[HAEvent[EventStateChangedData]] = async_capture_events(
hass, EVENT_STATE_CHANGED
)
ufp.ws_msg(mock_msg) ufp.ws_msg(mock_msg)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -561,6 +568,63 @@ async def test_camera_update_license_plate(
assert state assert state
assert state.state == "ABCD1234" 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( async def test_sensor_precision(
hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor, fixed_now: datetime 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 ufp.api.bootstrap.nvr.system_info.ustorage = None
await init_entry(hass, ufp, [doorbell, unadopted_camera]) 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]) await remove_entities(hass, ufp, [doorbell, unadopted_camera])
assert_entity_counts(hass, Platform.SWITCH, 2, 2) assert_entity_counts(hass, Platform.SWITCH, 2, 2)
await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) 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( 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).""" """Test switch entity setup for camera devices (all enabled feature flags)."""
await init_entry(hass, ufp, [doorbell]) 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: for description in CAMERA_SWITCHES_BASIC:
unique_id, entity_id = ids_from_device_description( unique_id, entity_id = ids_from_device_description(
@ -295,7 +295,7 @@ async def test_switch_camera_ssh(
"""Tests SSH switch for cameras.""" """Tests SSH switch for cameras."""
await init_entry(hass, ufp, [doorbell]) 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] description = CAMERA_SWITCHES[0]
@ -328,7 +328,7 @@ async def test_switch_camera_simple(
"""Tests all simple switches for cameras.""" """Tests all simple switches for cameras."""
await init_entry(hass, ufp, [doorbell]) 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 assert description.ufp_set_method is not None
@ -357,7 +357,7 @@ async def test_switch_camera_highfps(
"""Tests High FPS switch for cameras.""" """Tests High FPS switch for cameras."""
await init_entry(hass, ufp, [doorbell]) 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] description = CAMERA_SWITCHES[3]
@ -388,7 +388,7 @@ async def test_switch_camera_privacy(
previous_record = doorbell.recording_settings.mode = RecordingMode.DETECTIONS previous_record = doorbell.recording_settings.mode = RecordingMode.DETECTIONS
await init_entry(hass, ufp, [doorbell]) 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 description = PRIVACY_MODE_SWITCH
@ -440,7 +440,7 @@ async def test_switch_camera_privacy_already_on(
doorbell.add_privacy_zone() doorbell.add_privacy_zone()
await init_entry(hass, ufp, [doorbell]) 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 description = PRIVACY_MODE_SWITCH