Fix comparing end of event in unifiprotect (#120124)

This commit is contained in:
J. Nick Koston 2024-06-21 14:48:09 -05:00 committed by GitHub
parent 8b4a5042bb
commit c3ab72a1f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 125 additions and 19 deletions

View File

@ -714,7 +714,7 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity):
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device)
description = self.entity_description
event = self._event = self.entity_description.get_event_obj(device)
event = self.entity_description.get_event_obj(device)
if is_on := bool(description.get_ufp_value(device)):
if event:
self._set_event_attrs(event)
@ -737,25 +737,26 @@ class ProtectSmartEventBinarySensor(EventEntityMixin, BinarySensorEntity):
@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)
prev_event = self._event
prev_event_end = self._event_end
super()._async_update_device_from_protect(device)
event = self._event = description.get_event_obj(device)
self._event_end = event.end if event else None
if not (
(event := self._event)
and not self._event_already_ended(prev_event)
event
and not self._event_already_ended(prev_event, prev_event_end)
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:
if is_end:
self._async_event_with_immediate_end()

View File

@ -3,6 +3,7 @@
from __future__ import annotations
from collections.abc import Callable, Sequence
from datetime import datetime
from functools import partial
import logging
from operator import attrgetter
@ -303,6 +304,7 @@ class EventEntityMixin(ProtectDeviceEntity):
entity_description: ProtectEventMixin
_unrecorded_attributes = frozenset({ATTR_EVENT_ID, ATTR_EVENT_SCORE})
_event: Event | None = None
_event_end: datetime | None = None
@callback
def _set_event_done(self) -> None:
@ -326,6 +328,21 @@ class EventEntityMixin(ProtectDeviceEntity):
self.async_write_ha_state()
@callback
def _event_already_ended(self, prev_event: Event | None) -> bool:
def _event_already_ended(
self, prev_event: Event | None, prev_event_end: datetime | None
) -> bool:
"""Determine if the event has already ended.
The event_end time is passed because the prev_event and event object
may be the same object, and the uiprotect code will mutate the
event object so we need to check the datetime object that was
saved from the last time the entity was updated.
"""
event = self._event
return bool(event and event.end and prev_event and prev_event.id == event.id)
return bool(
event
and event.end
and prev_event
and prev_event_end
and prev_event.id == event.id
)

View File

@ -757,14 +757,17 @@ class ProtectLicensePlateEventSensor(ProtectEventSensor):
@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)
prev_event = self._event
prev_event_end = self._event_end
super()._async_update_device_from_protect(device)
event = self._event = description.get_event_obj(device)
self._event_end = event.end if event else None
if not (
(event := self._event)
and not self._event_already_ended(prev_event)
event
and not self._event_already_ended(prev_event, prev_event_end)
and description.has_matching_smart(event)
and ((is_end := event.end) or self.device.is_smart_detected)
and (metadata := event.metadata)
@ -773,9 +776,7 @@ class ProtectLicensePlateEventSensor(ProtectEventSensor):
self._set_event_done()
return
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:
if is_end:
self._async_event_with_immediate_end()

View File

@ -626,6 +626,93 @@ async def test_camera_update_license_plate(
assert state.state == "none"
async def test_camera_update_license_plate_changes_number_during_detect(
hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera, fixed_now: datetime
) -> None:
"""Test license plate sensor that changes number during detect."""
camera.feature_flags.smart_detect_types.append(SmartDetectObjectType.LICENSE_PLATE)
camera.feature_flags.has_smart_detect = True
camera.smart_detect_settings.object_types.append(
SmartDetectObjectType.LICENSE_PLATE
)
await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.SENSOR, 23, 13)
_, entity_id = ids_from_device_description(
Platform.SENSOR, camera, LICENSE_PLATE_EVENT_SENSORS[0]
)
event_metadata = EventMetadata(
license_plate=LicensePlateMetadata(name="ABCD1234", confidence_level=95)
)
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.LICENSE_PLATE],
smart_detect_event_ids=[],
metadata=event_metadata,
api=ufp.api,
)
new_camera = camera.copy()
new_camera.is_smart_detected = True
new_camera.last_smart_detect_event_ids[SmartDetectObjectType.LICENSE_PLATE] = (
event.id
)
mock_msg = Mock()
mock_msg.changed_data = {}
mock_msg.new_obj = new_camera
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()
state = hass.states.get(entity_id)
assert state
assert state.state == "ABCD1234"
assert len(state_changes) == 1
ufp.ws_msg(mock_msg)
await hass.async_block_till_done()
assert len(state_changes) == 1
# Now mutate the original event so it ends
# Also change the metadata to a different license plate
# since the model may not get the plate correct on
# the first update.
event.score = 99
event.end = fixed_now + timedelta(seconds=1)
event_metadata.license_plate.name = "DCBA4321"
ufp.api.bootstrap.events = {event.id: event}
ufp.ws_msg(mock_msg)
await hass.async_block_till_done()
assert len(state_changes) == 3
state = hass.states.get(entity_id)
assert state
assert state.state == "none"
assert state_changes[0].data["new_state"].state == "ABCD1234"
assert state_changes[1].data["new_state"].state == "DCBA4321"
assert state_changes[2].data["new_state"].state == "none"
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
) -> None: