Home Assistant Unofficial Reference 2024.12.1
device.py
Go to the documentation of this file.
1 """ONVIF device abstraction."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from contextlib import suppress
7 import datetime as dt
8 import os
9 import time
10 from typing import Any
11 
12 from httpx import RequestError
13 import onvif
14 from onvif import ONVIFCamera
15 from onvif.exceptions import ONVIFError
16 from zeep.exceptions import Fault, TransportError, XMLParseError, XMLSyntaxError
17 
18 from homeassistant.config_entries import ConfigEntry
19 from homeassistant.const import (
20  CONF_HOST,
21  CONF_NAME,
22  CONF_PASSWORD,
23  CONF_PORT,
24  CONF_USERNAME,
25  Platform,
26 )
27 from homeassistant.core import HomeAssistant, callback
28 import homeassistant.util.dt as dt_util
29 
30 from .const import (
31  ABSOLUTE_MOVE,
32  CONF_ENABLE_WEBHOOKS,
33  CONTINUOUS_MOVE,
34  DEFAULT_ENABLE_WEBHOOKS,
35  GET_CAPABILITIES_EXCEPTIONS,
36  GOTOPRESET_MOVE,
37  LOGGER,
38  PAN_FACTOR,
39  RELATIVE_MOVE,
40  STOP_MOVE,
41  TILT_FACTOR,
42  ZOOM_FACTOR,
43 )
44 from .event import EventManager
45 from .models import PTZ, Capabilities, DeviceInfo, Profile, Resolution, Video
46 
47 
49  """Manages an ONVIF device."""
50 
51  device: ONVIFCamera
52  events: EventManager
53 
54  def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
55  """Initialize the device."""
56  self.hass: HomeAssistant = hass
57  self.config_entry: ConfigEntry = config_entry
58  self._original_options_original_options = dict(config_entry.options)
59  self.available: bool = True
60 
61  self.infoinfo: DeviceInfo = DeviceInfo()
62  self.capabilitiescapabilities: Capabilities = Capabilities()
63  self.onvif_capabilitiesonvif_capabilities: dict[str, Any] | None = None
64  self.profilesprofiles: list[Profile] = []
65  self.max_resolutionmax_resolution: int = 0
66  self.platforms: list[Platform] = []
67 
68  self._dt_diff_seconds_dt_diff_seconds: float = 0
69 
71  self, hass: HomeAssistant, entry: ConfigEntry
72  ) -> None:
73  """Handle options update."""
74  if self._original_options_original_options != entry.options:
75  hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
76 
77  @property
78  def name(self) -> str:
79  """Return the name of this device."""
80  return self.config_entry.data[CONF_NAME]
81 
82  @property
83  def host(self) -> str:
84  """Return the host of this device."""
85  return self.config_entry.data[CONF_HOST]
86 
87  @property
88  def port(self) -> int:
89  """Return the port of this device."""
90  return self.config_entry.data[CONF_PORT]
91 
92  @property
93  def username(self) -> str:
94  """Return the username of this device."""
95  return self.config_entry.data[CONF_USERNAME]
96 
97  @property
98  def password(self) -> str:
99  """Return the password of this device."""
100  return self.config_entry.data[CONF_PASSWORD]
101 
102  async def async_setup(self) -> None:
103  """Set up the device."""
104  self.devicedevice = get_device(
105  self.hass,
106  host=self.config_entry.data[CONF_HOST],
107  port=self.config_entry.data[CONF_PORT],
108  username=self.config_entry.data[CONF_USERNAME],
109  password=self.config_entry.data[CONF_PASSWORD],
110  )
111 
112  # Get all device info
113  await self.devicedevice.update_xaddrs()
114  LOGGER.debug("%s: xaddrs = %s", self.namename, self.devicedevice.xaddrs)
115 
116  # Get device capabilities
117  self.onvif_capabilitiesonvif_capabilities = await self.devicedevice.get_capabilities()
118 
119  await self.async_check_date_and_timeasync_check_date_and_time()
120 
121  # Create event manager
122  assert self.config_entry.unique_id
123  self.eventsevents = EventManager(self.hass, self.devicedevice, self.config_entry, self.namename)
124 
125  # Fetch basic device info and capabilities
126  self.infoinfo = await self.async_get_device_infoasync_get_device_info()
127  LOGGER.debug("%s: camera info = %s", self.namename, self.infoinfo)
128 
129  #
130  # We need to check capabilities before profiles, because we need the data
131  # from capabilities to determine profiles correctly.
132  #
133  # We no longer initialize events in capabilities to avoid the problem
134  # where cameras become slow to respond for a bit after starting events, and
135  # instead we start events last and than update capabilities.
136  #
137  LOGGER.debug("%s: fetching initial capabilities", self.namename)
138  self.capabilitiescapabilities = await self.async_get_capabilitiesasync_get_capabilities()
139 
140  LOGGER.debug("%s: fetching profiles", self.namename)
141  self.profilesprofiles = await self.async_get_profilesasync_get_profiles()
142  LOGGER.debug("Camera %s profiles = %s", self.namename, self.profilesprofiles)
143 
144  # No camera profiles to add
145  if not self.profilesprofiles:
146  raise ONVIFError("No camera profiles found")
147 
148  if self.capabilitiescapabilities.ptz:
149  LOGGER.debug("%s: creating PTZ service", self.namename)
150  await self.devicedevice.create_ptz_service()
151 
152  # Determine max resolution from profiles
153  self.max_resolutionmax_resolution = max(
154  profile.video.resolution.width
155  for profile in self.profilesprofiles
156  if profile.video.encoding == "H264"
157  )
158 
159  # Start events last since some cameras become slow to respond
160  # for a bit after starting events
161  LOGGER.debug("%s: starting events", self.namename)
162  self.capabilitiescapabilities.events = await self.async_start_eventsasync_start_events()
163  LOGGER.debug("Camera %s capabilities = %s", self.namename, self.capabilitiescapabilities)
164 
165  # Bind the listener to the ONVIFDevice instance since
166  # async_update_listener only creates a weak reference to the listener
167  # and we need to make sure it doesn't get garbage collected since only
168  # the ONVIFDevice instance is stored in hass.data
169  self.config_entry.async_on_unload(
170  self.config_entry.add_update_listener(self._async_update_listener_async_update_listener)
171  )
172 
173  async def async_stop(self, event=None):
174  """Shut it all down."""
175  if self.eventsevents:
176  await self.eventsevents.async_stop()
177  await self.devicedevice.close()
178 
179  async def async_manually_set_date_and_time(self) -> None:
180  """Set Date and Time Manually using SetSystemDateAndTime command."""
181  device_mgmt = await self.devicedevice.create_devicemgmt_service()
182 
183  # Retrieve DateTime object from camera to use as template for Set operation
184  device_time = await device_mgmt.GetSystemDateAndTime()
185 
186  system_date = dt_util.utcnow()
187  LOGGER.debug("System date (UTC): %s", system_date)
188 
189  dt_param = device_mgmt.create_type("SetSystemDateAndTime")
190  dt_param.DateTimeType = "Manual"
191  # Retrieve DST setting from system
192  dt_param.DaylightSavings = bool(time.localtime().tm_isdst)
193  dt_param.UTCDateTime = {
194  "Date": {
195  "Year": system_date.year,
196  "Month": system_date.month,
197  "Day": system_date.day,
198  },
199  "Time": {
200  "Hour": system_date.hour,
201  "Minute": system_date.minute,
202  "Second": system_date.second,
203  },
204  }
205  # Retrieve timezone from system
206  system_timezone = str(system_date.astimezone().tzinfo)
207  timezone_names: list[str | None] = [system_timezone]
208  if (time_zone := device_time.TimeZone) and system_timezone != time_zone.TZ:
209  timezone_names.append(time_zone.TZ)
210  timezone_names.append(None)
211  timezone_max_idx = len(timezone_names) - 1
212  LOGGER.debug(
213  "%s: SetSystemDateAndTime: timezone_names:%s", self.namename, timezone_names
214  )
215  for idx, timezone_name in enumerate(timezone_names):
216  dt_param.TimeZone = timezone_name
217  LOGGER.debug("%s: SetSystemDateAndTime: %s", self.namename, dt_param)
218  try:
219  await device_mgmt.SetSystemDateAndTime(dt_param)
220  LOGGER.debug("%s: SetSystemDateAndTime: success", self.namename)
221  # Some cameras don't support setting the timezone and will throw an IndexError
222  # if we try to set it. If we get an error, try again without the timezone.
223  except (IndexError, Fault):
224  if idx == timezone_max_idx:
225  raise
226  else:
227  return
228 
229  async def async_check_date_and_time(self) -> None:
230  """Warns if device and system date not synced."""
231  LOGGER.debug("%s: Setting up the ONVIF device management service", self.namename)
232  device_mgmt = await self.devicedevice.create_devicemgmt_service()
233  system_date = dt_util.utcnow()
234 
235  LOGGER.debug("%s: Retrieving current device date/time", self.namename)
236  try:
237  device_time = await device_mgmt.GetSystemDateAndTime()
238  except RequestError as err:
239  LOGGER.warning(
240  "Couldn't get device '%s' date/time. Error: %s", self.namename, err
241  )
242  return
243 
244  if not device_time:
245  LOGGER.debug(
246  """Couldn't get device '%s' date/time.
247  GetSystemDateAndTime() return null/empty""",
248  self.namename,
249  )
250  return
251 
252  LOGGER.debug("%s: Device time: %s", self.namename, device_time)
253 
254  tzone = dt_util.get_default_time_zone()
255  cdate = device_time.LocalDateTime
256  if device_time.UTCDateTime:
257  tzone = dt_util.UTC
258  cdate = device_time.UTCDateTime
259  elif device_time.TimeZone:
260  tzone = await dt_util.async_get_time_zone(device_time.TimeZone.TZ) or tzone
261 
262  if cdate is None:
263  LOGGER.warning("%s: Could not retrieve date/time on this camera", self.namename)
264  return
265 
266  cam_date = dt.datetime(
267  cdate.Date.Year,
268  cdate.Date.Month,
269  cdate.Date.Day,
270  cdate.Time.Hour,
271  cdate.Time.Minute,
272  cdate.Time.Second,
273  0,
274  tzone,
275  )
276 
277  cam_date_utc = cam_date.astimezone(dt_util.UTC)
278 
279  LOGGER.debug(
280  "%s: Device date/time: %s | System date/time: %s",
281  self.namename,
282  cam_date_utc,
283  system_date,
284  )
285 
286  dt_diff = cam_date - system_date
287  self._dt_diff_seconds_dt_diff_seconds = dt_diff.total_seconds()
288 
289  # It could be off either direction, so we need to check the absolute value
290  if abs(self._dt_diff_seconds_dt_diff_seconds) < 5:
291  return
292 
293  if device_time.DateTimeType != "Manual":
294  self._async_log_time_out_of_sync_async_log_time_out_of_sync(cam_date_utc, system_date)
295  return
296 
297  # Set Date and Time ourselves if Date and Time is set manually in the camera.
298  try:
299  await self.async_manually_set_date_and_timeasync_manually_set_date_and_time()
300  except (RequestError, TransportError, IndexError, Fault):
301  LOGGER.warning("%s: Could not sync date/time on this camera", self.namename)
302  self._async_log_time_out_of_sync_async_log_time_out_of_sync(cam_date_utc, system_date)
303 
304  @callback
306  self, cam_date_utc: dt.datetime, system_date: dt.datetime
307  ) -> None:
308  """Log a warning if the camera and system date/time are not synced."""
309  LOGGER.warning(
310  (
311  "The date/time on %s (UTC) is '%s', "
312  "which is different from the system '%s', "
313  "this could lead to authentication issues"
314  ),
315  self.namename,
316  cam_date_utc,
317  system_date,
318  )
319 
320  async def async_get_device_info(self) -> DeviceInfo:
321  """Obtain information about this device."""
322  device_mgmt = await self.devicedevice.create_devicemgmt_service()
323  manufacturer = None
324  model = None
325  firmware_version = None
326  serial_number = None
327  try:
328  device_info = await device_mgmt.GetDeviceInformation()
329  except (XMLParseError, XMLSyntaxError, TransportError) as ex:
330  # Some cameras have invalid UTF-8 in their device information (TransportError)
331  # and others have completely invalid XML (XMLParseError, XMLSyntaxError)
332  LOGGER.warning("%s: Failed to fetch device information: %s", self.namename, ex)
333  else:
334  manufacturer = device_info.Manufacturer
335  model = device_info.Model
336  firmware_version = device_info.FirmwareVersion
337  serial_number = device_info.SerialNumber
338 
339  # Grab the last MAC address for backwards compatibility
340  mac = None
341  try:
342  network_interfaces = await device_mgmt.GetNetworkInterfaces()
343  for interface in network_interfaces:
344  if interface.Enabled:
345  mac = interface.Info.HwAddress
346  except Fault as fault:
347  if "not implemented" not in fault.message:
348  raise
349 
350  LOGGER.debug(
351  "Couldn't get network interfaces from ONVIF device '%s'. Error: %s",
352  self.namename,
353  fault,
354  )
355 
356  return DeviceInfo(
357  manufacturer,
358  model,
359  firmware_version,
360  serial_number,
361  mac,
362  )
363 
364  async def async_get_capabilities(self):
365  """Obtain information about the available services on the device."""
366  snapshot = False
367  with suppress(*GET_CAPABILITIES_EXCEPTIONS):
368  media_service = await self.devicedevice.create_media_service()
369  media_capabilities = await media_service.GetServiceCapabilities()
370  snapshot = media_capabilities and media_capabilities.SnapshotUri
371 
372  ptz = False
373  with suppress(*GET_CAPABILITIES_EXCEPTIONS):
374  self.devicedevice.get_definition("ptz")
375  ptz = True
376 
377  imaging = False
378  with suppress(*GET_CAPABILITIES_EXCEPTIONS):
379  await self.devicedevice.create_imaging_service()
380  imaging = True
381 
382  return Capabilities(snapshot=snapshot, ptz=ptz, imaging=imaging)
383 
384  async def async_start_events(self):
385  """Start the event handler."""
386  with suppress(*GET_CAPABILITIES_EXCEPTIONS, XMLParseError):
387  onvif_capabilities = self.onvif_capabilitiesonvif_capabilities or {}
388  pull_point_support = (onvif_capabilities.get("Events") or {}).get(
389  "WSPullPointSupport"
390  )
391  LOGGER.debug("%s: WSPullPointSupport: %s", self.namename, pull_point_support)
392  # Even if the camera claims it does not support PullPoint, try anyway
393  # since at least some AXIS and Bosch models do. The reverse is also
394  # true where some cameras claim they support PullPoint but don't so
395  # the only way to know is to try.
396  return await self.eventsevents.async_start(
397  True,
398  self.config_entry.options.get(
399  CONF_ENABLE_WEBHOOKS, DEFAULT_ENABLE_WEBHOOKS
400  ),
401  )
402 
403  return False
404 
405  async def async_get_profiles(self) -> list[Profile]:
406  """Obtain media profiles for this device."""
407  media_service = await self.devicedevice.create_media_service()
408  LOGGER.debug("%s: xaddr for media_service: %s", self.namename, media_service.xaddr)
409  try:
410  result = await media_service.GetProfiles()
411  except GET_CAPABILITIES_EXCEPTIONS:
412  LOGGER.debug(
413  "%s: Could not get profiles from ONVIF device", self.namename, exc_info=True
414  )
415  raise
416  profiles: list[Profile] = []
417 
418  if not isinstance(result, list):
419  return profiles
420 
421  for key, onvif_profile in enumerate(result):
422  # Only add H264 profiles
423  if (
424  not onvif_profile.VideoEncoderConfiguration
425  or onvif_profile.VideoEncoderConfiguration.Encoding != "H264"
426  ):
427  continue
428 
429  profile = Profile(
430  key,
431  onvif_profile.token,
432  onvif_profile.Name,
433  Video(
434  onvif_profile.VideoEncoderConfiguration.Encoding,
435  Resolution(
436  onvif_profile.VideoEncoderConfiguration.Resolution.Width,
437  onvif_profile.VideoEncoderConfiguration.Resolution.Height,
438  ),
439  ),
440  )
441 
442  # Configure PTZ options
443  if self.capabilitiescapabilities.ptz and onvif_profile.PTZConfiguration:
444  profile.ptz = PTZ(
445  onvif_profile.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace
446  is not None,
447  onvif_profile.PTZConfiguration.DefaultRelativePanTiltTranslationSpace
448  is not None,
449  onvif_profile.PTZConfiguration.DefaultAbsolutePantTiltPositionSpace
450  is not None,
451  )
452 
453  try:
454  ptz_service = await self.devicedevice.create_ptz_service()
455  presets = await ptz_service.GetPresets(profile.token)
456  profile.ptz.presets = [preset.token for preset in presets if preset]
457  except GET_CAPABILITIES_EXCEPTIONS:
458  # It's OK if Presets aren't supported
459  profile.ptz.presets = []
460 
461  # Configure Imaging options
462  if self.capabilitiescapabilities.imaging and onvif_profile.VideoSourceConfiguration:
463  profile.video_source_token = (
464  onvif_profile.VideoSourceConfiguration.SourceToken
465  )
466 
467  profiles.append(profile)
468 
469  return profiles
470 
471  async def async_get_stream_uri(self, profile: Profile) -> str:
472  """Get the stream URI for a specified profile."""
473  media_service = await self.devicedevice.create_media_service()
474  req = media_service.create_type("GetStreamUri")
475  req.ProfileToken = profile.token
476  req.StreamSetup = {
477  "Stream": "RTP-Unicast",
478  "Transport": {"Protocol": "RTSP"},
479  }
480  result = await media_service.GetStreamUri(req)
481  return result.Uri
482 
483  async def async_perform_ptz(
484  self,
485  profile: Profile,
486  distance,
487  speed,
488  move_mode,
489  continuous_duration,
490  preset,
491  pan=None,
492  tilt=None,
493  zoom=None,
494  ):
495  """Perform a PTZ action on the camera."""
496  if not self.capabilitiescapabilities.ptz:
497  LOGGER.warning("PTZ actions are not supported on device '%s'", self.namename)
498  return
499 
500  ptz_service = await self.devicedevice.create_ptz_service()
501 
502  pan_val = distance * PAN_FACTOR.get(pan, 0)
503  tilt_val = distance * TILT_FACTOR.get(tilt, 0)
504  zoom_val = distance * ZOOM_FACTOR.get(zoom, 0)
505  speed_val = speed
506  preset_val = preset
507  LOGGER.debug(
508  (
509  "Calling %s PTZ | Pan = %4.2f | Tilt = %4.2f | Zoom = %4.2f | Speed ="
510  " %4.2f | Preset = %s"
511  ),
512  move_mode,
513  pan_val,
514  tilt_val,
515  zoom_val,
516  speed_val,
517  preset_val,
518  )
519  try:
520  req = ptz_service.create_type(move_mode)
521  req.ProfileToken = profile.token
522  if move_mode == CONTINUOUS_MOVE:
523  # Guard against unsupported operation
524  if not profile.ptz or not profile.ptz.continuous:
525  LOGGER.warning(
526  "ContinuousMove not supported on device '%s'", self.namename
527  )
528  return
529 
530  velocity = {}
531  if pan is not None or tilt is not None:
532  velocity["PanTilt"] = {"x": pan_val, "y": tilt_val}
533  if zoom is not None:
534  velocity["Zoom"] = {"x": zoom_val}
535 
536  req.Velocity = velocity
537 
538  await ptz_service.ContinuousMove(req)
539  await asyncio.sleep(continuous_duration)
540  req = ptz_service.create_type("Stop")
541  req.ProfileToken = profile.token
542  await ptz_service.Stop(
543  {"ProfileToken": req.ProfileToken, "PanTilt": True, "Zoom": False}
544  )
545  elif move_mode == RELATIVE_MOVE:
546  # Guard against unsupported operation
547  if not profile.ptz or not profile.ptz.relative:
548  LOGGER.warning(
549  "RelativeMove not supported on device '%s'", self.namename
550  )
551  return
552 
553  req.Translation = {
554  "PanTilt": {"x": pan_val, "y": tilt_val},
555  "Zoom": {"x": zoom_val},
556  }
557  req.Speed = {
558  "PanTilt": {"x": speed_val, "y": speed_val},
559  "Zoom": {"x": speed_val},
560  }
561  await ptz_service.RelativeMove(req)
562  elif move_mode == ABSOLUTE_MOVE:
563  # Guard against unsupported operation
564  if not profile.ptz or not profile.ptz.absolute:
565  LOGGER.warning(
566  "AbsoluteMove not supported on device '%s'", self.namename
567  )
568  return
569 
570  req.Position = {
571  "PanTilt": {"x": pan_val, "y": tilt_val},
572  "Zoom": {"x": zoom_val},
573  }
574  req.Speed = {
575  "PanTilt": {"x": speed_val, "y": speed_val},
576  "Zoom": {"x": speed_val},
577  }
578  await ptz_service.AbsoluteMove(req)
579  elif move_mode == GOTOPRESET_MOVE:
580  # Guard against unsupported operation
581  if not profile.ptz or not profile.ptz.presets:
582  LOGGER.warning(
583  "Absolute Presets not supported on device '%s'", self.namename
584  )
585  return
586  if preset_val not in profile.ptz.presets:
587  LOGGER.warning(
588  (
589  "PTZ preset '%s' does not exist on device '%s'. Available"
590  " Presets: %s"
591  ),
592  preset_val,
593  self.namename,
594  ", ".join(profile.ptz.presets),
595  )
596  return
597 
598  req.PresetToken = preset_val
599  req.Speed = {
600  "PanTilt": {"x": speed_val, "y": speed_val},
601  "Zoom": {"x": speed_val},
602  }
603  await ptz_service.GotoPreset(req)
604  elif move_mode == STOP_MOVE:
605  await ptz_service.Stop(req)
606  except ONVIFError as err:
607  if "Bad Request" in err.reason:
608  LOGGER.warning("Device '%s' doesn't support PTZ", self.namename)
609  else:
610  LOGGER.error("Error trying to perform PTZ action: %s", err)
611 
613  self,
614  profile: Profile,
615  cmd: str,
616  ) -> None:
617  """Execute a PTZ auxiliary command on the camera."""
618  if not self.capabilitiescapabilities.ptz:
619  LOGGER.warning("PTZ actions are not supported on device '%s'", self.namename)
620  return
621 
622  ptz_service = await self.devicedevice.create_ptz_service()
623 
624  LOGGER.debug(
625  "Running Aux Command | Cmd = %s",
626  cmd,
627  )
628  try:
629  req = ptz_service.create_type("SendAuxiliaryCommand")
630  req.ProfileToken = profile.token
631  req.AuxiliaryData = cmd
632  await ptz_service.SendAuxiliaryCommand(req)
633  except ONVIFError as err:
634  if "Bad Request" in err.reason:
635  LOGGER.warning("Device '%s' doesn't support PTZ", self.namename)
636  else:
637  LOGGER.error("Error trying to send PTZ auxiliary command: %s", err)
638 
640  self,
641  profile: Profile,
642  settings: dict,
643  ) -> None:
644  """Set an imaging setting on the ONVIF imaging service."""
645  # The Imaging Service is defined by ONVIF standard
646  # https://www.onvif.org/specs/srv/img/ONVIF-Imaging-Service-Spec-v210.pdf
647  if not self.capabilitiescapabilities.imaging:
648  LOGGER.warning(
649  "The imaging service is not supported on device '%s'", self.namename
650  )
651  return
652 
653  imaging_service = await self.devicedevice.create_imaging_service()
654 
655  LOGGER.debug("Setting Imaging Setting | Settings = %s", settings)
656  try:
657  req = imaging_service.create_type("SetImagingSettings")
658  req.VideoSourceToken = profile.video_source_token
659  req.ImagingSettings = settings
660  await imaging_service.SetImagingSettings(req)
661  except ONVIFError as err:
662  if "Bad Request" in err.reason:
663  LOGGER.warning(
664  "Device '%s' doesn't support the Imaging Service", self.namename
665  )
666  else:
667  LOGGER.error("Error trying to set Imaging settings: %s", err)
668 
669 
671  hass: HomeAssistant,
672  host: str,
673  port: int,
674  username: str | None,
675  password: str | None,
676 ) -> ONVIFCamera:
677  """Get ONVIFCamera instance."""
678  return ONVIFCamera(
679  host,
680  port,
681  username,
682  password,
683  f"{os.path.dirname(onvif.__file__)}/wsdl/",
684  no_cache=True,
685  )
None async_set_imaging_settings(self, Profile profile, dict settings)
Definition: device.py:643
None __init__(self, HomeAssistant hass, ConfigEntry config_entry)
Definition: device.py:54
str async_get_stream_uri(self, Profile profile)
Definition: device.py:471
None _async_log_time_out_of_sync(self, dt.datetime cam_date_utc, dt.datetime system_date)
Definition: device.py:307
def async_perform_ptz(self, Profile profile, distance, speed, move_mode, continuous_duration, preset, pan=None, tilt=None, zoom=None)
Definition: device.py:494
None async_run_aux_command(self, Profile profile, str cmd)
Definition: device.py:616
None _async_update_listener(self, HomeAssistant hass, ConfigEntry entry)
Definition: device.py:72
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_start(HomeAssistant hass, str discovery_topic, ConfigEntry config_entry)
Definition: discovery.py:356
ONVIFCamera get_device(HomeAssistant hass, str host, int port, str|None username, str|None password)
Definition: device.py:676
Sequence[str]|None get_capabilities(Sequence[str] capabilities)