Improve nest media player clip/image and event handling for multiple events in a short time range (#63149)

This commit is contained in:
Allen Porter
2022-01-11 20:54:49 -08:00
committed by GitHub
parent 42706f780c
commit 789c0a24dd
6 changed files with 594 additions and 259 deletions

View File

@@ -26,7 +26,11 @@ import os
from google_nest_sdm.camera_traits import CameraClipPreviewTrait, CameraEventImageTrait
from google_nest_sdm.device import Device
from google_nest_sdm.event import EventImageType, ImageEventBase
from google_nest_sdm.event_media import EventMediaStore
from google_nest_sdm.event_media import (
ClipPreviewSession,
EventMediaStore,
ImageSession,
)
from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
from homeassistant.components.media_player.const import (
@@ -48,7 +52,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.storage import Store
from homeassistant.helpers.template import DATE_STR_FORMAT
from homeassistant.util import dt as dt_util, raise_if_invalid_filename
from homeassistant.util import dt as dt_util
from .const import DATA_SUBSCRIBER, DOMAIN
from .device_info import NestDeviceInfo
@@ -59,7 +63,7 @@ _LOGGER = logging.getLogger(__name__)
MEDIA_SOURCE_TITLE = "Nest"
DEVICE_TITLE_FORMAT = "{device_name}: Recent Events"
CLIP_TITLE_FORMAT = "{event_name} @ {event_time}"
EVENT_MEDIA_API_URL_FORMAT = "/api/nest/event_media/{device_id}/{event_id}"
EVENT_MEDIA_API_URL_FORMAT = "/api/nest/event_media/{device_id}/{event_token}"
STORAGE_KEY = "nest.event_media"
STORAGE_VERSION = 1
@@ -146,21 +150,30 @@ class NestEventMediaStore(EventMediaStore):
def get_media_key(self, device_id: str, event: ImageEventBase) -> str:
"""Return the filename to use for a new event."""
# Convert a nest device id to a home assistant device id
device_id_str = (
if event.event_image_type != EventImageType.IMAGE:
raise ValueError("No longer used for video clips")
return self.get_image_media_key(device_id, event)
def _map_device_id(self, device_id: str) -> str:
return (
self._devices.get(device_id, f"{device_id}-unknown_device")
if self._devices
else "unknown_device"
)
event_id_str = event.event_session_id
try:
raise_if_invalid_filename(event_id_str)
except ValueError:
event_id_str = ""
def get_image_media_key(self, device_id: str, event: ImageEventBase) -> str:
"""Return the filename for image media for an event."""
device_id_str = self._map_device_id(device_id)
time_str = str(int(event.timestamp.timestamp()))
event_type_str = EVENT_NAME_MAP.get(event.event_type, "event")
suffix = "jpg" if event.event_image_type == EventImageType.IMAGE else "mp4"
return f"{device_id_str}/{time_str}-{event_id_str}-{event_type_str}.{suffix}"
return f"{device_id_str}/{time_str}-{event_type_str}.jpg"
def get_clip_preview_media_key(self, device_id: str, event: ImageEventBase) -> str:
"""Return the filename for clip preview media for an event session."""
device_id_str = self._map_device_id(device_id)
time_str = str(int(event.timestamp.timestamp()))
event_type_str = EVENT_NAME_MAP.get(event.event_type, "event")
return f"{device_id_str}/{time_str}-{event_type_str}.mp4"
def get_media_filename(self, media_key: str) -> str:
"""Return the filename in storage for a media key."""
@@ -265,13 +278,13 @@ class MediaId:
"""
device_id: str
event_id: str | None = None
event_token: str | None = None
@property
def identifier(self) -> str:
"""Media identifier represented as a string."""
if self.event_id:
return f"{self.device_id}/{self.event_id}"
if self.event_token:
return f"{self.device_id}/{self.event_token}"
return self.device_id
@@ -308,24 +321,25 @@ class NestMediaSource(MediaSource):
media_id: MediaId | None = parse_media_id(item.identifier)
if not media_id:
raise Unresolvable("No identifier specified for MediaSourceItem")
if not media_id.event_id:
raise Unresolvable("Identifier missing an event_id: %s" % item.identifier)
if not media_id.event_token:
raise Unresolvable(
"Identifier missing an event_token: %s" % item.identifier
)
devices = await self.devices()
if not (device := devices.get(media_id.device_id)):
raise Unresolvable(
"Unable to find device with identifier: %s" % item.identifier
)
events = await _get_events(device)
if media_id.event_id not in events:
raise Unresolvable(
"Unable to find event with identifier: %s" % item.identifier
)
event = events[media_id.event_id]
# Infer content type from the device, since it only supports one
# snapshot type (either jpg or mp4 clip)
content_type = EventImageType.IMAGE.content_type
if CameraClipPreviewTrait.NAME in device.traits:
content_type = EventImageType.CLIP_PREVIEW.content_type
return PlayMedia(
EVENT_MEDIA_API_URL_FORMAT.format(
device_id=media_id.device_id, event_id=media_id.event_id
device_id=media_id.device_id, event_token=media_id.event_token
),
event.event_image_type.content_type,
content_type,
)
async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource:
@@ -354,35 +368,67 @@ class NestMediaSource(MediaSource):
raise BrowseError(
"Unable to find device with identiifer: %s" % item.identifier
)
if media_id.event_id is None:
# Clip previews are a session with multiple possible event types (e.g.
# person, motion, etc) and a single mp4
if CameraClipPreviewTrait.NAME in device.traits:
clips: dict[
str, ClipPreviewSession
] = await _async_get_clip_preview_sessions(device)
if media_id.event_token is None:
# Browse a specific device and return child events
browse_device = _browse_device(media_id, device)
browse_device.children = []
for clip in clips.values():
event_id = MediaId(media_id.device_id, clip.event_token)
browse_device.children.append(
_browse_clip_preview(event_id, device, clip)
)
return browse_device
# Browse a specific event
if not (single_clip := clips.get(media_id.event_token)):
raise BrowseError(
"Unable to find event with identiifer: %s" % item.identifier
)
return _browse_clip_preview(media_id, device, single_clip)
# Image events are 1:1 of media to event
images: dict[str, ImageSession] = await _async_get_image_sessions(device)
if media_id.event_token is None:
# Browse a specific device and return child events
browse_device = _browse_device(media_id, device)
browse_device.children = []
events = await _get_events(device)
for child_event in events.values():
event_id = MediaId(media_id.device_id, child_event.event_session_id)
for image in images.values():
event_id = MediaId(media_id.device_id, image.event_token)
browse_device.children.append(
_browse_event(event_id, device, child_event)
_browse_image_event(event_id, device, image)
)
return browse_device
# Browse a specific event
events = await _get_events(device)
if not (event := events.get(media_id.event_id)):
if not (single_image := images.get(media_id.event_token)):
raise BrowseError(
"Unable to find event with identiifer: %s" % item.identifier
)
return _browse_event(media_id, device, event)
return _browse_image_event(media_id, device, single_image)
async def devices(self) -> Mapping[str, Device]:
"""Return all event media related devices."""
return await get_media_source_devices(self.hass)
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_session_id: e for e in events}
async def _async_get_clip_preview_sessions(
device: Device,
) -> dict[str, ClipPreviewSession]:
"""Return clip preview sessions for the device."""
events = await device.event_media_manager.async_clip_preview_sessions()
return {e.event_token: e for e in events}
async def _async_get_image_sessions(device: Device) -> dict[str, ImageSession]:
"""Return image events for the device."""
events = await device.event_media_manager.async_image_sessions()
return {e.event_token: e for e in events}
def _browse_root() -> BrowseMediaSource:
@@ -418,10 +464,33 @@ def _browse_device(device_id: MediaId, device: Device) -> BrowseMediaSource:
)
def _browse_event(
event_id: MediaId, device: Device, event: ImageEventBase
def _browse_clip_preview(
event_id: MediaId, device: Device, event: ClipPreviewSession
) -> BrowseMediaSource:
"""Build a BrowseMediaSource for a specific event."""
"""Build a BrowseMediaSource for a specific clip preview event."""
types = []
for event_type in event.event_types:
types.append(MEDIA_SOURCE_EVENT_TITLE_MAP.get(event_type, "Event"))
return BrowseMediaSource(
domain=DOMAIN,
identifier=event_id.identifier,
media_class=MEDIA_CLASS_IMAGE,
media_content_type=MEDIA_TYPE_IMAGE,
title=CLIP_TITLE_FORMAT.format(
event_name=", ".join(types),
event_time=dt_util.as_local(event.timestamp).strftime(DATE_STR_FORMAT),
),
can_play=True,
can_expand=False,
thumbnail=None,
children=[],
)
def _browse_image_event(
event_id: MediaId, device: Device, event: ImageSession
) -> BrowseMediaSource:
"""Build a BrowseMediaSource for a specific image event."""
return BrowseMediaSource(
domain=DOMAIN,
identifier=event_id.identifier,
@@ -431,7 +500,7 @@ def _browse_event(
event_name=MEDIA_SOURCE_EVENT_TITLE_MAP.get(event.event_type, "Event"),
event_time=dt_util.as_local(event.timestamp).strftime(DATE_STR_FORMAT),
),
can_play=(event.event_image_type == EventImageType.CLIP_PREVIEW),
can_play=False,
can_expand=False,
thumbnail=None,
children=[],