Home Assistant Unofficial Reference 2024.12.1
camera.py
Go to the documentation of this file.
1 """Support for ONVIF Cameras with FFmpeg as decoder."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 
7 from haffmpeg.camera import CameraMjpeg
8 from onvif.exceptions import ONVIFError
9 import voluptuous as vol
10 from yarl import URL
11 
12 from homeassistant.components import ffmpeg
13 from homeassistant.components.camera import Camera, CameraEntityFeature
14 from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, get_ffmpeg_manager
16  CONF_RTSP_TRANSPORT,
17  CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
18  RTSP_TRANSPORTS,
19 )
20 from homeassistant.config_entries import ConfigEntry
21 from homeassistant.const import HTTP_BASIC_AUTHENTICATION
22 from homeassistant.core import HomeAssistant
23 from homeassistant.helpers import config_validation as cv, entity_platform
24 from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
25 from homeassistant.helpers.entity_platform import AddEntitiesCallback
26 
27 from .const import (
28  ABSOLUTE_MOVE,
29  ATTR_CONTINUOUS_DURATION,
30  ATTR_DISTANCE,
31  ATTR_MOVE_MODE,
32  ATTR_PAN,
33  ATTR_PRESET,
34  ATTR_SPEED,
35  ATTR_TILT,
36  ATTR_ZOOM,
37  CONF_SNAPSHOT_AUTH,
38  CONTINUOUS_MOVE,
39  DIR_DOWN,
40  DIR_LEFT,
41  DIR_RIGHT,
42  DIR_UP,
43  DOMAIN,
44  GOTOPRESET_MOVE,
45  LOGGER,
46  RELATIVE_MOVE,
47  SERVICE_PTZ,
48  STOP_MOVE,
49  ZOOM_IN,
50  ZOOM_OUT,
51 )
52 from .device import ONVIFDevice
53 from .entity import ONVIFBaseEntity
54 from .models import Profile
55 
56 
58  hass: HomeAssistant,
59  config_entry: ConfigEntry,
60  async_add_entities: AddEntitiesCallback,
61 ) -> None:
62  """Set up the ONVIF camera video stream."""
63  platform = entity_platform.async_get_current_platform()
64 
65  # Create PTZ service
66  platform.async_register_entity_service(
67  SERVICE_PTZ,
68  {
69  vol.Optional(ATTR_PAN): vol.In([DIR_LEFT, DIR_RIGHT]),
70  vol.Optional(ATTR_TILT): vol.In([DIR_UP, DIR_DOWN]),
71  vol.Optional(ATTR_ZOOM): vol.In([ZOOM_OUT, ZOOM_IN]),
72  vol.Optional(ATTR_DISTANCE, default=0.1): cv.small_float,
73  vol.Optional(ATTR_SPEED, default=0.5): cv.small_float,
74  vol.Optional(ATTR_MOVE_MODE, default=RELATIVE_MOVE): vol.In(
75  [
76  CONTINUOUS_MOVE,
77  RELATIVE_MOVE,
78  ABSOLUTE_MOVE,
79  GOTOPRESET_MOVE,
80  STOP_MOVE,
81  ]
82  ),
83  vol.Optional(ATTR_CONTINUOUS_DURATION, default=0.5): cv.small_float,
84  vol.Optional(ATTR_PRESET, default="0"): cv.string,
85  },
86  "async_perform_ptz",
87  )
88 
89  device = hass.data[DOMAIN][config_entry.unique_id]
91  [ONVIFCameraEntity(device, profile) for profile in device.profiles]
92  )
93 
94 
96  """Representation of an ONVIF camera."""
97 
98  _attr_supported_features = CameraEntityFeature.STREAM
99 
100  def __init__(self, device: ONVIFDevice, profile: Profile) -> None:
101  """Initialize ONVIF camera entity."""
102  ONVIFBaseEntity.__init__(self, device)
103  Camera.__init__(self)
104  self.profileprofile = profile
105  self.stream_options[CONF_RTSP_TRANSPORT] = device.config_entry.options.get(
106  CONF_RTSP_TRANSPORT, next(iter(RTSP_TRANSPORTS))
107  )
108  self.stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = (
109  device.config_entry.options.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False)
110  )
111  self._basic_auth_basic_auth = (
112  device.config_entry.data.get(CONF_SNAPSHOT_AUTH)
113  == HTTP_BASIC_AUTHENTICATION
114  )
115  self._stream_uri_stream_uri: str | None = None
116  self._stream_uri_future_stream_uri_future: asyncio.Future[str] | None = None
117  self._attr_entity_registry_enabled_default_attr_entity_registry_enabled_default = (
118  device.max_resolution == profile.video.resolution.width
119  )
120  if profile.index:
121  self._attr_unique_id_attr_unique_id = f"{self.mac_or_serial}_{profile.index}"
122  else:
123  self._attr_unique_id_attr_unique_id = self.mac_or_serialmac_or_serial
124  self._attr_name_attr_name = f"{device.name} {profile.name}"
125 
126  @property
127  def use_stream_for_stills(self) -> bool:
128  """Whether or not to use stream to generate stills."""
129  return bool(self.streamstream and self.streamstream.dynamic_stream_settings.preload_stream)
130 
131  async def stream_source(self):
132  """Return the stream source."""
133  return await self._async_get_stream_uri_async_get_stream_uri()
134 
136  self, width: int | None = None, height: int | None = None
137  ) -> bytes | None:
138  """Return a still image response from the camera."""
139 
140  if self.device.capabilities.snapshot:
141  try:
142  if image := await self.device.device.get_snapshot(
143  self.profileprofile.token, self._basic_auth_basic_auth
144  ):
145  return image
146  except ONVIFError as err:
147  LOGGER.error(
148  "Fetch snapshot image failed from %s, falling back to FFmpeg; %s",
149  self.device.name,
150  err,
151  )
152  else:
153  LOGGER.error(
154  "Fetch snapshot image failed from %s, falling back to FFmpeg",
155  self.device.name,
156  )
157 
158  stream_uri = await self._async_get_stream_uri_async_get_stream_uri()
159  return await ffmpeg.async_get_image(
160  self.hasshass,
161  stream_uri,
162  extra_cmd=self.device.config_entry.options.get(CONF_EXTRA_ARGUMENTS),
163  width=width,
164  height=height,
165  )
166 
167  async def handle_async_mjpeg_stream(self, request):
168  """Generate an HTTP MJPEG stream from the camera."""
169  LOGGER.debug("Handling mjpeg stream from camera '%s'", self.device.name)
170 
171  ffmpeg_manager = get_ffmpeg_manager(self.hasshass)
172  stream = CameraMjpeg(ffmpeg_manager.binary)
173  stream_uri = await self._async_get_stream_uri_async_get_stream_uri()
174 
175  await stream.open_camera(
176  stream_uri,
177  extra_cmd=self.device.config_entry.options.get(CONF_EXTRA_ARGUMENTS),
178  )
179 
180  try:
181  stream_reader = await stream.get_reader()
182  return await async_aiohttp_proxy_stream(
183  self.hasshass,
184  request,
185  stream_reader,
186  ffmpeg_manager.ffmpeg_stream_content_type,
187  )
188  finally:
189  await stream.close()
190 
191  async def _async_get_stream_uri(self) -> str:
192  """Return the stream URI."""
193  if self._stream_uri_stream_uri:
194  return self._stream_uri_stream_uri
195  if self._stream_uri_future_stream_uri_future:
196  return await self._stream_uri_future_stream_uri_future
197  loop = asyncio.get_running_loop()
198  self._stream_uri_future_stream_uri_future = loop.create_future()
199  try:
200  uri_no_auth = await self.device.async_get_stream_uri(self.profileprofile)
201  except (TimeoutError, Exception) as err:
202  LOGGER.error("Failed to get stream uri: %s", err)
203  if self._stream_uri_future_stream_uri_future:
204  self._stream_uri_future_stream_uri_future.set_exception(err)
205  raise
206  url = URL(uri_no_auth)
207  url = url.with_user(self.device.username)
208  url = url.with_password(self.device.password)
209  self._stream_uri_stream_uri = str(url)
210  self._stream_uri_future_stream_uri_future.set_result(self._stream_uri_stream_uri)
211  return self._stream_uri_stream_uri
212 
213  async def async_perform_ptz(
214  self,
215  distance,
216  speed,
217  move_mode,
218  continuous_duration,
219  preset,
220  pan=None,
221  tilt=None,
222  zoom=None,
223  ) -> None:
224  """Perform a PTZ action on the camera."""
225  await self.device.async_perform_ptz(
226  self.profileprofile,
227  distance,
228  speed,
229  move_mode,
230  continuous_duration,
231  preset,
232  pan,
233  tilt,
234  zoom,
235  )
None async_perform_ptz(self, distance, speed, move_mode, continuous_duration, preset, pan=None, tilt=None, zoom=None)
Definition: camera.py:223
bytes|None async_camera_image(self, int|None width=None, int|None height=None)
Definition: camera.py:137
None __init__(self, ONVIFDevice device, Profile profile)
Definition: camera.py:100
FFmpegManager get_ffmpeg_manager(HomeAssistant hass)
Definition: __init__.py:106
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: camera.py:61
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)