mirror of
https://github.com/home-assistant/core.git
synced 2025-11-08 10:29:27 +00:00
Improve nest media player clip/image and event handling for multiple events in a short time range (#63149)
This commit is contained in:
@@ -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=[],
|
||||
|
||||
Reference in New Issue
Block a user