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: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device) super()._async_update_device_from_protect(device)
description = self.entity_description 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 is_on := bool(description.get_ufp_value(device)):
if event: if event:
self._set_event_attrs(event) self._set_event_attrs(event)
@ -737,25 +737,26 @@ class ProtectSmartEventBinarySensor(EventEntityMixin, BinarySensorEntity):
@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)
description = self.entity_description 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 ( if not (
(event := self._event) event
and not self._event_already_ended(prev_event) and not self._event_already_ended(prev_event, prev_event_end)
and description.has_matching_smart(event) and description.has_matching_smart(event)
and ((is_end := event.end) or self.device.is_smart_detected) and ((is_end := event.end) or self.device.is_smart_detected)
): ):
self._set_event_done() self._set_event_done()
return return
was_on = self._attr_is_on
self._attr_is_on = True self._attr_is_on = True
self._set_event_attrs(event) self._set_event_attrs(event)
if is_end:
if is_end and not was_on:
self._async_event_with_immediate_end() self._async_event_with_immediate_end()

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable, Sequence from collections.abc import Callable, Sequence
from datetime import datetime
from functools import partial from functools import partial
import logging import logging
from operator import attrgetter from operator import attrgetter
@ -303,6 +304,7 @@ class EventEntityMixin(ProtectDeviceEntity):
entity_description: ProtectEventMixin entity_description: ProtectEventMixin
_unrecorded_attributes = frozenset({ATTR_EVENT_ID, ATTR_EVENT_SCORE}) _unrecorded_attributes = frozenset({ATTR_EVENT_ID, ATTR_EVENT_SCORE})
_event: Event | None = None _event: Event | None = None
_event_end: datetime | None = None
@callback @callback
def _set_event_done(self) -> None: def _set_event_done(self) -> None:
@ -326,6 +328,21 @@ class EventEntityMixin(ProtectDeviceEntity):
self.async_write_ha_state() self.async_write_ha_state()
@callback @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 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 @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)
description = self.entity_description 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 ( if not (
(event := self._event) event
and not self._event_already_ended(prev_event) and not self._event_already_ended(prev_event, prev_event_end)
and description.has_matching_smart(event) and description.has_matching_smart(event)
and ((is_end := event.end) or self.device.is_smart_detected) and ((is_end := event.end) or self.device.is_smart_detected)
and (metadata := event.metadata) and (metadata := event.metadata)
@ -773,9 +776,7 @@ class ProtectLicensePlateEventSensor(ProtectEventSensor):
self._set_event_done() self._set_event_done()
return return
previous_plate = self._attr_native_value
self._attr_native_value = license_plate.name self._attr_native_value = license_plate.name
self._set_event_attrs(event) self._set_event_attrs(event)
if is_end:
if is_end and previous_plate != license_plate.name:
self._async_event_with_immediate_end() self._async_event_with_immediate_end()

View File

@ -626,6 +626,93 @@ async def test_camera_update_license_plate(
assert state.state == "none" 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( 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
) -> None: ) -> None: