1 """Support for Google Nest SDM Cameras."""
3 from __future__
import annotations
7 from collections.abc
import Awaitable, Callable
11 from pathlib
import Path
13 from google_nest_sdm.camera_traits
import (
14 CameraLiveStreamTrait,
19 from google_nest_sdm.device
import Device
20 from google_nest_sdm.device_manager
import DeviceManager
21 from google_nest_sdm.exceptions
import ApiException
22 from webrtc_models
import RTCIceCandidateInit
28 WebRTCClientConfiguration,
39 from .const
import DATA_DEVICE_MANAGER, DOMAIN
40 from .device_info
import NestDeviceInfo
42 _LOGGER = logging.getLogger(__name__)
44 PLACEHOLDER = Path(__file__).parent /
"placeholder.png"
47 STREAM_EXPIRATION_BUFFER = datetime.timedelta(seconds=30)
50 MIN_REFRESH_BACKOFF_INTERVAL = datetime.timedelta(minutes=1)
51 MAX_REFRESH_BACKOFF_INTERVAL = datetime.timedelta(minutes=10)
52 BACKOFF_MULTIPLIER = 1.5
56 hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
58 """Set up the cameras."""
60 device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][
63 entities: list[NestCameraBaseEntity] = []
64 for device
in device_manager.devices.values():
65 if (live_stream := device.traits.get(CameraLiveStreamTrait.NAME))
is None:
67 if StreamingProtocol.WEB_RTC
in live_stream.supported_protocols:
69 elif StreamingProtocol.RTSP
in live_stream.supported_protocols:
76 """Class that will refresh an expiring stream.
78 This class will schedule an alarm for the next expiration time of a stream.
79 When the alarm fires, it runs the provided `refresh_cb` to extend the
80 lifetime of the stream and return a new expiration time.
82 A simple backoff will be applied when the refresh callback fails.
88 expires_at: datetime.datetime,
89 refresh_cb: Callable[[], Awaitable[datetime.datetime |
None]],
91 """Initialize StreamRefresh."""
93 self.
_unsub_unsub: Callable[[],
None] |
None =
None
99 """Invalidates the stream."""
104 """Alarm that fires to check if the stream should be refreshed."""
108 except ApiException
as err:
109 _LOGGER.debug(
"Failed to refresh stream: %s", err)
113 MAX_REFRESH_BACKOFF_INTERVAL,
117 if expires_at
is None:
122 expires_at - STREAM_EXPIRATION_BUFFER,
128 """Schedules an alarm to refresh any streams before expiration."""
129 _LOGGER.debug(
"Scheduling stream refresh for %s", refresh_time)
138 """Devices that support cameras."""
140 _attr_has_entity_name =
True
142 _attr_is_streaming =
True
143 _attr_supported_features = CameraEntityFeature.STREAM
146 """Initialize the camera."""
153 self.stream_options[CONF_EXTRA_PART_WAIT_TIME] = 3
158 """Run when entity is added to register update signal handler."""
165 """Nest cameras that use RTSP."""
167 _rtsp_stream: RtspStream |
None =
None
168 _rtsp_live_stream_trait: CameraLiveStreamTrait
171 """Initialize the camera."""
175 self.
_refresh_unsub_refresh_unsub: Callable[[],
None] |
None =
None
179 """Always use the RTSP stream to generate snapshots."""
184 """Return True if entity is available."""
193 """Return the source of the stream."""
196 _LOGGER.debug(
"Fetching stream url")
201 except ApiException
as err:
211 _LOGGER.warning(
"Stream already expired")
215 """Refresh stream to extend expiration time."""
218 _LOGGER.debug(
"Extending RTSP stream")
221 except ApiException
as err:
222 _LOGGER.debug(
"Failed to extend stream: %s", err)
235 """Invalidates the RTSP token when unloaded."""
242 except ApiException
as err:
243 _LOGGER.debug(
"Error stopping stream: %s", err)
248 """Nest cameras that use WebRTC."""
251 """Initialize the camera."""
253 self._webrtc_sessions: dict[str, WebRtcStream] = {}
254 self._refresh_unsub: dict[str, Callable[[],
None]] = {}
257 """Refresh stream to extend expiration time."""
258 if not (webrtc_stream := self._webrtc_sessions.
get(session_id)):
260 _LOGGER.debug(
"Extending WebRTC stream %s", webrtc_stream.media_session_id)
261 webrtc_stream = await webrtc_stream.extend_stream()
262 if session_id
in self._webrtc_sessions:
263 self._webrtc_sessions[session_id] = webrtc_stream
264 return webrtc_stream.expires_at
268 self, width: int |
None =
None, height: int |
None =
None
270 """Return a placeholder image for WebRTC cameras that don't support snapshots."""
276 """Return placeholder image to use when no stream is available."""
277 return PLACEHOLDER.read_bytes()
280 self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
282 """Return the source of the stream."""
283 trait: CameraLiveStreamTrait = self.
_device_device.traits[CameraLiveStreamTrait.NAME]
285 stream = await trait.generate_web_rtc_stream(offer_sdp)
286 except ApiException
as err:
289 "Started WebRTC session %s, %s", session_id, stream.media_session_id
291 self._webrtc_sessions[session_id] = stream
298 self._refresh_unsub[session_id] = refresh.unsub
301 self, session_id: str, candidate: RTCIceCandidateInit
303 """Ignore WebRTC candidates for Nest cloud based cameras."""
308 """Close a WebRTC session."""
309 if (stream := self._webrtc_sessions.pop(session_id,
None))
is not None:
311 "Closing WebRTC session %s, %s", session_id, stream.media_session_id
313 unsub = self._refresh_unsub.pop(session_id)
316 async
def stop_stream() -> None:
318 await stream.stop_stream()
319 except ApiException
as err:
320 _LOGGER.debug(
"Error stopping stream: %s", err)
322 self.
hasshass.async_create_task(stop_stream())
327 """Return the WebRTC client configuration adjustable per integration."""
328 return WebRTCClientConfiguration(data_channel=
"dataSendChannel")
331 """Invalidates the RTSP token when unloaded."""
333 for session_id
in list(self._webrtc_sessions.keys()):
None async_write_ha_state(self)
None close_webrtc_session(self, str session_id)
None __init__(self, Device device)
None async_added_to_hass(self)
None async_will_remove_from_hass(self)
str|None stream_source(self)
bool use_stream_for_stills(self)
datetime.datetime|None _async_refresh_stream(self)
None __init__(self, Device device)
None __init__(self, Device device)
bytes|None async_camera_image(self, int|None width=None, int|None height=None)
None async_will_remove_from_hass(self)
bytes placeholder_image(cls)
WebRTCClientConfiguration _async_get_webrtc_client_configuration(self)
None async_handle_async_webrtc_offer(self, str offer_sdp, str session_id, WebRTCSendMessage send_message)
None close_webrtc_session(self, str session_id)
datetime.datetime|None _async_refresh_stream(self, str session_id)
None async_on_webrtc_candidate(self, str session_id, RTCIceCandidateInit candidate)
None _handle_refresh(self, datetime.datetime _)
None _schedule_stream_refresh(self, datetime.datetime refresh_time)
None __init__(self, HomeAssistant hass, datetime.datetime expires_at, Callable[[], Awaitable[datetime.datetime|None]] refresh_cb)
None async_write_ha_state(self)
None async_on_remove(self, CALLBACK_TYPE func)
web.Response get(self, web.Request request, str config_key)
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
CALLBACK_TYPE async_track_point_in_utc_time(HomeAssistant hass, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action, datetime point_in_time)