From 0c3a5ae5da7b4bfd670c9469e2ef7355e1026b89 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Jun 2024 16:17:31 -0500 Subject: [PATCH] Dispatch unifiprotect websocket messages based on model (#119633) --- homeassistant/components/unifiprotect/data.py | 63 ++++++++++--------- .../unifiprotect/test_binary_sensor.py | 14 +++-- .../unifiprotect/test_media_source.py | 23 +++++++ .../components/unifiprotect/test_recorder.py | 3 +- tests/components/unifiprotect/test_sensor.py | 12 +++- tests/components/unifiprotect/test_views.py | 12 +++- 6 files changed, 91 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 97f3a4129ae..59a5242273a 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -6,7 +6,7 @@ from collections.abc import Callable, Iterable from datetime import datetime, timedelta from functools import partial import logging -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast from typing_extensions import Generator from uiprotect import ProtectApiClient @@ -16,7 +16,6 @@ from uiprotect.data import ( Camera, Event, EventType, - Liveview, ModelType, ProtectAdoptableDeviceModel, WSSubscriptionMessage, @@ -231,41 +230,49 @@ class ProtectData: @callback def _async_process_ws_message(self, message: WSSubscriptionMessage) -> None: - if message.new_obj is None: + """Process a message from the websocket.""" + if (new_obj := message.new_obj) is None: if isinstance(message.old_obj, ProtectAdoptableDeviceModel): self._async_remove_device(message.old_obj) return - obj = message.new_obj - if isinstance(obj, (ProtectAdoptableDeviceModel, NVR)): - if message.old_obj is None and isinstance(obj, ProtectAdoptableDeviceModel): - self._async_add_device(obj) - elif getattr(obj, "is_adopted_by_us", True): - self._async_update_device(obj, message.changed_data) - - # trigger updates for camera that the event references - elif isinstance(obj, Event): + model_type = new_obj.model + if model_type is ModelType.EVENT: + if TYPE_CHECKING: + assert isinstance(new_obj, Event) if _LOGGER.isEnabledFor(logging.DEBUG): - log_event(obj) - if obj.type is EventType.DEVICE_ADOPTED: - if obj.metadata is not None and obj.metadata.device_id is not None: - device = self.api.bootstrap.get_device_from_id( - obj.metadata.device_id - ) - if device is not None: - self._async_add_device(device) - elif obj.camera is not None: - self._async_signal_device_update(obj.camera) - elif obj.light is not None: - self._async_signal_device_update(obj.light) - elif obj.sensor is not None: - self._async_signal_device_update(obj.sensor) - # alert user viewport needs restart so voice clients can get new options - elif len(self.api.bootstrap.viewers) > 0 and isinstance(obj, Liveview): + log_event(new_obj) + if ( + (new_obj.type is EventType.DEVICE_ADOPTED) + and (metadata := new_obj.metadata) + and (device_id := metadata.device_id) + and (device := self.api.bootstrap.get_device_from_id(device_id)) + ): + self._async_add_device(device) + elif camera := new_obj.camera: + self._async_signal_device_update(camera) + elif light := new_obj.light: + self._async_signal_device_update(light) + elif sensor := new_obj.sensor: + self._async_signal_device_update(sensor) + return + + if model_type is ModelType.LIVEVIEW and len(self.api.bootstrap.viewers) > 0: + # alert user viewport needs restart so voice clients can get new options _LOGGER.warning( "Liveviews updated. Restart Home Assistant to update Viewport select" " options" ) + return + + if message.old_obj is None and isinstance(new_obj, ProtectAdoptableDeviceModel): + self._async_add_device(new_obj) + return + + if getattr(new_obj, "is_adopted_by_us", True) and hasattr(new_obj, "mac"): + if TYPE_CHECKING: + assert isinstance(new_obj, (ProtectAdoptableDeviceModel, NVR)) + self._async_update_device(new_obj, message.changed_data) @callback def _async_process_updates(self, updates: Bootstrap | None) -> None: diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index b23fd529233..3231c233ca3 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -5,7 +5,7 @@ from __future__ import annotations from datetime import datetime, timedelta from unittest.mock import Mock -from uiprotect.data import Camera, Event, EventType, Light, MountType, Sensor +from uiprotect.data import Camera, Event, EventType, Light, ModelType, MountType, Sensor from uiprotect.data.nvr import EventMetadata from homeassistant.components.binary_sensor import BinarySensorDeviceClass @@ -281,6 +281,7 @@ async def test_binary_sensor_update_motion( ) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=1), @@ -289,19 +290,21 @@ async def test_binary_sensor_update_motion( smart_detect_types=[], smart_detect_event_ids=[], camera_id=doorbell.id, + api=ufp.api, ) new_camera = doorbell.copy() new_camera.is_motion_detected = True new_camera.last_motion_event_id = 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} + + 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) @@ -325,6 +328,7 @@ async def test_binary_sensor_update_light_motion( event_metadata = EventMetadata(light_id=light.id) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION_LIGHT, start=fixed_now - timedelta(seconds=1), diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py index 2cdebeafb04..60cd3150884 100644 --- a/tests/components/unifiprotect/test_media_source.py +++ b/tests/components/unifiprotect/test_media_source.py @@ -10,6 +10,7 @@ from uiprotect.data import ( Camera, Event, EventType, + ModelType, Permission, SmartDetectObjectType, ) @@ -72,6 +73,7 @@ async def test_resolve_media_thumbnail( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -103,6 +105,7 @@ async def test_resolve_media_event( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -172,6 +175,7 @@ async def test_browse_media_event_ongoing( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -591,6 +595,7 @@ async def test_browse_media_recent( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -628,6 +633,7 @@ async def test_browse_media_recent_truncated( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -660,6 +666,7 @@ async def test_browse_media_recent_truncated( [ ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.RING, start=datetime(1000, 1, 1, 0, 0, 0), @@ -673,6 +680,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=datetime(1000, 1, 1, 0, 0, 0), @@ -686,6 +694,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=datetime(1000, 1, 1, 0, 0, 0), @@ -708,6 +717,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=datetime(1000, 1, 1, 0, 0, 0), @@ -721,6 +731,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=datetime(1000, 1, 1, 0, 0, 0), @@ -734,6 +745,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=datetime(1000, 1, 1, 0, 0, 0), @@ -757,6 +769,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=datetime(1000, 1, 1, 0, 0, 0), @@ -786,6 +799,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=datetime(1000, 1, 1, 0, 0, 0), @@ -820,6 +834,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=datetime(1000, 1, 1, 0, 0, 0), @@ -852,6 +867,7 @@ async def test_browse_media_recent_truncated( ), ( Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_AUDIO_DETECT, start=datetime(1000, 1, 1, 0, 0, 0), @@ -906,6 +922,7 @@ async def test_browse_media_eventthumb( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=fixed_now - timedelta(seconds=20), @@ -969,6 +986,7 @@ async def test_browse_media_browse_day( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -1010,6 +1028,7 @@ async def test_browse_media_browse_whole_month( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -1052,6 +1071,7 @@ async def test_browse_media_browse_whole_month_december( await init_entry(hass, ufp, [doorbell], regenerate_ids=False) event1 = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SMART_DETECT, start=fixed_now - timedelta(seconds=3663), @@ -1063,6 +1083,7 @@ async def test_browse_media_browse_whole_month_december( ) event1._api = ufp.api event2 = Event( + model=ModelType.EVENT, id="test_event_id2", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -1074,6 +1095,7 @@ async def test_browse_media_browse_whole_month_december( ) event2._api = ufp.api event3 = Event( + model=ModelType.EVENT, id="test_event_id3", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), @@ -1085,6 +1107,7 @@ async def test_browse_media_browse_whole_month_december( ) event3._api = ufp.api event4 = Event( + model=ModelType.EVENT, id="test_event_id4", type=EventType.MOTION, start=fixed_now - timedelta(seconds=20), diff --git a/tests/components/unifiprotect/test_recorder.py b/tests/components/unifiprotect/test_recorder.py index 94c93413de5..fe102c2fdbc 100644 --- a/tests/components/unifiprotect/test_recorder.py +++ b/tests/components/unifiprotect/test_recorder.py @@ -5,7 +5,7 @@ from __future__ import annotations from datetime import datetime, timedelta from unittest.mock import Mock -from uiprotect.data import Camera, Event, EventType +from uiprotect.data import Camera, Event, EventType, ModelType from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states @@ -40,6 +40,7 @@ async def test_exclude_attributes( ) event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.MOTION, start=fixed_now - timedelta(seconds=1), diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index 72915936a70..1a1374390ae 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -6,7 +6,15 @@ from datetime import datetime, timedelta from unittest.mock import Mock import pytest -from uiprotect.data import NVR, Camera, Event, EventType, Sensor, SmartDetectObjectType +from uiprotect.data import ( + NVR, + Camera, + Event, + EventType, + ModelType, + Sensor, + SmartDetectObjectType, +) from uiprotect.data.nvr import EventMetadata, LicensePlateMetadata from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION @@ -438,6 +446,7 @@ async def test_sensor_update_alarm( event_metadata = EventMetadata(sensor_id=sensor_all.id, alarm_type="smoke") event = Event( + model=ModelType.EVENT, id="test_event_id", type=EventType.SENSOR_ALARM, start=fixed_now - timedelta(seconds=1), @@ -521,6 +530,7 @@ async def test_camera_update_licenseplate( 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), diff --git a/tests/components/unifiprotect/test_views.py b/tests/components/unifiprotect/test_views.py index 2b80a41b16f..fed0a98552d 100644 --- a/tests/components/unifiprotect/test_views.py +++ b/tests/components/unifiprotect/test_views.py @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, Mock from aiohttp import ClientResponse import pytest -from uiprotect.data import Camera, Event, EventType +from uiprotect.data import Camera, Event, EventType, ModelType from uiprotect.exceptions import ClientError from homeassistant.components.unifiprotect.views import ( @@ -179,6 +179,7 @@ async def test_video_bad_event( await init_entry(hass, ufp, [camera]) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id="test_id", start=fixed_now - timedelta(seconds=30), @@ -205,6 +206,7 @@ async def test_video_bad_event_ongoing( await init_entry(hass, ufp, [camera]) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=fixed_now - timedelta(seconds=30), @@ -232,6 +234,7 @@ async def test_video_bad_perms( await init_entry(hass, ufp, [camera]) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=fixed_now - timedelta(seconds=30), @@ -260,6 +263,7 @@ async def test_video_bad_nvr_id( await init_entry(hass, ufp, [camera]) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=fixed_now - timedelta(seconds=30), @@ -294,6 +298,7 @@ async def test_video_bad_camera_id( await init_entry(hass, ufp, [camera]) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=fixed_now - timedelta(seconds=30), @@ -328,6 +333,7 @@ async def test_video_bad_camera_perms( await init_entry(hass, ufp, [camera]) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=fixed_now - timedelta(seconds=30), @@ -368,6 +374,7 @@ async def test_video_bad_params( event_start = fixed_now - timedelta(seconds=30) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=event_start, @@ -405,6 +412,7 @@ async def test_video_bad_video( event_start = fixed_now - timedelta(seconds=30) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=event_start, @@ -447,6 +455,7 @@ async def test_video( event_start = fixed_now - timedelta(seconds=30) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=event_start, @@ -490,6 +499,7 @@ async def test_video_entity_id( event_start = fixed_now - timedelta(seconds=30) event = Event( + model=ModelType.EVENT, api=ufp.api, camera_id=camera.id, start=event_start,