mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 00:37:53 +00:00
Fix unifiprotect smart detection when end is set (#120027)
This commit is contained in:
parent
ecadaf314d
commit
68462b014c
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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]):
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user