google_nest_sdm.camera_traits

Traits belonging to camera devices.

  1"""Traits belonging to camera devices."""
  2
  3from __future__ import annotations
  4
  5from abc import ABC, abstractmethod
  6from dataclasses import dataclass, field
  7import datetime
  8from enum import Enum
  9import logging
 10from typing import ClassVar
 11import urllib.parse as urlparse
 12
 13from mashumaro import DataClassDictMixin, field_options
 14from mashumaro.config import BaseConfig
 15from mashumaro.types import SerializationStrategy
 16
 17from .event import (
 18    CameraClipPreviewEvent,
 19    CameraMotionEvent,
 20    CameraPersonEvent,
 21    CameraSoundEvent,
 22    EventImageContentType,
 23    EventImageType,
 24    EventType,
 25)
 26from .traits import CommandDataClass, TraitType
 27from .webrtc_util import fix_sdp_answer
 28
 29__all__ = [
 30    "CameraImageTrait",
 31    "CameraLiveStreamTrait",
 32    "CameraEventImageTrait",
 33    "CameraMotionTrait",
 34    "CameraPersonTrait",
 35    "CameraSoundTrait",
 36    "CameraClipPreviewTrait",
 37    "Resolution",
 38    "Stream",
 39    "StreamUrls",
 40    "RtspStream",
 41    "WebRtcStream",
 42    "StreamingProtocol",
 43    "EventImage",
 44]
 45
 46_LOGGER = logging.getLogger(__name__)
 47
 48MAX_IMAGE_RESOLUTION = "maxImageResolution"
 49MAX_VIDEO_RESOLUTION = "maxVideoResolution"
 50WIDTH = "width"
 51HEIGHT = "height"
 52VIDEO_CODECS = "videoCodecs"
 53AUDIO_CODECS = "audioCodecs"
 54SUPPORTED_PROTOCOLS = "supportedProtocols"
 55STREAM_URLS = "streamUrls"
 56RESULTS = "results"
 57RTSP_URL = "rtspUrl"
 58STREAM_EXTENSION_TOKEN = "streamExtensionToken"
 59STREAM_TOKEN = "streamToken"
 60URL = "url"
 61TOKEN = "token"
 62ANSWER_SDP = "answerSdp"
 63MEDIA_SESSION_ID = "mediaSessionId"
 64
 65EVENT_IMAGE_CLIP_PREVIEW = "clip_preview"
 66
 67
 68@dataclass
 69class Resolution:
 70    """Maximum Resolution of an image or stream."""
 71
 72    width: int | None = None
 73    height: int | None = None
 74
 75
 76@dataclass
 77class CameraImageTrait(DataClassDictMixin):
 78    """This trait belongs to any device that supports taking images."""
 79
 80    NAME: ClassVar[TraitType] = TraitType.CAMERA_IMAGE
 81
 82    max_image_resolution: Resolution | None = field(
 83        metadata=field_options(alias="maxImageResolution"), default=None
 84    )
 85    """Maximum resolution of the camera image."""
 86
 87
 88@dataclass
 89class Stream(DataClassDictMixin, CommandDataClass, ABC):
 90    """Base class for streams."""
 91
 92    expires_at: datetime.datetime = field(metadata=field_options(alias="expiresAt"))
 93    """Time at which both streamExtensionToken and streamToken expire."""
 94
 95    @abstractmethod
 96    async def extend_stream(self) -> Stream:
 97        """Extend the lifetime of the stream."""
 98
 99    @abstractmethod
100    async def stop_stream(self) -> None:
101        """Invalidate the stream."""
102
103
104@dataclass
105class StreamUrls:
106    """Response object for stream urls"""
107
108    rtsp_url: str = field(metadata=field_options(alias="rtspUrl"))
109    """RTSP live stream URL."""
110
111
112@dataclass
113class RtspStream(Stream):
114    """Provides access an RTSP live stream URL."""
115
116    stream_urls: StreamUrls = field(metadata=field_options(alias="streamUrls"))
117    """Stream urls to access the live stream."""
118
119    stream_token: str = field(metadata=field_options(alias="streamToken"))
120    """Token to use to access an RTSP live stream."""
121
122    stream_extension_token: str = field(
123        metadata=field_options(alias="streamExtensionToken")
124    )
125    """Token to use to extend access to an RTSP live stream."""
126
127    @property
128    def rtsp_stream_url(self) -> str:
129        """RTSP live stream URL."""
130        return self.stream_urls.rtsp_url
131
132    async def extend_stream(self) -> Stream | RtspStream:
133        """Extend the lifetime of the stream."""
134        return await self.extend_rtsp_stream()
135
136    async def extend_rtsp_stream(self) -> RtspStream:
137        """Request a new RTSP live stream URL access token."""
138        data = {
139            "command": "sdm.devices.commands.CameraLiveStream.ExtendRtspStream",
140            "params": {"streamExtensionToken": self.stream_extension_token},
141        }
142        response_data = await self.cmd.execute_json(data)
143        results = response_data[RESULTS]
144        # Update the stream url with the new token
145        stream_token = results[STREAM_TOKEN]
146        parsed = urlparse.urlparse(self.rtsp_stream_url)
147        parsed = parsed._replace(query=f"auth={stream_token}")
148        url = urlparse.urlunparse(parsed)
149        results[STREAM_URLS] = {}
150        results[STREAM_URLS][RTSP_URL] = url
151        obj = RtspStream.from_dict(results)
152        obj._cmd = self.cmd
153        return obj
154
155    async def stop_stream(self) -> None:
156        """Invalidate the stream."""
157        return await self.stop_rtsp_stream()
158
159    async def stop_rtsp_stream(self) -> None:
160        """Invalidates a valid RTSP access token and stops the RTSP live stream."""
161        data = {
162            "command": "sdm.devices.commands.CameraLiveStream.StopRtspStream",
163            "params": {"streamExtensionToken": self.stream_extension_token},
164        }
165        await self.cmd.execute(data)
166
167
168@dataclass
169class WebRtcStream(Stream):
170    """Provides access an RTSP live stream URL."""
171
172    answer_sdp: str = field(metadata=field_options(alias="answerSdp"))
173    """An SDP answer to use with the local device displaying the stream."""
174
175    media_session_id: str = field(metadata=field_options(alias="mediaSessionId"))
176    """Media Session ID of the live stream."""
177
178    async def extend_stream(self) -> WebRtcStream:
179        """Request a new RTSP live stream URL access token."""
180        data = {
181            "command": "sdm.devices.commands.CameraLiveStream.ExtendWebRtcStream",
182            "params": {MEDIA_SESSION_ID: self.media_session_id},
183        }
184        response_data = await self.cmd.execute_json(data)
185        # Preserve original answerSdp, and merge with response that contains
186        # the other fields (expiresAt, and mediaSessionId.
187        results = response_data[RESULTS]
188        results[ANSWER_SDP] = self.answer_sdp
189        obj = WebRtcStream.from_dict(results)
190        obj._cmd = self.cmd
191        return obj
192
193    async def stop_stream(self) -> None:
194        """Invalidates a valid RTSP access token and stops the RTSP live stream."""
195        data = {
196            "command": "sdm.devices.commands.CameraLiveStream.StopWebRtcStream",
197            "params": {MEDIA_SESSION_ID: self.media_session_id},
198        }
199        await self.cmd.execute(data)
200
201
202class StreamingProtocol(str, Enum):
203    """Streaming protocols supported by the device."""
204
205    RTSP = "RTSP"
206    WEB_RTC = "WEB_RTC"
207
208
209def _default_streaming_protocol() -> list[StreamingProtocol]:
210    return [
211        StreamingProtocol.RTSP,
212    ]
213
214
215class StreamingProtocolSerializationStrategy(
216    SerializationStrategy, use_annotations=True
217):
218    """Parser for streaming protocols that ignores invalid values."""
219
220    def serialize(self, value: list[StreamingProtocol]) -> list[str]:
221        return [str(x.name) for x in value]
222
223    def deserialize(self, value: list[str]) -> list[StreamingProtocol]:
224        return [
225            StreamingProtocol[x] for x in value if x in StreamingProtocol.__members__
226        ] or _default_streaming_protocol()
227
228
229@dataclass
230class CameraLiveStreamTrait(DataClassDictMixin, CommandDataClass):
231    """This trait belongs to any device that supports live streaming."""
232
233    NAME: ClassVar[TraitType] = TraitType.CAMERA_LIVE_STREAM
234
235    max_video_resolution: Resolution = field(
236        metadata=field_options(alias="maxVideoResolution"), default_factory=Resolution
237    )
238    """Maximum resolution of the video live stream."""
239
240    video_codecs: list[str] = field(
241        metadata=field_options(alias="videoCodecs"), default_factory=list
242    )
243    """Video codecs supported for the live stream."""
244
245    audio_codecs: list[str] = field(
246        metadata=field_options(alias="audioCodecs"), default_factory=list
247    )
248    """Audio codecs supported for the live stream."""
249
250    supported_protocols: list[StreamingProtocol] = field(
251        metadata=field_options(alias="supportedProtocols"),
252        default_factory=_default_streaming_protocol,
253    )
254    """Streaming protocols supported for the live stream."""
255
256    async def generate_rtsp_stream(self) -> RtspStream:
257        """Request a token to access an RTSP live stream URL."""
258        if StreamingProtocol.RTSP not in self.supported_protocols:
259            raise ValueError("Device does not support RTSP stream")
260        data = {
261            "command": "sdm.devices.commands.CameraLiveStream.GenerateRtspStream",
262            "params": {},
263        }
264        response_data = await self.cmd.execute_json(data)
265        results = response_data[RESULTS]
266        obj = RtspStream.from_dict(results)
267        obj._cmd = self.cmd
268        return obj
269
270    async def generate_web_rtc_stream(self, offer_sdp: str) -> WebRtcStream:
271        """Request a token to access a Web RTC live stream URL."""
272        if StreamingProtocol.WEB_RTC not in self.supported_protocols:
273            raise ValueError("Device does not support WEB_RTC stream")
274        data = {
275            "command": "sdm.devices.commands.CameraLiveStream.GenerateWebRtcStream",
276            "params": {"offerSdp": offer_sdp},
277        }
278        response_data = await self.cmd.execute_json(data)
279        results = response_data[RESULTS]
280        obj = WebRtcStream.from_dict(results)
281        obj._cmd = self.cmd
282        _LOGGER.debug("Received answer_sdp: %s", obj.answer_sdp)
283        obj.answer_sdp = fix_sdp_answer(offer_sdp, obj.answer_sdp)
284        _LOGGER.debug("Return answer_sdp: %s", obj.answer_sdp)
285        return obj
286
287    class Config(BaseConfig):
288        serialization_strategy = {
289            list[StreamingProtocol]: StreamingProtocolSerializationStrategy(),
290        }
291        serialize_by_alias = True
292
293
294@dataclass
295class EventImage(DataClassDictMixin, CommandDataClass):
296    """Provides access to an image in response to an event.
297
298    Use a ?width or ?height query parameters to customize the resolution
299    of the downloaded image. Only one of these parameters need to specified.
300    The other parameter is scaled automatically according to the camera's
301    aspect ratio.
302
303    The token should be added as an HTTP header:
304    Authorization: Basic <token>
305    """
306
307    event_image_type: EventImageContentType
308    """Return the type of event image."""
309
310    url: str | None = field(default=None)
311    """URL to download the camera image from."""
312
313    token: str | None = field(default=None)
314    """Token to use in the HTTP Authorization header when downloading."""
315
316    async def contents(
317        self,
318        width: int | None = None,
319        height: int | None = None,
320    ) -> bytes:
321        """Download the image bytes."""
322        if width:
323            fetch_url = f"{self.url}?width={width}"
324        elif height:
325            fetch_url = f"{self.url}?width={height}"
326        else:
327            assert self.url
328            fetch_url = self.url
329        return await self.cmd.fetch_image(fetch_url, basic_auth=self.token)
330
331
332@dataclass
333class CameraEventImageTrait(DataClassDictMixin, CommandDataClass):
334    """This trait belongs to any device that generates images from events."""
335
336    NAME: ClassVar[TraitType] = TraitType.CAMERA_EVENT_IMAGE
337
338    async def generate_image(self, event_id: str) -> EventImage:
339        """Provide a URL to download a camera image."""
340        data = {
341            "command": "sdm.devices.commands.CameraEventImage.GenerateImage",
342            "params": {
343                "eventId": event_id,
344            },
345        }
346        response_data = await self.cmd.execute_json(data)
347        results = response_data[RESULTS]
348        img = EventImage(**results, event_image_type=EventImageType.IMAGE)
349        img._cmd = self.cmd
350        return img
351
352
353@dataclass
354class CameraMotionTrait:
355    """For any device that supports motion detection events."""
356
357    NAME: ClassVar[TraitType] = TraitType.CAMERA_MOTION
358    EVENT_NAME: ClassVar[EventType] = CameraMotionEvent.NAME
359
360
361@dataclass
362class CameraPersonTrait:
363    """For any device that supports person detection events."""
364
365    NAME: ClassVar[TraitType] = TraitType.CAMERA_PERSON
366    EVENT_NAME: ClassVar[EventType] = CameraPersonEvent.NAME
367
368
369@dataclass
370class CameraSoundTrait:
371    """For any device that supports sound detection events."""
372
373    NAME: ClassVar[TraitType] = TraitType.CAMERA_SOUND
374    EVENT_NAME: ClassVar[EventType] = CameraSoundEvent.NAME
375
376
377@dataclass
378class CameraClipPreviewTrait(DataClassDictMixin, CommandDataClass):
379    """For any device that supports a clip preview."""
380
381    NAME: ClassVar[TraitType] = TraitType.CAMERA_CLIP_PREVIEW
382    EVENT_NAME: ClassVar[EventType] = CameraClipPreviewEvent.NAME
383
384    async def generate_event_image(self, preview_url: str) -> EventImage | None:
385        """Provide a URL to download a camera image from the active event."""
386        img = EventImage(url=preview_url, event_image_type=EventImageType.CLIP_PREVIEW)
387        img._cmd = self.cmd
388        return img
@dataclass
class CameraImageTrait(mashumaro.mixins.dict.DataClassDictMixin):
77@dataclass
78class CameraImageTrait(DataClassDictMixin):
79    """This trait belongs to any device that supports taking images."""
80
81    NAME: ClassVar[TraitType] = TraitType.CAMERA_IMAGE
82
83    max_image_resolution: Resolution | None = field(
84        metadata=field_options(alias="maxImageResolution"), default=None
85    )
86    """Maximum resolution of the camera image."""

This trait belongs to any device that supports taking images.

CameraImageTrait( max_image_resolution: Resolution | None = None)
NAME: ClassVar[google_nest_sdm.traits.TraitType] = <TraitType.CAMERA_IMAGE: 'sdm.devices.traits.CameraImage'>
max_image_resolution: Resolution | None = None

Maximum resolution of the camera image.

def to_dict(self):

The type of the None singleton.

def from_dict(cls, d, *, dialect=None):

The type of the None singleton.

@dataclass
class CameraLiveStreamTrait(mashumaro.mixins.dict.DataClassDictMixin, google_nest_sdm.traits.CommandDataClass):
230@dataclass
231class CameraLiveStreamTrait(DataClassDictMixin, CommandDataClass):
232    """This trait belongs to any device that supports live streaming."""
233
234    NAME: ClassVar[TraitType] = TraitType.CAMERA_LIVE_STREAM
235
236    max_video_resolution: Resolution = field(
237        metadata=field_options(alias="maxVideoResolution"), default_factory=Resolution
238    )
239    """Maximum resolution of the video live stream."""
240
241    video_codecs: list[str] = field(
242        metadata=field_options(alias="videoCodecs"), default_factory=list
243    )
244    """Video codecs supported for the live stream."""
245
246    audio_codecs: list[str] = field(
247        metadata=field_options(alias="audioCodecs"), default_factory=list
248    )
249    """Audio codecs supported for the live stream."""
250
251    supported_protocols: list[StreamingProtocol] = field(
252        metadata=field_options(alias="supportedProtocols"),
253        default_factory=_default_streaming_protocol,
254    )
255    """Streaming protocols supported for the live stream."""
256
257    async def generate_rtsp_stream(self) -> RtspStream:
258        """Request a token to access an RTSP live stream URL."""
259        if StreamingProtocol.RTSP not in self.supported_protocols:
260            raise ValueError("Device does not support RTSP stream")
261        data = {
262            "command": "sdm.devices.commands.CameraLiveStream.GenerateRtspStream",
263            "params": {},
264        }
265        response_data = await self.cmd.execute_json(data)
266        results = response_data[RESULTS]
267        obj = RtspStream.from_dict(results)
268        obj._cmd = self.cmd
269        return obj
270
271    async def generate_web_rtc_stream(self, offer_sdp: str) -> WebRtcStream:
272        """Request a token to access a Web RTC live stream URL."""
273        if StreamingProtocol.WEB_RTC not in self.supported_protocols:
274            raise ValueError("Device does not support WEB_RTC stream")
275        data = {
276            "command": "sdm.devices.commands.CameraLiveStream.GenerateWebRtcStream",
277            "params": {"offerSdp": offer_sdp},
278        }
279        response_data = await self.cmd.execute_json(data)
280        results = response_data[RESULTS]
281        obj = WebRtcStream.from_dict(results)
282        obj._cmd = self.cmd
283        _LOGGER.debug("Received answer_sdp: %s", obj.answer_sdp)
284        obj.answer_sdp = fix_sdp_answer(offer_sdp, obj.answer_sdp)
285        _LOGGER.debug("Return answer_sdp: %s", obj.answer_sdp)
286        return obj
287
288    class Config(BaseConfig):
289        serialization_strategy = {
290            list[StreamingProtocol]: StreamingProtocolSerializationStrategy(),
291        }
292        serialize_by_alias = True

