Home Assistant Unofficial Reference 2024.12.1
camera.py
Go to the documentation of this file.
1 """Support for IP Cameras."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Mapping
7 from datetime import datetime, timedelta
8 import logging
9 from typing import Any
10 
11 import httpx
12 import voluptuous as vol
13 import yarl
14 
15 from homeassistant.components.camera import Camera, CameraEntityFeature
17  CONF_RTSP_TRANSPORT,
18  CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
19 )
20 from homeassistant.config_entries import ConfigEntry
21 from homeassistant.const import (
22  CONF_AUTHENTICATION,
23  CONF_NAME,
24  CONF_PASSWORD,
25  CONF_USERNAME,
26  CONF_VERIFY_SSL,
27  HTTP_DIGEST_AUTHENTICATION,
28 )
29 from homeassistant.core import HomeAssistant
30 from homeassistant.exceptions import TemplateError
31 from homeassistant.helpers.device_registry import DeviceInfo
32 from homeassistant.helpers.entity_platform import AddEntitiesCallback
33 from homeassistant.helpers.httpx_client import get_async_client
34 from homeassistant.helpers.template import Template
35 
36 from . import DOMAIN
37 from .const import (
38  CONF_CONTENT_TYPE,
39  CONF_FRAMERATE,
40  CONF_LIMIT_REFETCH_TO_URL_CHANGE,
41  CONF_STILL_IMAGE_URL,
42  CONF_STREAM_SOURCE,
43  GET_IMAGE_TIMEOUT,
44 )
45 
46 _LOGGER = logging.getLogger(__name__)
47 
48 
50  hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
51 ) -> None:
52  """Set up a generic IP Camera."""
53 
55  [GenericCamera(hass, entry.options, entry.entry_id, entry.title)]
56  )
57 
58 
59 def generate_auth(device_info: Mapping[str, Any]) -> httpx.Auth | None:
60  """Generate httpx.Auth object from credentials."""
61  username: str | None = device_info.get(CONF_USERNAME)
62  password: str | None = device_info.get(CONF_PASSWORD)
63  authentication = device_info.get(CONF_AUTHENTICATION)
64  if username and password:
65  if authentication == HTTP_DIGEST_AUTHENTICATION:
66  return httpx.DigestAuth(username=username, password=password)
67  return httpx.BasicAuth(username=username, password=password)
68  return None
69 
70 
72  """A generic implementation of an IP camera."""
73 
74  _last_image: bytes | None
75  _last_update: datetime
76  _update_lock: asyncio.Lock
77 
78  def __init__(
79  self,
80  hass: HomeAssistant,
81  device_info: Mapping[str, Any],
82  identifier: str,
83  title: str,
84  ) -> None:
85  """Initialize a generic camera."""
86  super().__init__()
87  self.hasshasshass = hass
88  self._attr_unique_id_attr_unique_id = identifier
89  self._authentication_authentication = device_info.get(CONF_AUTHENTICATION)
90  self._username_username = device_info.get(CONF_USERNAME)
91  self._password_password = device_info.get(CONF_PASSWORD)
92  self._name_name = device_info.get(CONF_NAME, title)
93  self._still_image_url_still_image_url = device_info.get(CONF_STILL_IMAGE_URL)
94  if self._still_image_url_still_image_url:
95  self._still_image_url_still_image_url = Template(self._still_image_url_still_image_url, hass)
96  self._stream_source_stream_source = device_info.get(CONF_STREAM_SOURCE)
97  if self._stream_source_stream_source:
98  self._stream_source_stream_source = Template(self._stream_source_stream_source, hass)
99  self._limit_refetch_limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
100  self._attr_frame_interval_attr_frame_interval = 1 / device_info[CONF_FRAMERATE]
101  if self._stream_source_stream_source:
102  self._attr_supported_features_attr_supported_features = CameraEntityFeature.STREAM
103  self.content_typecontent_type = device_info[CONF_CONTENT_TYPE]
104  self.verify_sslverify_ssl = device_info[CONF_VERIFY_SSL]
105  if device_info.get(CONF_RTSP_TRANSPORT):
106  self.stream_options[CONF_RTSP_TRANSPORT] = device_info[CONF_RTSP_TRANSPORT]
107  self._auth_auth = generate_auth(device_info)
108  if device_info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS):
109  self.stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True
110 
111  self._last_url_last_url = None
112  self._last_image_last_image = None
113  self._last_update_last_update = datetime.min
114  self._update_lock_update_lock = asyncio.Lock()
115 
116  self._attr_device_info_attr_device_info = DeviceInfo(
117  identifiers={(DOMAIN, identifier)},
118  manufacturer="Generic",
119  )
120 
121  @property
122  def use_stream_for_stills(self) -> bool:
123  """Whether or not to use stream to generate stills."""
124  return not self._still_image_url_still_image_url
125 
127  self, width: int | None = None, height: int | None = None
128  ) -> bytes | None:
129  """Return a still image response from the camera."""
130  if not self._still_image_url_still_image_url:
131  return None
132  try:
133  url = self._still_image_url_still_image_url.async_render(parse_result=False)
134  except TemplateError as err:
135  _LOGGER.error("Error parsing template %s: %s", self._still_image_url_still_image_url, err)
136  return self._last_image_last_image
137 
138  try:
139  vol.Schema(vol.Url())(url)
140  except vol.Invalid as err:
141  _LOGGER.warning("Invalid URL '%s': %s, returning last image", url, err)
142  return self._last_image_last_image
143 
144  if url == self._last_url_last_url and self._limit_refetch_limit_refetch:
145  return self._last_image_last_image
146 
147  async with self._update_lock_update_lock:
148  if (
149  self._last_image_last_image is not None
150  and url == self._last_url_last_url
151  and self._last_update_last_update + timedelta(0, self._attr_frame_interval_attr_frame_interval)
152  > datetime.now()
153  ):
154  return self._last_image_last_image
155 
156  try:
157  update_time = datetime.now()
158  async_client = get_async_client(self.hasshasshass, verify_ssl=self.verify_sslverify_ssl)
159  response = await async_client.get(
160  url,
161  auth=self._auth_auth,
162  follow_redirects=True,
163  timeout=GET_IMAGE_TIMEOUT,
164  )
165  response.raise_for_status()
166  self._last_image_last_image = response.content
167  self._last_update_last_update = update_time
168 
169  except httpx.TimeoutException:
170  _LOGGER.error("Timeout getting camera image from %s", self._name_name)
171  return self._last_image_last_image
172  except (httpx.RequestError, httpx.HTTPStatusError) as err:
173  _LOGGER.error(
174  "Error getting new camera image from %s: %s", self._name_name, err
175  )
176  return self._last_image_last_image
177 
178  self._last_url_last_url = url
179  return self._last_image_last_image
180 
181  @property
182  def name(self) -> str:
183  """Return the name of this device."""
184  return self._name_name
185 
186  async def stream_source(self) -> str | None:
187  """Return the source of the stream."""
188  if self._stream_source_stream_source is None:
189  return None
190 
191  try:
192  stream_url = self._stream_source_stream_source.async_render(parse_result=False)
193  url = yarl.URL(stream_url)
194  if (
195  not url.user
196  and not url.password
197  and self._username_username
198  and self._password_password
199  and url.is_absolute()
200  ):
201  url = url.with_user(self._username_username).with_password(self._password_password)
202  return str(url)
203  except TemplateError as err:
204  _LOGGER.error("Error parsing template %s: %s", self._stream_source_stream_source, err)
205  return None
None __init__(self, HomeAssistant hass, Mapping[str, Any] device_info, str identifier, str title)
Definition: camera.py:84
bytes|None async_camera_image(self, int|None width=None, int|None height=None)
Definition: camera.py:128
httpx.Auth|None generate_auth(Mapping[str, Any] device_info)
Definition: camera.py:59
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: camera.py:51
httpx.AsyncClient get_async_client(HomeAssistant hass, bool verify_ssl=True)
Definition: httpx_client.py:41