Home Assistant Unofficial Reference 2024.12.1
camera.py
Go to the documentation of this file.
1 """Component providing support to the Ring Door Bell camera."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 from datetime import timedelta
8 import logging
9 from typing import TYPE_CHECKING, Any, Generic
10 
11 from aiohttp import web
12 from haffmpeg.camera import CameraMjpeg
13 from ring_doorbell import RingDoorBell
14 from ring_doorbell.webrtcstream import RingWebRtcMessage
15 
16 from homeassistant.components import ffmpeg
18  Camera,
19  CameraEntityDescription,
20  CameraEntityFeature,
21  RTCIceCandidateInit,
22  WebRTCAnswer,
23  WebRTCCandidate,
24  WebRTCError,
25  WebRTCSendMessage,
26 )
27 from homeassistant.core import HomeAssistant, callback
28 from homeassistant.exceptions import HomeAssistantError
29 from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
30 from homeassistant.helpers.entity_platform import AddEntitiesCallback
31 from homeassistant.util import dt as dt_util
32 
33 from . import RingConfigEntry
34 from .coordinator import RingDataCoordinator
35 from .entity import RingDeviceT, RingEntity, exception_wrap
36 
37 FORCE_REFRESH_INTERVAL = timedelta(minutes=3)
38 MOTION_DETECTION_CAPABILITY = "motion_detection"
39 
40 _LOGGER = logging.getLogger(__name__)
41 
42 
43 @dataclass(frozen=True, kw_only=True)
45  """Base class for event entity description."""
46 
47  exists_fn: Callable[[RingDoorBell], bool]
48  live_stream: bool
49  motion_detection: bool
50 
51 
52 CAMERA_DESCRIPTIONS: tuple[RingCameraEntityDescription, ...] = (
54  key="live_view",
55  translation_key="live_view",
56  exists_fn=lambda _: True,
57  live_stream=True,
58  motion_detection=False,
59  ),
61  key="last_recording",
62  translation_key="last_recording",
63  entity_registry_enabled_default=False,
64  exists_fn=lambda camera: camera.has_subscription,
65  live_stream=False,
66  motion_detection=True,
67  ),
68 )
69 
70 
72  hass: HomeAssistant,
73  entry: RingConfigEntry,
74  async_add_entities: AddEntitiesCallback,
75 ) -> None:
76  """Set up a Ring Door Bell and StickUp Camera."""
77  ring_data = entry.runtime_data
78  devices_coordinator = ring_data.devices_coordinator
79  ffmpeg_manager = ffmpeg.get_ffmpeg_manager(hass)
80 
81  cams = [
82  RingCam(camera, devices_coordinator, description, ffmpeg_manager=ffmpeg_manager)
83  for description in CAMERA_DESCRIPTIONS
84  for camera in ring_data.devices.video_devices
85  if description.exists_fn(camera)
86  ]
87 
88  async_add_entities(cams)
89 
90 
91 class RingCam(RingEntity[RingDoorBell], Camera):
92  """An implementation of a Ring Door Bell camera."""
93 
94  def __init__(
95  self,
96  device: RingDoorBell,
97  coordinator: RingDataCoordinator,
98  description: RingCameraEntityDescription,
99  *,
100  ffmpeg_manager: ffmpeg.FFmpegManager,
101  ) -> None:
102  """Initialize a Ring Door Bell camera."""
103  super().__init__(device, coordinator)
104  self.entity_descriptionentity_description = description
105  Camera.__init__(self)
106  self._ffmpeg_manager_ffmpeg_manager = ffmpeg_manager
107  self._last_event_last_event: dict[str, Any] | None = None
108  self._last_video_id_last_video_id: int | None = None
109  self._video_url_video_url: str | None = None
110  self._images_images: dict[tuple[int | None, int | None], bytes] = {}
111  self._expires_at_expires_at = dt_util.utcnow() - FORCE_REFRESH_INTERVAL
112  self._attr_unique_id_attr_unique_id = f"{device.id}-{description.key}"
113  if description.motion_detection and device.has_capability(
114  MOTION_DETECTION_CAPABILITY
115  ):
116  self._attr_motion_detection_enabled_attr_motion_detection_enabled = device.motion_detection
117  if description.live_stream:
118  self._attr_supported_features |= CameraEntityFeature.STREAM
119 
120  @callback
121  def _handle_coordinator_update(self) -> None:
122  """Call update method."""
123  self._device_device_device_device = self._get_coordinator_data_get_coordinator_data().get_video_device(
124  self._device_device_device_device.device_api_id
125  )
126  history_data = self._device_device_device_device.last_history
127  if history_data:
128  self._last_event_last_event = history_data[0]
129  # will call async_update to update the attributes and get the
130  # video url from the api
131  self.async_schedule_update_ha_stateasync_schedule_update_ha_state(True)
132  else:
133  self._last_event_last_event = None
134  self._last_video_id_last_video_id = None
135  self._video_url_video_url = None
136  self._images_images = {}
137  self._expires_at_expires_at = dt_util.utcnow()
138  self.async_write_ha_stateasync_write_ha_stateasync_write_ha_state()
139 
140  @property
141  def extra_state_attributes(self) -> dict[str, Any]:
142  """Return the state attributes."""
143  return {
144  "video_url": self._video_url_video_url,
145  "last_video_id": self._last_video_id_last_video_id,
146  }
147 
149  self, width: int | None = None, height: int | None = None
150  ) -> bytes | None:
151  """Return a still image response from the camera."""
152  key = (width, height)
153  if not (image := self._images_images.get(key)) and self._video_url_video_url is not None:
154  image = await ffmpeg.async_get_image(
155  self.hasshasshass,
156  self._video_url_video_url,
157  width=width,
158  height=height,
159  )
160 
161  if image:
162  self._images_images[key] = image
163 
164  return image
165 
167  self, request: web.Request
168  ) -> web.StreamResponse | None:
169  """Generate an HTTP MJPEG stream from the camera."""
170  if self._video_url_video_url is None:
171  return None
172 
173  stream = CameraMjpeg(self._ffmpeg_manager_ffmpeg_manager.binary)
174  await stream.open_camera(self._video_url_video_url)
175 
176  try:
177  stream_reader = await stream.get_reader()
178  return await async_aiohttp_proxy_stream(
179  self.hasshasshass,
180  request,
181  stream_reader,
182  self._ffmpeg_manager_ffmpeg_manager.ffmpeg_stream_content_type,
183  )
184  finally:
185  await stream.close()
186 
188  self, offer_sdp: str, session_id: str, send_message: WebRTCSendMessage
189  ) -> None:
190  """Return the source of the stream."""
191 
192  def message_wrapper(ring_message: RingWebRtcMessage) -> None:
193  if ring_message.error_code:
194  msg = ring_message.error_message or ""
195  send_message(WebRTCError(ring_message.error_code, msg))
196  elif ring_message.answer:
197  send_message(WebRTCAnswer(ring_message.answer))
198  elif ring_message.candidate:
199  send_message(
201  RTCIceCandidateInit(
202  ring_message.candidate,
203  sdp_m_line_index=ring_message.sdp_m_line_index or 0,
204  )
205  )
206  )
207 
208  return await self._device_device_device_device.generate_async_webrtc_stream(
209  offer_sdp, session_id, message_wrapper, keep_alive_timeout=None
210  )
211 
213  self, session_id: str, candidate: RTCIceCandidateInit
214  ) -> None:
215  """Handle a WebRTC candidate."""
216  if candidate.sdp_m_line_index is None:
217  msg = "The sdp_m_line_index is required for ring webrtc streaming"
218  raise HomeAssistantError(msg)
219  await self._device_device_device_device.on_webrtc_candidate(
220  session_id, candidate.candidate, candidate.sdp_m_line_index
221  )
222 
223  @callback
224  def close_webrtc_session(self, session_id: str) -> None:
225  """Close a WebRTC session."""
226  self._device_device_device_device.sync_close_webrtc_stream(session_id)
227 
228  async def async_update(self) -> None:
229  """Update camera entity and refresh attributes."""
230  if (
231  self._device_device_device_device.has_capability(MOTION_DETECTION_CAPABILITY)
232  and self._attr_motion_detection_enabled_attr_motion_detection_enabled != self._device_device_device_device.motion_detection
233  ):
234  self._attr_motion_detection_enabled_attr_motion_detection_enabled = self._device_device_device_device.motion_detection
235  self.async_write_ha_stateasync_write_ha_stateasync_write_ha_state()
236 
237  if TYPE_CHECKING:
238  # _last_event is set before calling update so will never be None
239  assert self._last_event_last_event
240 
241  if self._last_event_last_event["recording"]["status"] != "ready":
242  return
243 
244  utcnow = dt_util.utcnow()
245  if self._last_video_id_last_video_id == self._last_event_last_event["id"] and utcnow <= self._expires_at_expires_at:
246  return
247 
248  if self._last_video_id_last_video_id != self._last_event_last_event["id"]:
249  self._images_images = {}
250 
251  self._video_url_video_url = await self._async_get_video_async_get_video()
252 
253  self._last_video_id_last_video_id = self._last_event_last_event["id"]
254  self._expires_at_expires_at = FORCE_REFRESH_INTERVAL + utcnow
255 
256  @exception_wrap
257  async def _async_get_video(self) -> str | None:
258  if TYPE_CHECKING:
259  # _last_event is set before calling update so will never be None
260  assert self._last_event_last_event
261  event_id = self._last_event_last_event.get("id")
262  assert event_id and isinstance(event_id, int)
263  return await self._device_device_device_device.async_recording_url(event_id)
264 
265  @exception_wrap
266  async def _async_set_motion_detection_enabled(self, new_state: bool) -> None:
267  if not self._device_device_device_device.has_capability(MOTION_DETECTION_CAPABILITY):
268  _LOGGER.error(
269  "Entity %s does not have motion detection capability", self.entity_identity_id
270  )
271  return
272 
273  await self._device_device_device_device.async_set_motion_detection(new_state)
274  self._attr_motion_detection_enabled_attr_motion_detection_enabled = new_state
275  self.async_write_ha_stateasync_write_ha_stateasync_write_ha_state()
276 
277  async def async_enable_motion_detection(self) -> None:
278  """Enable motion detection in the camera."""
279  await self._async_set_motion_detection_enabled_async_set_motion_detection_enabled(True)
280 
281  async def async_disable_motion_detection(self) -> None:
282  """Disable motion detection in camera."""
283  await self._async_set_motion_detection_enabled_async_set_motion_detection_enabled(False)
None async_on_webrtc_candidate(self, str session_id, RTCIceCandidateInit candidate)
Definition: camera.py:214
None close_webrtc_session(self, str session_id)
Definition: camera.py:224
web.StreamResponse|None handle_async_mjpeg_stream(self, web.Request request)
Definition: camera.py:168
None async_handle_async_webrtc_offer(self, str offer_sdp, str session_id, WebRTCSendMessage send_message)
Definition: camera.py:189
bytes|None async_camera_image(self, int|None width=None, int|None height=None)
Definition: camera.py:150
None _async_set_motion_detection_enabled(self, bool new_state)
Definition: camera.py:266
dict[str, Any] extra_state_attributes(self)
Definition: camera.py:141
None __init__(self, RingDoorBell device, RingDataCoordinator coordinator, RingCameraEntityDescription description, *ffmpeg.FFmpegManager ffmpeg_manager)
Definition: camera.py:101
None async_schedule_update_ha_state(self, bool force_refresh=False)
Definition: entity.py:1265
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_entry(HomeAssistant hass, RingConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: camera.py:75
web.StreamResponse async_aiohttp_proxy_stream(HomeAssistant hass, web.BaseRequest request, aiohttp.StreamReader stream, str|None content_type, int buffer_size=102400, int timeout=10)