This trait belongs to any device that supports live streaming.

CameraLiveStreamTrait( max_video_resolution: Resolution = <factory>, video_codecs: list[str] = <factory>, audio_codecs: list[str] = <factory>, supported_protocols: list[StreamingProtocol] = <factory>)
NAME: ClassVar[google_nest_sdm.traits.TraitType] = <TraitType.CAMERA_LIVE_STREAM: 'sdm.devices.traits.CameraLiveStream'>
max_video_resolution: Resolution

Maximum resolution of the video live stream.

video_codecs: list[str]

Video codecs supported for the live stream.

audio_codecs: list[str]

Audio codecs supported for the live stream.

supported_protocols: list[StreamingProtocol]

Streaming protocols supported for the live stream.

async def generate_rtsp_stream(self) -> RtspStream:
257    async def generate_rtsp_stream(self) -> RtspStream:
258        """Request a token to access an RTSP live stream URL."""
259        if StreamingProtocol.RTSP not in self.supported_protocols:
260            raise ValueError("Device does not support RTSP stream")
261        data = {
262            "command": "sdm.devices.commands.CameraLiveStream.GenerateRtspStream",
263            "params": {},
264        }
265        response_data = await self.cmd.execute_json(data)
266        results = response_data[RESULTS]
267        obj = RtspStream.from_dict(results)
268        obj._cmd = self.cmd
269        return obj

Request a token to access an RTSP live stream URL.

async def generate_web_rtc_stream(self, offer_sdp: str) -> WebRtcStream:
271    async def generate_web_rtc_stream(self, offer_sdp: str) -> WebRtcStream:
272        """Request a token to access a Web RTC live stream URL."""
273        if StreamingProtocol.WEB_RTC not in self.supported_protocols:
274            raise ValueError("Device does not support WEB_RTC stream")
275        data = {
276            "command": "sdm.devices.commands.CameraLiveStream.GenerateWebRtcStream",
277            "params": {"offerSdp": offer_sdp},
278        }
279        response_data = await self.cmd.execute_json(data)
280        results = response_data[RESULTS]
281        obj = WebRtcStream.from_dict(results)
282        obj._cmd = self.cmd
283        _LOGGER.debug("Received answer_sdp: %s", obj.answer_sdp)
284        obj.answer_sdp = fix_sdp_answer(offer_sdp, obj.answer_sdp)
285        _LOGGER.debug("Return answer_sdp: %s", obj.answer_sdp)
286        return obj

Request a token to access a Web RTC live stream URL.

def to_dict(self):

The type of the None singleton.

def from_dict(cls, d, *, dialect=None):

The type of the None singleton.

class CameraLiveStreamTrait.Config(mashumaro.config.BaseConfig):
288    class Config(BaseConfig):
289        serialization_strategy = {
290            list[StreamingProtocol]: StreamingProtocolSerializationStrategy(),
291        }
292        serialize_by_alias = True
serialization_strategy = {list[StreamingProtocol]: <google_nest_sdm.camera_traits.StreamingProtocolSerializationStrategy object>}
serialize_by_alias = True
@dataclass
class CameraEventImageTrait(mashumaro.mixins.dict.DataClassDictMixin, google_nest_sdm.traits.CommandDataClass):
333@dataclass
334class CameraEventImageTrait(DataClassDictMixin, CommandDataClass):
335    """This trait belongs to any device that generates images from events."""
336
337    NAME: ClassVar[TraitType] = TraitType.CAMERA_EVENT_IMAGE
338
339    async def generate_image(self, event_id: str) -> EventImage:
340        """Provide a URL to download a camera image."""
341        data = {
342            "command": "sdm.devices.commands.CameraEventImage.GenerateImage",
343            "params": {
344                "eventId": event_id,
345            },
346        }
347        response_data = await self.cmd.execute_json(data)
348        results = response_data[RESULTS]
349        img = EventImage(**results, event_image_type=EventImageType.IMAGE)
350        img._cmd = self.cmd
351        return img

This trait belongs to any device that generates images from events.

NAME: ClassVar[google_nest_sdm.traits.TraitType] = <TraitType.CAMERA_EVENT_IMAGE: 'sdm.devices.traits.CameraEventImage'>
async def generate_image(self, event_id: str) -> EventImage:
339    async def generate_image(self, event_id: str) -> EventImage:
340        """Provide a URL to download a camera image."""
341        data = {
342            "command": "sdm.devices.commands.CameraEventImage.GenerateImage",
343            "params": {
344                "eventId": event_id,
345            },
346        }
347        response_data = await self.cmd.execute_json(data)
348        results = response_data[RESULTS]
349        img = EventImage(**results, event_image_type=EventImageType.IMAGE)
350        img._cmd = self.cmd
351        return img

Provide a URL to download a camera image.

def to_dict(self):

The type of the None singleton.

def from_dict(cls, d, *, dialect=None):

The type of the None singleton.

@dataclass
class CameraMotionTrait:
354@dataclass
355class CameraMotionTrait:
356    """For any device that supports motion detection events."""
357
358    NAME: ClassVar[TraitType] = TraitType.CAMERA_MOTION
359    EVENT_NAME: ClassVar[EventType] = CameraMotionEvent.NAME

For any device that supports motion detection events.

NAME: ClassVar[google_nest_sdm.traits.TraitType] = <TraitType.CAMERA_MOTION: 'sdm.devices.traits.CameraMotion'>
EVENT_NAME: ClassVar[google_nest_sdm.event.EventType] = <EventType.CAMERA_MOTION: 'sdm.devices.events.CameraMotion.Motion'>
@dataclass
class CameraPersonTrait:
362@dataclass
363class CameraPersonTrait:
364    """For any device that supports person detection events."""
365
366    NAME: ClassVar[TraitType] = TraitType.CAMERA_PERSON
367    EVENT_NAME: ClassVar[EventType] = CameraPersonEvent.NAME

For any device that supports person detection events.

NAME: ClassVar[google_nest_sdm.traits.TraitType] = <TraitType.CAMERA_PERSON: 'sdm.devices.traits.CameraPerson'>
EVENT_NAME: ClassVar[google_nest_sdm.event.EventType] = <EventType.CAMERA_PERSON: 'sdm.devices.events.CameraPerson.Person'>
@dataclass
class CameraSoundTrait:
370@dataclass
371class CameraSoundTrait:
372    """For any device that supports sound detection events."""
373
374    NAME: ClassVar[TraitType] = TraitType.CAMERA_SOUND
375    EVENT_NAME: ClassVar[EventType] = CameraSoundEvent.NAME

For any device that supports sound detection events.

