From bbe4a67a98cb1710f1e439e998fac3672c71bd1d Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Dec 2021 23:59:24 -0800 Subject: [PATCH] Coalesce nest media source preview clips by session and bump google-nest-sdm (#61081) --- homeassistant/components/nest/__init__.py | 2 +- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/media_source.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nest/test_events.py | 12 +-- tests/components/nest/test_media_source.py | 74 +++++++++++-------- 7 files changed, 56 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 382edb80ca5..eebbdcf026a 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -198,7 +198,7 @@ class SignalUpdateCallback: "device_id": device_entry.id, "type": event_type, "timestamp": event_message.timestamp, - "nest_event_id": image_event.event_id, + "nest_event_id": image_event.event_session_id, } self._hass.bus.async_fire(NEST_EVENT, message) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index a82f8395733..b9f20e92670 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http", "media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.3"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.4.4"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 4f6cd8147d9..8fd7d384e36 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -181,7 +181,7 @@ class NestMediaSource(MediaSource): browse_device.children = [] events = await _get_events(device) for child_event in events.values(): - event_id = MediaId(media_id.device_id, child_event.event_id) + event_id = MediaId(media_id.device_id, child_event.event_session_id) browse_device.children.append( _browse_event(event_id, device, child_event) ) @@ -203,7 +203,7 @@ class NestMediaSource(MediaSource): async def _get_events(device: Device) -> Mapping[str, ImageEventBase]: """Return relevant events for the specified device.""" events = await device.event_media_manager.async_events() - return {e.event_id: e for e in events} + return {e.event_session_id: e for e in events} def _browse_root() -> BrowseMediaSource: diff --git a/requirements_all.txt b/requirements_all.txt index 501ab2f4819..89896a6dbc8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -741,7 +741,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.4.3 +google-nest-sdm==0.4.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0ba7da5d24..3da27e4926b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -464,7 +464,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.4.3 +google-nest-sdm==0.4.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 6e9dd7dd40d..a2f5c21fdac 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -117,7 +117,7 @@ async def test_doorbell_chime_event(hass): "device_id": entry.device_id, "type": "doorbell_chime", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } @@ -145,7 +145,7 @@ async def test_camera_motion_event(hass): "device_id": entry.device_id, "type": "camera_motion", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } @@ -173,7 +173,7 @@ async def test_camera_sound_event(hass): "device_id": entry.device_id, "type": "camera_sound", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } @@ -201,7 +201,7 @@ async def test_camera_person_event(hass): "device_id": entry.device_id, "type": "camera_person", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } @@ -238,13 +238,13 @@ async def test_camera_multiple_event(hass): "device_id": entry.device_id, "type": "camera_motion", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } assert events[1].data == { "device_id": entry.device_id, "type": "camera_person", "timestamp": event_time, - "nest_event_id": EVENT_ID, + "nest_event_id": EVENT_SESSION_ID, } diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 82c87579525..22ed0721eb2 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -27,6 +27,7 @@ DEVICE_ID = "example/api/device/id" DEVICE_NAME = "Front" PLATFORM = "camera" NEST_EVENT = "nest_event" +EVENT_ID = "1aXEvi9ajKVTdDsXdJda8fzfCa..." EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..." CAMERA_DEVICE_TYPE = "sdm.devices.types.CAMERA" CAMERA_TRAITS = { @@ -81,26 +82,28 @@ async def async_setup_devices(hass, auth, device_type, traits={}, events=[]): return subscriber -def create_event(event_id, event_type, timestamp=None, device_id=None): +def create_event( + event_session_id, event_id, event_type, timestamp=None, device_id=None +): """Create an EventMessage for a single event type.""" if not timestamp: timestamp = dt_util.now() event_data = { event_type: { - "eventSessionId": EVENT_SESSION_ID, + "eventSessionId": event_session_id, "eventId": event_id, }, } - return create_event_message(event_id, event_data, timestamp, device_id=device_id) + return create_event_message(event_data, timestamp, device_id=device_id) -def create_event_message(event_id, event_data, timestamp, device_id=None): +def create_event_message(event_data, timestamp, device_id=None): """Create an EventMessage for a single event type.""" if device_id is None: device_id = DEVICE_ID return EventMessage( { - "eventId": f"{event_id}-{timestamp}", + "eventId": f"{EVENT_ID}-{timestamp}", "timestamp": timestamp.isoformat(timespec="seconds"), "resourceUpdate": { "name": device_id, @@ -163,7 +166,6 @@ async def test_supported_device(hass, auth): async def test_camera_event(hass, auth, hass_client): """Test a media source and image created for an event.""" - event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp = dt_util.now() await async_setup_devices( hass, @@ -172,7 +174,8 @@ async def test_camera_event(hass, auth, hass_client): CAMERA_TRAITS, events=[ create_event( - event_id, + EVENT_SESSION_ID, + EVENT_ID, PERSON_EVENT, timestamp=event_timestamp, ), @@ -213,7 +216,7 @@ async def test_camera_event(hass, auth, hass_client): # The device expands recent events assert len(browse.children) == 1 assert browse.children[0].domain == DOMAIN - assert browse.children[0].identifier == f"{device.id}/{event_id}" + assert browse.children[0].identifier == f"{device.id}/{EVENT_SESSION_ID}" event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Person @ {event_timestamp_string}" assert not browse.children[0].can_expand @@ -221,19 +224,19 @@ async def test_camera_event(hass, auth, hass_client): # Browse to the event browse = await media_source.async_browse_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_id}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}" ) assert browse.domain == DOMAIN - assert browse.identifier == f"{device.id}/{event_id}" + assert browse.identifier == f"{device.id}/{EVENT_SESSION_ID}" assert "Person" in browse.title assert not browse.can_expand assert not browse.children # Resolving the event links to the media media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_id}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}" ) - assert media.url == f"/api/nest/event_media/{device.id}/{event_id}" + assert media.url == f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}" assert media.mime_type == "image/jpeg" auth.responses = [ @@ -250,9 +253,9 @@ async def test_camera_event(hass, auth, hass_client): async def test_event_order(hass, auth): """Test multiple events are in descending timestamp order.""" - event_id1 = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." + event_session_id1 = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp1 = dt_util.now() - event_id2 = "GXXWRWVeHNUlUU3V3MGV3bUOYW..." + event_session_id2 = "GXXWRWVeHNUlUU3V3MGV3bUOYW..." event_timestamp2 = event_timestamp1 + datetime.timedelta(seconds=5) await async_setup_devices( hass, @@ -261,12 +264,14 @@ async def test_event_order(hass, auth): CAMERA_TRAITS, events=[ create_event( - event_id1, + event_session_id1, + EVENT_ID + "1", PERSON_EVENT, timestamp=event_timestamp1, ), create_event( - event_id2, + event_session_id2, + EVENT_ID + "2", MOTION_EVENT, timestamp=event_timestamp2, ), @@ -293,7 +298,7 @@ async def test_event_order(hass, auth): # Motion event is most recent assert len(browse.children) == 2 assert browse.children[0].domain == DOMAIN - assert browse.children[0].identifier == f"{device.id}/{event_id2}" + assert browse.children[0].identifier == f"{device.id}/{event_session_id2}" event_timestamp_string = event_timestamp2.strftime(DATE_STR_FORMAT) assert browse.children[0].title == f"Motion @ {event_timestamp_string}" assert not browse.children[0].can_expand @@ -301,7 +306,7 @@ async def test_event_order(hass, auth): # Person event is next assert browse.children[1].domain == DOMAIN - assert browse.children[1].identifier == f"{device.id}/{event_id1}" + assert browse.children[1].identifier == f"{device.id}/{event_session_id1}" event_timestamp_string = event_timestamp1.strftime(DATE_STR_FORMAT) assert browse.children[1].title == f"Person @ {event_timestamp_string}" assert not browse.children[1].can_expand @@ -395,9 +400,12 @@ async def test_resolve_invalid_event_id(hass, auth): async def test_camera_event_clip_preview(hass, auth, hass_client): """Test an event for a battery camera video clip.""" - event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp = dt_util.now() event_data = { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": EVENT_SESSION_ID, + "eventId": "n:2", + }, "sdm.devices.events.CameraClipPreview.ClipPreview": { "eventSessionId": EVENT_SESSION_ID, "previewUrl": "https://127.0.0.1/example", @@ -410,7 +418,6 @@ async def test_camera_event_clip_preview(hass, auth, hass_client): BATTERY_CAMERA_TRAITS, events=[ create_event_message( - event_id, event_data, timestamp=event_timestamp, ), @@ -439,7 +446,7 @@ async def test_camera_event_clip_preview(hass, auth, hass_client): assert browse.children[0].domain == DOMAIN actual_event_id = browse.children[0].identifier event_timestamp_string = event_timestamp.strftime(DATE_STR_FORMAT) - assert browse.children[0].title == f"Event @ {event_timestamp_string}" + assert browse.children[0].title == f"Motion @ {event_timestamp_string}" assert not browse.children[0].can_expand assert len(browse.children[0].children) == 0 @@ -490,7 +497,6 @@ async def test_event_media_render_invalid_event_id(hass, auth, hass_client): async def test_event_media_failure(hass, auth, hass_client): """Test event media fetch sees a failure from the server.""" - event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp = dt_util.now() await async_setup_devices( hass, @@ -499,7 +505,8 @@ async def test_event_media_failure(hass, auth, hass_client): CAMERA_TRAITS, events=[ create_event( - event_id, + EVENT_SESSION_ID, + EVENT_ID, PERSON_EVENT, timestamp=event_timestamp, ), @@ -517,9 +524,9 @@ async def test_event_media_failure(hass, auth, hass_client): # Resolving the event links to the media media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_id}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{EVENT_SESSION_ID}" ) - assert media.url == f"/api/nest/event_media/{device.id}/{event_id}" + assert media.url == f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}" assert media.mime_type == "image/jpeg" auth.responses = [ @@ -535,7 +542,6 @@ async def test_event_media_failure(hass, auth, hass_client): async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin_user): """Test case where user does not have permissions to view media.""" - event_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp = dt_util.now() await async_setup_devices( hass, @@ -544,7 +550,8 @@ async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin CAMERA_TRAITS, events=[ create_event( - event_id, + EVENT_SESSION_ID, + EVENT_ID, PERSON_EVENT, timestamp=event_timestamp, ), @@ -560,7 +567,7 @@ async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin assert device assert device.name == DEVICE_NAME - media_url = f"/api/nest/event_media/{device.id}/{event_id}" + media_url = f"/api/nest/event_media/{device.id}/{EVENT_SESSION_ID}" # Empty policy with no access to the entity hass_admin_user.mock_policy({}) @@ -616,7 +623,12 @@ async def test_multiple_devices(hass, auth, hass_client): # Send events for device #1 for i in range(0, 5): await subscriber.async_receive_event( - create_event(f"event-id-{i}", PERSON_EVENT, device_id=device_id1) + create_event( + f"event-session-id-{i}", + f"event-id-{i}", + PERSON_EVENT, + device_id=device_id1, + ) ) browse = await media_source.async_browse_media( @@ -631,7 +643,9 @@ async def test_multiple_devices(hass, auth, hass_client): # Send events for device #2 for i in range(0, 3): await subscriber.async_receive_event( - create_event(f"other-id-{i}", PERSON_EVENT, device_id=device_id2) + create_event( + f"other-id-{i}", f"event-id{i}", PERSON_EVENT, device_id=device_id2 + ) ) browse = await media_source.async_browse_media(