Home Assistant Unofficial Reference 2024.12.1
camera.py
Go to the documentation of this file.
1 """The motionEye integration."""
2 
3 from __future__ import annotations
4 
5 from contextlib import suppress
6 from types import MappingProxyType
7 from typing import Any
8 
9 import aiohttp
10 from jinja2 import Template
11 from motioneye_client.client import MotionEyeClient, MotionEyeClientURLParseError
12 from motioneye_client.const import (
13  DEFAULT_SURVEILLANCE_USERNAME,
14  KEY_ACTION_SNAPSHOT,
15  KEY_MOTION_DETECTION,
16  KEY_STREAMING_AUTH_MODE,
17  KEY_TEXT_OVERLAY_CAMERA_NAME,
18  KEY_TEXT_OVERLAY_CUSTOM_TEXT,
19  KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT,
20  KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT,
21  KEY_TEXT_OVERLAY_DISABLED,
22  KEY_TEXT_OVERLAY_LEFT,
23  KEY_TEXT_OVERLAY_RIGHT,
24  KEY_TEXT_OVERLAY_TIMESTAMP,
25 )
26 import voluptuous as vol
27 
29  CONF_MJPEG_URL,
30  CONF_STILL_IMAGE_URL,
31  MjpegCamera,
32 )
33 from homeassistant.config_entries import ConfigEntry
34 from homeassistant.const import (
35  CONF_AUTHENTICATION,
36  CONF_NAME,
37  CONF_PASSWORD,
38  CONF_USERNAME,
39  HTTP_BASIC_AUTHENTICATION,
40  HTTP_DIGEST_AUTHENTICATION,
41  Platform,
42 )
43 from homeassistant.core import HomeAssistant, callback
44 from homeassistant.helpers import config_validation as cv, entity_platform
45 from homeassistant.helpers.entity_platform import AddEntitiesCallback
46 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
47 
48 from . import get_camera_from_cameras, is_acceptable_camera, listen_for_new_cameras
49 from .const import (
50  CONF_ACTION,
51  CONF_CLIENT,
52  CONF_COORDINATOR,
53  CONF_STREAM_URL_TEMPLATE,
54  CONF_SURVEILLANCE_PASSWORD,
55  CONF_SURVEILLANCE_USERNAME,
56  DOMAIN,
57  MOTIONEYE_MANUFACTURER,
58  SERVICE_ACTION,
59  SERVICE_SET_TEXT_OVERLAY,
60  SERVICE_SNAPSHOT,
61  TYPE_MOTIONEYE_MJPEG_CAMERA,
62 )
63 from .entity import MotionEyeEntity
64 
65 PLATFORMS = [Platform.CAMERA]
66 
67 SCHEMA_TEXT_OVERLAY = vol.In(
68  [
69  KEY_TEXT_OVERLAY_DISABLED,
70  KEY_TEXT_OVERLAY_TIMESTAMP,
71  KEY_TEXT_OVERLAY_CUSTOM_TEXT,
72  KEY_TEXT_OVERLAY_CAMERA_NAME,
73  ]
74 )
75 SCHEMA_SERVICE_SET_TEXT = vol.Schema(
76  vol.All(
77  cv.make_entity_service_schema(
78  {
79  vol.Optional(KEY_TEXT_OVERLAY_LEFT): SCHEMA_TEXT_OVERLAY,
80  vol.Optional(KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT): cv.string,
81  vol.Optional(KEY_TEXT_OVERLAY_RIGHT): SCHEMA_TEXT_OVERLAY,
82  vol.Optional(KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT): cv.string,
83  },
84  ),
85  cv.has_at_least_one_key(
86  KEY_TEXT_OVERLAY_LEFT,
87  KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT,
88  KEY_TEXT_OVERLAY_RIGHT,
89  KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT,
90  ),
91  ),
92 )
93 
94 
96  hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
97 ) -> None:
98  """Set up motionEye from a config entry."""
99  entry_data = hass.data[DOMAIN][entry.entry_id]
100 
101  @callback
102  def camera_add(camera: dict[str, Any]) -> None:
103  """Add a new motionEye camera."""
105  [
107  entry.entry_id,
108  entry.data.get(
109  CONF_SURVEILLANCE_USERNAME, DEFAULT_SURVEILLANCE_USERNAME
110  ),
111  entry.data.get(CONF_SURVEILLANCE_PASSWORD, ""),
112  camera,
113  entry_data[CONF_CLIENT],
114  entry_data[CONF_COORDINATOR],
115  entry.options,
116  )
117  ]
118  )
119 
120  listen_for_new_cameras(hass, entry, camera_add)
121 
122  platform = entity_platform.async_get_current_platform()
123  platform.async_register_entity_service(
124  SERVICE_SET_TEXT_OVERLAY,
125  SCHEMA_SERVICE_SET_TEXT,
126  "async_set_text_overlay",
127  )
128  platform.async_register_entity_service(
129  SERVICE_ACTION,
130  {vol.Required(CONF_ACTION): cv.string},
131  "async_request_action",
132  )
133  platform.async_register_entity_service(
134  SERVICE_SNAPSHOT,
135  None,
136  "async_request_snapshot",
137  )
138 
139 
141  """motionEye mjpeg camera."""
142 
143  _attr_brand = MOTIONEYE_MANUFACTURER
144  # motionEye cameras are always streaming or unavailable.
145  _attr_is_streaming = True
146 
147  def __init__(
148  self,
149  config_entry_id: str,
150  username: str,
151  password: str,
152  camera: dict[str, Any],
153  client: MotionEyeClient,
154  coordinator: DataUpdateCoordinator,
155  options: MappingProxyType[str, str],
156  ) -> None:
157  """Initialize a MJPEG camera."""
158  self._surveillance_username_surveillance_username = username
159  self._surveillance_password_surveillance_password = password
160  self._motion_detection_enabled_motion_detection_enabled: bool = camera.get(KEY_MOTION_DETECTION, False)
161 
162  MotionEyeEntity.__init__(
163  self,
164  config_entry_id,
165  TYPE_MOTIONEYE_MJPEG_CAMERA,
166  camera,
167  client,
168  coordinator,
169  options,
170  )
171  MjpegCamera.__init__(
172  self,
173  verify_ssl=False,
174  **self._get_mjpeg_camera_properties_for_camera_get_mjpeg_camera_properties_for_camera(camera),
175  )
176 
177  @callback
179  self, camera: dict[str, Any]
180  ) -> dict[str, Any]:
181  """Convert a motionEye camera to MjpegCamera internal properties."""
182  auth = None
183  if camera.get(KEY_STREAMING_AUTH_MODE) in (
184  HTTP_BASIC_AUTHENTICATION,
185  HTTP_DIGEST_AUTHENTICATION,
186  ):
187  auth = camera[KEY_STREAMING_AUTH_MODE]
188 
189  streaming_template = self._options_options.get(CONF_STREAM_URL_TEMPLATE, "").strip()
190  streaming_url = None
191 
192  if streaming_template:
193  # Note: Can't use homeassistant.helpers.template as it requires hass
194  # which is not available during entity construction.
195  streaming_url = Template(streaming_template).render(**camera)
196  else:
197  with suppress(MotionEyeClientURLParseError):
198  streaming_url = self._client_client.get_camera_stream_url(camera)
199 
200  return {
201  CONF_NAME: None,
202  CONF_USERNAME: self._surveillance_username_surveillance_username if auth is not None else None,
203  CONF_PASSWORD: self._surveillance_password_surveillance_password if auth is not None else "",
204  CONF_MJPEG_URL: streaming_url or "",
205  CONF_STILL_IMAGE_URL: self._client_client.get_camera_snapshot_url(camera),
206  CONF_AUTHENTICATION: auth,
207  }
208 
209  @callback
210  def _set_mjpeg_camera_state_for_camera(self, camera: dict[str, Any]) -> None:
211  """Set the internal state to match the given camera."""
212 
213  # Sets the state of the underlying (inherited) MjpegCamera based on the updated
214  # MotionEye camera dictionary.
215  properties = self._get_mjpeg_camera_properties_for_camera_get_mjpeg_camera_properties_for_camera(camera)
216  self._username_username_username = properties[CONF_USERNAME]
217  self._password_password_password = properties[CONF_PASSWORD]
218  self._mjpeg_url_mjpeg_url_mjpeg_url = properties[CONF_MJPEG_URL]
219  self._still_image_url_still_image_url_still_image_url = properties[CONF_STILL_IMAGE_URL]
220  self._authentication_authentication_authentication = properties[CONF_AUTHENTICATION]
221 
222  if (
223  self._authentication_authentication_authentication == HTTP_BASIC_AUTHENTICATION
224  and self._username_username_username is not None
225  ):
226  self._auth_auth_auth = aiohttp.BasicAuth(self._username_username_username, password=self._password_password_password)
227 
229  """Determine if a camera is streaming/usable."""
230  return is_acceptable_camera(
231  self._camera_camera
232  ) and MotionEyeClient.is_camera_streaming(self._camera_camera)
233 
234  @property
235  def available(self) -> bool:
236  """Return if entity is available."""
237  return super().available and self._is_acceptable_streaming_camera_is_acceptable_streaming_camera()
238 
239  @callback
240  def _handle_coordinator_update(self) -> None:
241  """Handle updated data from the coordinator."""
242  self._camera_camera = get_camera_from_cameras(self._camera_id_camera_id, self.coordinator.data)
243  if self._camera_camera and self._is_acceptable_streaming_camera_is_acceptable_streaming_camera():
244  self._set_mjpeg_camera_state_for_camera_set_mjpeg_camera_state_for_camera(self._camera_camera)
245  self._motion_detection_enabled_motion_detection_enabled = self._camera_camera.get(
246  KEY_MOTION_DETECTION, False
247  )
249 
250  @property
251  def motion_detection_enabled(self) -> bool:
252  """Return the camera motion detection status."""
253  return self._motion_detection_enabled_motion_detection_enabled
254 
256  self,
257  left_text: str | None = None,
258  right_text: str | None = None,
259  custom_left_text: str | None = None,
260  custom_right_text: str | None = None,
261  ) -> None:
262  """Set text overlay for a camera."""
263  # Fetch the very latest camera config to reduce the risk of updating with a
264  # stale configuration.
265  camera = await self._client_client.async_get_camera(self._camera_id_camera_id)
266  if not camera:
267  return
268  if left_text is not None:
269  camera[KEY_TEXT_OVERLAY_LEFT] = left_text
270  if right_text is not None:
271  camera[KEY_TEXT_OVERLAY_RIGHT] = right_text
272  if custom_left_text is not None:
273  camera[KEY_TEXT_OVERLAY_CUSTOM_TEXT_LEFT] = custom_left_text.encode(
274  "unicode_escape"
275  ).decode("UTF-8")
276  if custom_right_text is not None:
277  camera[KEY_TEXT_OVERLAY_CUSTOM_TEXT_RIGHT] = custom_right_text.encode(
278  "unicode_escape"
279  ).decode("UTF-8")
280  await self._client_client.async_set_camera(self._camera_id_camera_id, camera)
281 
282  async def async_request_action(self, action: str) -> None:
283  """Call a motionEye action on a camera."""
284  await self._client_client.async_action(self._camera_id_camera_id, action)
285 
286  async def async_request_snapshot(self) -> None:
287  """Request a motionEye snapshot be saved."""
288  await self.async_request_actionasync_request_action(KEY_ACTION_SNAPSHOT)
None __init__(self, str config_entry_id, str username, str password, dict[str, Any] camera, MotionEyeClient client, DataUpdateCoordinator coordinator, MappingProxyType[str, str] options)
Definition: camera.py:156
dict[str, Any] _get_mjpeg_camera_properties_for_camera(self, dict[str, Any] camera)
Definition: camera.py:180
None async_set_text_overlay(self, str|None left_text=None, str|None right_text=None, str|None custom_left_text=None, str|None custom_right_text=None)
Definition: camera.py:261
None _set_mjpeg_camera_state_for_camera(self, dict[str, Any] camera)
Definition: camera.py:210
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: camera.py:97
bool is_acceptable_camera(dict[str, Any]|None camera)
Definition: __init__.py:132
None listen_for_new_cameras(HomeAssistant hass, ConfigEntry entry, Callable add_func)
Definition: __init__.py:142
dict[str, Any]|None get_camera_from_cameras(int camera_id, dict[str, Any]|None data)
Definition: __init__.py:123