NAME: ClassVar[google_nest_sdm.traits.TraitType] = <TraitType.CAMERA_SOUND: 'sdm.devices.traits.CameraSound'>
EVENT_NAME: ClassVar[google_nest_sdm.event.EventType] = <EventType.CAMERA_SOUND: 'sdm.devices.events.CameraSound.Sound'>
@dataclass
class CameraClipPreviewTrait(mashumaro.mixins.dict.DataClassDictMixin, google_nest_sdm.traits.CommandDataClass):
378@dataclass
379class CameraClipPreviewTrait(DataClassDictMixin, CommandDataClass):
380    """For any device that supports a clip preview."""
381
382    NAME: ClassVar[TraitType] = TraitType.CAMERA_CLIP_PREVIEW
383    EVENT_NAME: ClassVar[EventType] = CameraClipPreviewEvent.NAME
384
385    async def generate_event_image(self, preview_url: str) -> EventImage | None:
386        """Provide a URL to download a camera image from the active event."""
387        img = EventImage(url=preview_url, event_image_type=EventImageType.CLIP_PREVIEW)
388        img._cmd = self.cmd
389        return img

For any device that supports a clip preview.

NAME: ClassVar[google_nest_sdm.traits.TraitType] = <TraitType.CAMERA_CLIP_PREVIEW: 'sdm.devices.traits.CameraClipPreview'>
EVENT_NAME: ClassVar[google_nest_sdm.event.EventType] = <EventType.CAMERA_CLIP_PREVIEW: 'sdm.devices.events.CameraClipPreview.ClipPreview'>
async def generate_event_image( self, preview_url: str) -> EventImage | None:
385    async def generate_event_image(self, preview_url: str) -> EventImage | None:
386        """Provide a URL to download a camera image from the active event."""
387        img = EventImage(url=preview_url, event_image_type=EventImageType.CLIP_PREVIEW)
388        img._cmd = self.cmd
389        return img

Provide a URL to download a camera image from the active event.

def to_dict(self):

The type of the None singleton.

def from_dict(cls, d, *, dialect=None):

The type of the None singleton.

@dataclass
class Resolution:
69@dataclass
70class Resolution:
71    """Maximum Resolution of an image or stream."""
72
73    width: int | None = None
74    height: int | None = None

Maximum Resolution of an image or stream.

Resolution(width: int | None = None, height: int | None = None)
width: int | None = None
height: int | None = None
@dataclass
class Stream(mashumaro.mixins.dict.DataClassDictMixin, google_nest_sdm.traits.CommandDataClass, abc.ABC):
 89@dataclass
 90class Stream(DataClassDictMixin, CommandDataClass, ABC):
 91    """Base class for streams."""
 92
 93    expires_at: datetime.datetime = field(metadata=field_options(alias="expiresAt"))
 94    """Time at which both streamExtensionToken and streamToken expire."""
 95
 96    @abstractmethod
 97    async def extend_stream(self) -> Stream:
 98        """Extend the lifetime of the stream."""
 99
100    @abstractmethod
101    async def stop_stream(self) -> None:
102        """Invalidate the stream."""

Base class for streams.

expires_at: datetime.datetime

Time at which both streamExtensionToken and streamToken expire.

@abstractmethod
async def extend_stream(self) -> Stream:
96    @abstractmethod
97    async def extend_stream(self) -> Stream:
98        """Extend the lifetime of the stream."""

Extend the lifetime of the stream.

@abstractmethod
async def stop_stream(self) -> None:
100    @abstractmethod
101    async def stop_stream(self) -> None:
102        """Invalidate the stream."""

Invalidate the stream.

def to_dict(self):

The type of the None singleton.

def from_dict(cls, d, *, dialect=None):

The type of the None singleton.

@dataclass
class StreamUrls:
105@dataclass
106class StreamUrls:
107    """Response object for stream urls"""
108
109    rtsp_url: str = field(metadata=field_options(alias="rtspUrl"))
110    """RTSP live stream URL."""

Response object for stream urls

StreamUrls(rtsp_url: str)
rtsp_url: str

RTSP live stream URL.

