Home Assistant Unofficial Reference 2024.12.1
camera.py
Go to the documentation of this file.
1 """Support for Ubiquiti's UVC cameras."""
2 
3 from __future__ import annotations
4 
5 from datetime import datetime
6 import logging
7 import re
8 from typing import Any, cast
9 
10 from uvcclient import camera as uvc_camera, nvr
11 from uvcclient.camera import UVCCameraClient
12 from uvcclient.nvr import UVCRemote
13 import voluptuous as vol
14 
16  PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA,
17  Camera,
18  CameraEntityFeature,
19 )
20 from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_SSL
21 from homeassistant.core import HomeAssistant
22 from homeassistant.exceptions import PlatformNotReady
24 from homeassistant.helpers.entity_platform import AddEntitiesCallback
25 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
26 from homeassistant.util.dt import utc_from_timestamp
27 
28 _LOGGER = logging.getLogger(__name__)
29 
30 CONF_NVR = "nvr"
31 CONF_KEY = "key"
32 
33 DEFAULT_PASSWORD = "ubnt"
34 DEFAULT_PORT = 7080
35 DEFAULT_SSL = False
36 
37 PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend(
38  {
39  vol.Required(CONF_NVR): cv.string,
40  vol.Required(CONF_KEY): cv.string,
41  vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
42  vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
43  vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
44  }
45 )
46 
47 
49  hass: HomeAssistant,
50  config: ConfigType,
51  add_entities: AddEntitiesCallback,
52  discovery_info: DiscoveryInfoType | None = None,
53 ) -> None:
54  """Discover cameras on a Unifi NVR."""
55  addr = config[CONF_NVR]
56  key = config[CONF_KEY]
57  password = config[CONF_PASSWORD]
58  port = config[CONF_PORT]
59  ssl = config[CONF_SSL]
60 
61  try:
62  nvrconn = nvr.UVCRemote(addr, port, key, ssl=ssl)
63  # Exceptions may be raised in all method calls to the nvr library.
64  cameras = nvrconn.index()
65 
66  identifier = nvrconn.camera_identifier
67  # Filter out airCam models, which are not supported in the latest
68  # version of UnifiVideo and which are EOL by Ubiquiti
69  cameras = [
70  camera
71  for camera in cameras
72  if "airCam" not in nvrconn.get_camera(camera[identifier])["model"]
73  ]
74  except nvr.NotAuthorized:
75  _LOGGER.error("Authorization failure while connecting to NVR")
76  return
77  except nvr.NvrError as ex:
78  _LOGGER.error("NVR refuses to talk to me: %s", str(ex))
79  raise PlatformNotReady from ex
80 
82  (
83  UnifiVideoCamera(nvrconn, camera[identifier], camera["name"], password)
84  for camera in cameras
85  ),
86  True,
87  )
88 
89 
91  """A Ubiquiti Unifi Video Camera."""
92 
93  _attr_should_poll = True # Cameras default to False
94  _attr_brand = "Ubiquiti"
95  _attr_is_streaming = False
96  _caminfo: dict[str, Any]
97 
98  def __init__(self, camera: UVCRemote, uuid: str, name: str, password: str) -> None:
99  """Initialize an Unifi camera."""
100  super().__init__()
101  self._nvr_nvr = camera
102  self._uuid_uuid = self._attr_unique_id_attr_unique_id = uuid
103  self._attr_name_attr_name = name
104  self._password_password = password
105  self._connect_addr_connect_addr: str | None = None
106  self._camera_camera: UVCCameraClient | None = None
107 
108  @property
109  def supported_features(self) -> CameraEntityFeature:
110  """Return supported features."""
111  channels = self._caminfo_caminfo["channels"]
112  for channel in channels:
113  if channel["isRtspEnabled"]:
114  return CameraEntityFeature.STREAM
115 
116  return CameraEntityFeature(0)
117 
118  @property
119  def extra_state_attributes(self) -> dict[str, Any]:
120  """Return the camera state attributes."""
121  attr = {}
122  if self.motion_detection_enabledmotion_detection_enabledmotion_detection_enabled:
123  attr["last_recording_start_time"] = timestamp_ms_to_date(
124  self._caminfo_caminfo["lastRecordingStartTime"]
125  )
126  return attr
127 
128  @property
129  def is_recording(self) -> bool:
130  """Return true if the camera is recording."""
131  recording_state = "DISABLED"
132  if "recordingIndicator" in self._caminfo_caminfo:
133  recording_state = self._caminfo_caminfo["recordingIndicator"]
134 
135  return self._caminfo_caminfo["recordingSettings"][
136  "fullTimeRecordEnabled"
137  ] or recording_state in ("MOTION_INPROGRESS", "MOTION_FINISHED")
138 
139  @property
140  def motion_detection_enabled(self) -> bool:
141  """Camera Motion Detection Status."""
142  return bool(self._caminfo_caminfo["recordingSettings"]["motionRecordEnabled"])
143 
144  @property
145  def model(self) -> str:
146  """Return the model of this camera."""
147  return cast(str, self._caminfo_caminfo["model"])
148 
149  def _login(self) -> bool:
150  """Login to the camera."""
151  caminfo = self._caminfo_caminfo
152  if self._connect_addr_connect_addr:
153  addrs = [self._connect_addr_connect_addr]
154  else:
155  addrs = [caminfo["host"], caminfo["internalHost"]]
156 
157  client_cls: type[uvc_camera.UVCCameraClient]
158  if self._nvr_nvr.server_version >= (3, 2, 0):
159  client_cls = uvc_camera.UVCCameraClientV320
160  else:
161  client_cls = uvc_camera.UVCCameraClient
162 
163  if caminfo["username"] is None:
164  caminfo["username"] = "ubnt"
165 
166  assert isinstance(caminfo["username"], str)
167 
168  camera = None
169  for addr in addrs:
170  try:
171  camera = client_cls(addr, caminfo["username"], self._password_password)
172  camera.login()
173  _LOGGER.debug("Logged into UVC camera %s via %s", self._attr_name_attr_name, addr)
174  self._connect_addr_connect_addr = addr
175  break
176  except OSError:
177  pass
178  except uvc_camera.CameraConnectError:
179  pass
180  except uvc_camera.CameraAuthError:
181  pass
182  if not self._connect_addr_connect_addr:
183  _LOGGER.error("Unable to login to camera")
184  return False
185 
186  self._camera_camera = camera
187  self._caminfo_caminfo = caminfo
188  return True
189 
191  self, width: int | None = None, height: int | None = None
192  ) -> bytes | None:
193  """Return the image of this camera."""
194  if not self._camera_camera and not self._login_login():
195  return None
196 
197  def _get_image(retry: bool = True) -> bytes | None:
198  assert self._camera_camera is not None
199  try:
200  return self._camera_camera.get_snapshot()
201  except uvc_camera.CameraConnectError:
202  _LOGGER.error("Unable to contact camera")
203  return None
204  except uvc_camera.CameraAuthError:
205  if retry:
206  self._login_login()
207  return _get_image(retry=False)
208  _LOGGER.error("Unable to log into camera, unable to get snapshot")
209  raise
210 
211  return _get_image()
212 
213  def set_motion_detection(self, mode: bool) -> None:
214  """Set motion detection on or off."""
215  set_mode = "motion" if mode is True else "none"
216 
217  try:
218  self._nvr_nvr.set_recordmode(self._uuid_uuid, set_mode)
219  except nvr.NvrError as err:
220  _LOGGER.error("Unable to set recordmode to %s", set_mode)
221  _LOGGER.debug(err)
222 
223  def enable_motion_detection(self) -> None:
224  """Enable motion detection in camera."""
225  self.set_motion_detectionset_motion_detection(True)
226 
227  def disable_motion_detection(self) -> None:
228  """Disable motion detection in camera."""
229  self.set_motion_detectionset_motion_detection(False)
230 
231  async def stream_source(self) -> str | None:
232  """Return the source of the stream."""
233  for channel in self._caminfo_caminfo["channels"]:
234  if channel["isRtspEnabled"]:
235  return cast(
236  str,
237  next(
238  (
239  uri
240  for i, uri in enumerate(channel["rtspUris"])
241  if re.search(self._nvr_nvr._host, uri) # noqa: SLF001
242  )
243  ),
244  )
245 
246  return None
247 
248  def update(self) -> None:
249  """Update the info."""
250  self._caminfo_caminfo = self._nvr_nvr.get_camera(self._uuid_uuid)
251 
252 
253 def timestamp_ms_to_date(epoch_ms: int) -> datetime | None:
254  """Convert millisecond timestamp to datetime."""
255  if epoch_ms:
256  return utc_from_timestamp(epoch_ms / 1000)
257  return None
CameraEntityFeature supported_features(self)
Definition: camera.py:109
None __init__(self, UVCRemote camera, str uuid, str name, str password)
Definition: camera.py:98
bytes|None camera_image(self, int|None width=None, int|None height=None)
Definition: camera.py:192
None add_entities(HomeAssistant hass, FreeboxRouter router, AddEntitiesCallback async_add_entities, set[str] tracked)
Definition: camera.py:54
None setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: camera.py:53
datetime|None timestamp_ms_to_date(int epoch_ms)
Definition: camera.py:253