Home Assistant Unofficial Reference 2024.12.1
camera.py
Go to the documentation of this file.
1 """Support for Canary camera."""
2 
3 from __future__ import annotations
4 
5 from datetime import timedelta
6 import logging
7 from typing import Final
8 
9 from aiohttp.web import Request, StreamResponse
10 from canary.live_stream_api import LiveStreamSession
11 from canary.model import Device, Location
12 from haffmpeg.camera import CameraMjpeg
13 import voluptuous as vol
14 
15 from homeassistant.components import ffmpeg
17  PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA,
18  Camera,
19 )
20 from homeassistant.components.ffmpeg import FFmpegManager, get_ffmpeg_manager
21 from homeassistant.config_entries import ConfigEntry
22 from homeassistant.core import HomeAssistant
23 from homeassistant.helpers import config_validation as cv
24 from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
25 from homeassistant.helpers.device_registry import DeviceInfo
26 from homeassistant.helpers.entity_platform import AddEntitiesCallback
27 from homeassistant.helpers.update_coordinator import CoordinatorEntity
28 from homeassistant.util import dt as dt_util
29 
30 from .const import (
31  CONF_FFMPEG_ARGUMENTS,
32  DATA_COORDINATOR,
33  DEFAULT_FFMPEG_ARGUMENTS,
34  DOMAIN,
35  MANUFACTURER,
36 )
37 from .coordinator import CanaryDataUpdateCoordinator
38 
39 FORCE_CAMERA_REFRESH_INTERVAL: Final = timedelta(minutes=15)
40 
41 PLATFORM_SCHEMA: Final = vol.All(
42  cv.deprecated(CONF_FFMPEG_ARGUMENTS),
43  CAMERA_PLATFORM_SCHEMA.extend(
44  {
45  vol.Optional(
46  CONF_FFMPEG_ARGUMENTS, default=DEFAULT_FFMPEG_ARGUMENTS
47  ): cv.string
48  }
49  ),
50 )
51 
52 _LOGGER = logging.getLogger(__name__)
53 
54 
56  hass: HomeAssistant,
57  entry: ConfigEntry,
58  async_add_entities: AddEntitiesCallback,
59 ) -> None:
60  """Set up Canary sensors based on a config entry."""
61  coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
62  DATA_COORDINATOR
63  ]
64  ffmpeg_arguments: str = entry.options.get(
65  CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS
66  )
67 
69  (
71  hass,
72  coordinator,
73  location_id,
74  device,
75  ffmpeg_arguments,
76  )
77  for location_id, location in coordinator.data["locations"].items()
78  for device in location.devices
79  if device.is_online
80  ),
81  True,
82  )
83 
84 
85 class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera):
86  """An implementation of a Canary security camera."""
87 
88  def __init__(
89  self,
90  hass: HomeAssistant,
91  coordinator: CanaryDataUpdateCoordinator,
92  location_id: str,
93  device: Device,
94  ffmpeg_args: str,
95  ) -> None:
96  """Initialize a Canary security camera."""
97  super().__init__(coordinator)
98  Camera.__init__(self)
99  self._ffmpeg: FFmpegManager = get_ffmpeg_manager(hass)
100  self._ffmpeg_arguments_ffmpeg_arguments = ffmpeg_args
101  self._location_id_location_id = location_id
102  self._device_device = device
103  self._live_stream_session_live_stream_session: LiveStreamSession | None = None
104  self._attr_name_attr_name = device.name
105  self._attr_unique_id_attr_unique_id = str(device.device_id)
106  self._attr_device_info_attr_device_info = DeviceInfo(
107  identifiers={(DOMAIN, str(device.device_id))},
108  manufacturer=MANUFACTURER,
109  model=device.device_type["name"],
110  name=device.name,
111  )
112  self._image_image: bytes | None = None
113  self._expires_at_expires_at = dt_util.utcnow()
114  _LOGGER.debug(
115  "%s %s has been initialized", self.namenamename, device.device_type["name"]
116  )
117 
118  @property
119  def location(self) -> Location:
120  """Return information about the location."""
121  return self.coordinator.data["locations"][self._location_id_location_id]
122 
123  @property
124  def is_recording(self) -> bool:
125  """Return true if the device is recording."""
126  return self.locationlocation.is_recording # type: ignore[no-any-return]
127 
128  @property
129  def motion_detection_enabled(self) -> bool:
130  """Return the camera motion detection status."""
131  return not self.locationlocation.is_recording
132 
134  self, width: int | None = None, height: int | None = None
135  ) -> bytes | None:
136  """Return a still image response from the camera."""
137  utcnow = dt_util.utcnow()
138  if self._expires_at_expires_at <= utcnow:
139  _LOGGER.debug("Grabbing a live view image from %s", self.namenamename)
140  await self.hasshasshass.async_add_executor_job(self.renew_live_stream_sessionrenew_live_stream_session)
141 
142  if (live_stream_session := self._live_stream_session_live_stream_session) is None:
143  return None
144 
145  if not (live_stream_url := live_stream_session.live_stream_url):
146  return None
147 
148  image = await ffmpeg.async_get_image(
149  self.hasshasshass,
150  live_stream_url,
151  extra_cmd=self._ffmpeg_arguments_ffmpeg_arguments,
152  width=width,
153  height=height,
154  )
155 
156  if image:
157  self._image_image = image
158  self._expires_at_expires_at = FORCE_CAMERA_REFRESH_INTERVAL + utcnow
159  _LOGGER.debug("Grabbed a live view image from %s", self.namenamename)
160  await self.hasshasshass.async_add_executor_job(live_stream_session.stop_session)
161  _LOGGER.debug("Stopped live session from %s", self.namenamename)
162 
163  return self._image_image
164 
166  self, request: Request
167  ) -> StreamResponse | None:
168  """Generate an HTTP MJPEG stream from the camera."""
169  if self._live_stream_session_live_stream_session is None:
170  return None
171 
172  live_stream_url = await self.hasshasshass.async_add_executor_job(
173  getattr, self._live_stream_session_live_stream_session, "live_stream_url"
174  )
175  stream = CameraMjpeg(self._ffmpeg.binary)
176  await stream.open_camera(live_stream_url, extra_cmd=self._ffmpeg_arguments_ffmpeg_arguments)
177 
178  try:
179  stream_reader = await stream.get_reader()
180  return await async_aiohttp_proxy_stream(
181  self.hasshasshass,
182  request,
183  stream_reader,
184  self._ffmpeg.ffmpeg_stream_content_type,
185  )
186  finally:
187  await stream.close()
188 
189  def renew_live_stream_session(self) -> None:
190  """Renew live stream session."""
191  self._live_stream_session_live_stream_session = self.coordinator.canary.get_live_stream_session(
192  self._device_device
193  )
194 
195  _LOGGER.debug(
196  "Live Stream URL for %s is %s",
197  self.namenamename,
198  self._live_stream_session_live_stream_session.live_stream_url,
199  )
None __init__(self, HomeAssistant hass, CanaryDataUpdateCoordinator coordinator, str location_id, Device device, str ffmpeg_args)
Definition: camera.py:95
StreamResponse|None handle_async_mjpeg_stream(self, Request request)
Definition: camera.py:167
bytes|None async_camera_image(self, int|None width=None, int|None height=None)
Definition: camera.py:135
str|UndefinedType|None name(self)
Definition: entity.py:738
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: camera.py:59
FFmpegManager get_ffmpeg_manager(HomeAssistant hass)
Definition: __init__.py:106
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)