@dataclass
class RtspStream(Stream):
113@dataclass
114class RtspStream(Stream):
115    """Provides access an RTSP live stream URL."""
116
117    stream_urls: StreamUrls = field(metadata=field_options(alias="streamUrls"))
118    """Stream urls to access the live stream."""
119
120    stream_token: str = field(metadata=field_options(alias="streamToken"))
121    """Token to use to access an RTSP live stream."""
122
123    stream_extension_token: str = field(
124        metadata=field_options(alias="streamExtensionToken")
125    )
126    """Token to use to extend access to an RTSP live stream."""
127
128    @property
129    def rtsp_stream_url(self) -> str:
130        """RTSP live stream URL."""
131        return self.stream_urls.rtsp_url
132
133    async def extend_stream(self) -> Stream | RtspStream:
134        """Extend the lifetime of the stream."""
135        return await self.extend_rtsp_stream()
136
137    async def extend_rtsp_stream(self) -> RtspStream:
138        """Request a new RTSP live stream URL access token."""
139        data = {
140            "command": "sdm.devices.commands.CameraLiveStream.ExtendRtspStream",
141            "params": {"streamExtensionToken": self.stream_extension_token},
142        }
143        response_data = await self.cmd.execute_json(data)
144        results = response_data[RESULTS]
145        # Update the stream url with the new token
146        stream_token = results[STREAM_TOKEN]
147        parsed = urlparse.urlparse(self.rtsp_stream_url)
148        parsed = parsed._replace(query=f"auth={stream_token}")
149        url = urlparse.urlunparse(parsed)
150        results[STREAM_URLS] = {}
151        results[STREAM_URLS][RTSP_URL] = url
152        obj = RtspStream.from_dict(results)
153        obj._cmd = self.cmd
154        return obj
155
156    async def stop_stream(self) -> None:
157        """Invalidate the stream."""
158        return await self.stop_rtsp_stream()
159
160    async def stop_rtsp_stream(self) -> None:
161        """Invalidates a valid RTSP access token and stops the RTSP live stream."""
162        data = {
163            "command": "sdm.devices.commands.CameraLiveStream.StopRtspStream",
164            "params": {"streamExtensionToken": self.stream_extension_token},
165        }
166        await self.cmd.execute(data)

Provides access an RTSP live stream URL.

RtspStream( expires_at: datetime.datetime, stream_urls: StreamUrls, stream_token: str, stream_extension_token: str)
stream_urls: StreamUrls

Stream urls to access the live stream.

stream_token: str

Token to use to access an RTSP live stream.

stream_extension_token: str

Token to use to extend access to an RTSP live stream.

rtsp_stream_url: str
128    @property
129    def rtsp_stream_url(self) -> str:
130        """RTSP live stream URL."""
131        return self.stream_urls.rtsp_url

RTSP live stream URL.

async def extend_stream( self) -> Stream | RtspStream:
133    async def extend_stream(self) -> Stream | RtspStream:
134        """Extend the lifetime of the stream."""
135        return await self.extend_rtsp_stream()

Extend the lifetime of the stream.

async def extend_rtsp_stream(self) -> RtspStream:
137    async def extend_rtsp_stream(self) -> RtspStream:
138        """Request a new RTSP live stream URL access token."""
139        data = {
140            "command": "sdm.devices.commands.CameraLiveStream.ExtendRtspStream",
141            "params": {"streamExtensionToken": self.stream_extension_token},
142        }
143        response_data = await self.cmd.execute_json(data)
144        results = response_data[RESULTS]
145        # Update the stream url with the new token
146        stream_token = results[STREAM_TOKEN]
147        parsed = urlparse.urlparse(self.rtsp_stream_url)
148        parsed = parsed._replace(query=f"auth={stream_token}")
149        url = urlparse.urlunparse(parsed)
150        results[STREAM_URLS] = {}
151        results[STREAM_URLS][RTSP_URL] = url
152        obj = RtspStream.from_dict(results)
153        obj._cmd = self.cmd
154        return obj

Request a new RTSP live stream URL access token.

async def stop_stream(self) -> None:
156    async def stop_stream(self) -> None:
157        """Invalidate the stream."""
158        return await self.stop_rtsp_stream()

Invalidate the stream.

async def stop_rtsp_stream(self) -> None:
160    async def stop_rtsp_stream(self) -> None:
161        """Invalidates a valid RTSP access token and stops the RTSP live stream."""
162        data = {
163            "command": "sdm.devices.commands.CameraLiveStream.StopRtspStream",
164            "params": {"streamExtensionToken": self.stream_extension_token},
165        }
166        await self.cmd.execute(data)

Invalidates a valid RTSP access token and stops the RTSP live stream.

def to_dict(self):

The type of the None singleton.

def from_dict(cls, d, *, dialect=None):

The type of the None singleton.

Inherited Members
Stream
expires_at
@dataclass
class WebRtcStream(Stream):
169@dataclass
170class WebRtcStream(Stream):
171    """Provides access an RTSP live stream URL."""
172
173    answer_sdp: str = field(metadata=field_options(alias="answerSdp"))
174    """An SDP answer to use with the local device displaying the stream."""
175
176    media_session_id: str = field(metadata=field_options(alias="mediaSessionId"))
177    """Media Session ID of the live stream."""
178
179    async def extend_stream(self) -> WebRtcStream:
180        """Request a new RTSP live stream URL access token."""
181        data = {
182            "command": "sdm.devices.commands.CameraLiveStream.ExtendWebRtcStream",
183            "params": {MEDIA_SESSION_ID: self.media_session_id},
184        }
185        response_data = await self.cmd.execute_json(data)
186        # Preserve original answerSdp, and merge with response that contains
187        # the other fields (expiresAt, and mediaSessionId.
188        results = response_data[RESULTS]
189        results[ANSWER_SDP] = self.answer_sdp
190        obj = WebRtcStream.from_dict(results)
191        obj._cmd = self.cmd
192        return obj
193
194    async def stop_stream(self) -> None:
195        """Invalidates a valid RTSP access token and stops the RTSP live stream."""
196        data = {
197            "command": "sdm.devices.commands.CameraLiveStream.StopWebRtcStream",
198            "params": {MEDIA_SESSION_ID: self.media_session_id},
199        }
200        await self.cmd.execute(data)

