Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The ONVIF integration."""
2 
3 import asyncio
4 from contextlib import suppress
5 from http import HTTPStatus
6 import logging
7 
8 from httpx import RequestError
9 from onvif.exceptions import ONVIFError
10 from onvif.util import is_auth_error, stringify_onvif_error
11 from zeep.exceptions import Fault, TransportError
12 
13 from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS
14 from homeassistant.components.stream import CONF_RTSP_TRANSPORT, RTSP_TRANSPORTS
15 from homeassistant.config_entries import ConfigEntry
16 from homeassistant.const import (
17  EVENT_HOMEASSISTANT_STOP,
18  HTTP_BASIC_AUTHENTICATION,
19  HTTP_DIGEST_AUTHENTICATION,
20  Platform,
21 )
22 from homeassistant.core import HomeAssistant
23 from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
24 
25 from .const import (
26  CONF_ENABLE_WEBHOOKS,
27  CONF_SNAPSHOT_AUTH,
28  DEFAULT_ARGUMENTS,
29  DEFAULT_ENABLE_WEBHOOKS,
30  DOMAIN,
31 )
32 from .device import ONVIFDevice
33 
34 LOGGER = logging.getLogger(__name__)
35 
36 
37 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
38  """Set up ONVIF from a config entry."""
39  if DOMAIN not in hass.data:
40  hass.data[DOMAIN] = {}
41 
42  if not entry.options:
43  await async_populate_options(hass, entry)
44 
45  device = ONVIFDevice(hass, entry)
46 
47  try:
48  await device.async_setup()
49  if not entry.data.get(CONF_SNAPSHOT_AUTH):
50  await async_populate_snapshot_auth(hass, device, entry)
51  except RequestError as err:
52  await device.device.close()
53  raise ConfigEntryNotReady(
54  f"Could not connect to camera {device.device.host}:{device.device.port}: {err}"
55  ) from err
56  except Fault as err:
57  await device.device.close()
58  if is_auth_error(err):
60  f"Auth Failed: {stringify_onvif_error(err)}"
61  ) from err
62  raise ConfigEntryNotReady(
63  f"Could not connect to camera: {stringify_onvif_error(err)}"
64  ) from err
65  except ONVIFError as err:
66  await device.device.close()
67  raise ConfigEntryNotReady(
68  f"Could not setup camera {device.device.host}:{device.device.port}: {stringify_onvif_error(err)}"
69  ) from err
70  except TransportError as err:
71  await device.device.close()
72  stringified_onvif_error = stringify_onvif_error(err)
73  if err.status_code in (
74  HTTPStatus.UNAUTHORIZED.value,
75  HTTPStatus.FORBIDDEN.value,
76  ):
78  f"Auth Failed: {stringified_onvif_error}"
79  ) from err
80  raise ConfigEntryNotReady(
81  f"Could not setup camera {device.device.host}:{device.device.port}: {stringified_onvif_error}"
82  ) from err
83  except asyncio.CancelledError as err:
84  # After https://github.com/agronholm/anyio/issues/374 is resolved
85  # this may be able to be removed
86  await device.device.close()
87  raise ConfigEntryNotReady(f"Setup was unexpectedly canceled: {err}") from err
88 
89  if not device.available:
90  raise ConfigEntryNotReady
91 
92  hass.data[DOMAIN][entry.unique_id] = device
93 
94  device.platforms = [Platform.BUTTON, Platform.CAMERA]
95 
96  if device.capabilities.events:
97  device.platforms += [Platform.BINARY_SENSOR, Platform.SENSOR]
98 
99  if device.capabilities.imaging:
100  device.platforms += [Platform.SWITCH]
101 
102  await hass.config_entries.async_forward_entry_setups(entry, device.platforms)
103 
104  entry.async_on_unload(
105  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.async_stop)
106  )
107 
108  return True
109 
110 
111 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
112  """Unload a config entry."""
113 
114  device: ONVIFDevice = hass.data[DOMAIN][entry.unique_id]
115 
116  if device.capabilities.events and device.events.started:
117  try:
118  await device.events.async_stop()
119  except (ONVIFError, Fault, RequestError, TransportError):
120  LOGGER.warning("Error while stopping events: %s", device.name)
121 
122  return await hass.config_entries.async_unload_platforms(entry, device.platforms)
123 
124 
125 async def _get_snapshot_auth(device: ONVIFDevice) -> str | None:
126  """Determine auth type for snapshots."""
127  if not device.capabilities.snapshot:
128  return None
129 
130  for basic_auth in (False, True):
131  method = HTTP_BASIC_AUTHENTICATION if basic_auth else HTTP_DIGEST_AUTHENTICATION
132  with suppress(ONVIFError):
133  if await device.device.get_snapshot(device.profiles[0].token, basic_auth):
134  return method
135 
136  return None
137 
138 
140  hass: HomeAssistant, device: ONVIFDevice, entry: ConfigEntry
141 ) -> None:
142  """Check if digest auth for snapshots is possible."""
143  if auth := await _get_snapshot_auth(device):
144  hass.config_entries.async_update_entry(
145  entry, data={**entry.data, CONF_SNAPSHOT_AUTH: auth}
146  )
147 
148 
149 async def async_populate_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
150  """Populate default options for device."""
151  options = {
152  CONF_EXTRA_ARGUMENTS: DEFAULT_ARGUMENTS,
153  CONF_RTSP_TRANSPORT: next(iter(RTSP_TRANSPORTS)),
154  CONF_ENABLE_WEBHOOKS: DEFAULT_ENABLE_WEBHOOKS,
155  }
156 
157  hass.config_entries.async_update_entry(entry, options=options)
str stringify_onvif_error(Exception error)
Definition: util.py:17
bool is_auth_error(Exception error)
Definition: util.py:41
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:37
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:111
None async_populate_options(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:149
None async_populate_snapshot_auth(HomeAssistant hass, ONVIFDevice device, ConfigEntry entry)
Definition: __init__.py:141
str|None _get_snapshot_auth(ONVIFDevice device)
Definition: __init__.py:125