Provides access an RTSP live stream URL.

WebRtcStream( expires_at: datetime.datetime, answer_sdp: str, media_session_id: str)
answer_sdp: str

An SDP answer to use with the local device displaying the stream.

media_session_id: str

Media Session ID of the live stream.

async def extend_stream(self) -> WebRtcStream:
179    async def extend_stream(self) -> WebRtcStream:
180        """Request a new RTSP live stream URL access token."""
181        data = {
182            "command": "sdm.devices.commands.CameraLiveStream.ExtendWebRtcStream",
183            "params": {MEDIA_SESSION_ID: self.media_session_id},
184        }
185        response_data = await self.cmd.execute_json(data)
186        # Preserve original answerSdp, and merge with response that contains
187        # the other fields (expiresAt, and mediaSessionId.
188        results = response_data[RESULTS]
189        results[ANSWER_SDP] = self.answer_sdp
190        obj = WebRtcStream.from_dict(results)
191        obj._cmd = self.cmd
192        return obj

Request a new RTSP live stream URL access token.

async def stop_stream(self) -> None:
194    async def stop_stream(self) -> None:
195        """Invalidates a valid RTSP access token and stops the RTSP live stream."""
196        data = {
197            "command": "sdm.devices.commands.CameraLiveStream.StopWebRtcStream",
198            "params": {MEDIA_SESSION_ID: self.media_session_id},
199        }
200        await self.cmd.execute(data)

Invalidates a valid RTSP access token and stops the RTSP live stream.

def to_dict(self):

The type of the None singleton.

def from_dict(cls, d, *, dialect=None):

The type of the None singleton.

Inherited Members
Stream
expires_at
class StreamingProtocol(builtins.str, enum.Enum):
203class StreamingProtocol(str, Enum):
204    """Streaming protocols supported by the device."""
205
206    RTSP = "RTSP"
207    WEB_RTC = "WEB_RTC"

Streaming protocols supported by the device.

RTSP = <StreamingProtocol.RTSP: 'RTSP'>
WEB_RTC = <StreamingProtocol.WEB_RTC: 'WEB_RTC'>
@dataclass
class EventImage(mashumaro.mixins.dict.DataClassDictMixin, google_nest_sdm.traits.CommandDataClass):
295@dataclass
296class EventImage(DataClassDictMixin, CommandDataClass):
297    """Provides access to an image in response to an event.
298
299    Use a ?width or ?height query parameters to customize the resolution
300    of the downloaded image. Only one of these parameters need to specified.
301    The other parameter is scaled automatically according to the camera's
302    aspect ratio.
303
304    The token should be added as an HTTP header:
305    Authorization: Basic <token>
306    """
307
308    event_image_type: EventImageContentType
309    """Return the type of event image."""
310
311    url: str | None = field(default=None)
312    """URL to download the camera image from."""
313
314    token: str | None = field(default=None)
315    """Token to use in the HTTP Authorization header when downloading."""
316
317    async def contents(
318        self,
319        width: int | None = None,
320        height: int | None = None,
321    ) -> bytes:
322        """Download the image bytes."""
323        if width:
324            fetch_url = f"{self.url}?width={width}"
325        elif height:
326            fetch_url = f"{self.url}?width={height}"
327        else:
328            assert self.url
329            fetch_url = self.url
330        return await self.cmd.fetch_image(fetch_url, basic_auth=self.token)

Provides access to an image in response to an event.

Use a ?width or ?height query parameters to customize the resolution of the downloaded image. Only one of these parameters need to specified. The other parameter is scaled automatically according to the camera's aspect ratio.

The token should be added as an HTTP header: Authorization: Basic

EventImage( event_image_type: google_nest_sdm.event.EventImageContentType, url: str | None = None, token: str | None = None)
event_image_type: google_nest_sdm.event.EventImageContentType

Return the type of event image.

url: str | None = None

URL to download the camera image from.

token: str | None = None

Token to use in the HTTP Authorization header when downloading.

async def contents(self, width: int | None = None, height: int | None = None) -> bytes:
317    async def contents(
318        self,
319        width: int | None = None,
320        height: int | None = None,
321    ) -> bytes:
322        """Download the image bytes."""
323        if width:
324            fetch_url = f"{self.url}?width={width}"
325        elif height:
326            fetch_url = f"{self.url}?width={height}"
327        else:
328            assert self.url
329            fetch_url = self.url
330        return await self.cmd.fetch_image(fetch_url, basic_auth=self.token)

Download the image bytes.

def to_dict(self):

The type of the None singleton.

def from_dict(cls, d, *, dialect=None):

The type of the None singleton.