1 """Config flow for ONVIF."""
3 from __future__
import annotations
5 from collections.abc
import Mapping
7 from pprint
import pformat
9 from urllib.parse
import urlparse
11 from onvif.util
import is_auth_error, stringify_onvif_error
12 import voluptuous
as vol
13 from wsdiscovery.discovery
import ThreadedWSDiscovery
as WSDiscovery
14 from wsdiscovery.scope
import Scope
15 from wsdiscovery.service
import Service
16 from zeep.exceptions
import Fault
22 CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
48 DEFAULT_ENABLE_WEBHOOKS,
51 GET_CAPABILITIES_EXCEPTIONS,
54 from .device
import get_device
56 CONF_MANUAL_INPUT =
"Manually configure ONVIF device"
60 """Get ONVIF Profile S devices from network."""
61 discovery = WSDiscovery(ttl=4)
64 return discovery.searchServices(
65 scopes=[Scope(
"onvif://www.onvif.org/Profile/Streaming")]
70 discovery._stopThreads()
74 """Return if there are devices that can be discovered."""
75 LOGGER.debug(
"Starting ONVIF discovery")
76 services = await hass.async_add_executor_job(wsdiscovery)
79 for service
in services:
80 url = urlparse(service.getXAddrs()[0])
83 CONF_NAME: service.getEPR(),
84 CONF_HOST: url.hostname,
85 CONF_PORT: url.port
or 80,
88 for scope
in service.getScopes():
89 scope_str = scope.getValue()
90 if scope_str.lower().startswith(
"onvif://www.onvif.org/name"):
91 device[CONF_NAME] = scope_str.split(
"/")[-1]
92 if scope_str.lower().startswith(
"onvif://www.onvif.org/hardware"):
93 device[CONF_HARDWARE] = scope_str.split(
"/")[-1]
94 if scope_str.lower().startswith(
"onvif://www.onvif.org/mac"):
95 device[CONF_DEVICE_ID] = scope_str.split(
"/")[-1]
96 devices.append(device)
102 """Handle a ONVIF config flow."""
109 config_entry: ConfigEntry,
110 ) -> OnvifOptionsFlowHandler:
111 """Get the options flow for this handler."""
115 """Initialize the ONVIF config flow."""
117 self.devices: list[dict[str, Any]] = []
121 self, user_input: dict[str, Any] |
None =
None
122 ) -> ConfigFlowResult:
123 """Handle user flow."""
125 if user_input[
"auto"]:
131 data_schema=vol.Schema({vol.Required(
"auto", default=
True): bool}),
135 self, entry_data: Mapping[str, Any]
136 ) -> ConfigFlowResult:
137 """Handle re-authentication of an existing config entry."""
141 self, user_input: dict[str, Any] |
None =
None
142 ) -> ConfigFlowResult:
143 """Confirm reauth."""
144 errors: dict[str, str] |
None = {}
146 description_placeholders: dict[str, str] |
None =
None
147 if user_input
is not None:
150 configure_unique_id=
False
157 username = (user_input
or {}).
get(CONF_USERNAME)
or reauth_entry.data[
161 step_id=
"reauth_confirm",
162 data_schema=vol.Schema(
164 vol.Required(CONF_USERNAME, default=username): str,
165 vol.Required(CONF_PASSWORD): str,
169 description_placeholders=description_placeholders,
173 self, discovery_info: dhcp.DhcpServiceInfo
174 ) -> ConfigFlowResult:
175 """Handle dhcp discovery."""
177 mac = discovery_info.macaddress
178 registry = dr.async_get(self.hass)
180 device := registry.async_get_device(
181 connections={(dr.CONNECTION_NETWORK_MAC, mac)}
185 for entry_id
in device.config_entries:
187 not (entry := hass.config_entries.async_get_entry(entry_id))
188 or entry.domain != DOMAIN
189 or entry.state
is ConfigEntryState.LOADED
192 if hass.config_entries.async_update_entry(
193 entry, data=entry.data | {CONF_HOST: discovery_info.ip}
195 hass.async_create_task(self.hass.config_entries.async_reload(entry_id))
199 self, user_input: dict[str, str] |
None =
None
200 ) -> ConfigFlowResult:
201 """Handle WS-Discovery.
203 Let user choose between discovered devices and manual configuration.
204 If no device is found allow user to manually input configuration.
207 if user_input[CONF_HOST] == CONF_MANUAL_INPUT:
210 for device
in self.devices:
211 if device[CONF_HOST] == user_input[CONF_HOST]:
212 self.
device_iddevice_id = device[CONF_DEVICE_ID]
214 CONF_NAME: device[CONF_NAME],
215 CONF_HOST: device[CONF_HOST],
216 CONF_PORT: device[CONF_PORT],
221 for device
in discovery:
223 entry.unique_id == device[CONF_DEVICE_ID]
228 self.devices.append(device)
230 if LOGGER.isEnabledFor(logging.DEBUG):
231 LOGGER.debug(
"Discovered ONVIF devices %s", pformat(self.devices))
234 devices = {CONF_MANUAL_INPUT: CONF_MANUAL_INPUT}
235 for device
in self.devices:
236 description = f
"{device[CONF_NAME]} ({device[CONF_HOST]})"
237 if hardware := device[CONF_HARDWARE]:
238 description += f
" [{hardware}]"
239 devices[device[CONF_HOST]] = description
243 data_schema=vol.Schema({vol.Optional(CONF_HOST): vol.In(devices)}),
249 self, user_input: dict[str, Any] |
None =
None
250 ) -> ConfigFlowResult:
251 """Device configuration."""
252 errors: dict[str, str] = {}
253 description_placeholders: dict[str, str] = {}
258 title = f
"{self.onvif_config[CONF_NAME]} - {self.device_id}"
261 def conf(name, default=None):
270 data_schema=vol.Schema(
272 vol.Required(CONF_NAME, default=conf(CONF_NAME)): str,
273 vol.Required(CONF_HOST, default=conf(CONF_HOST)): str,
274 vol.Required(CONF_PORT, default=conf(CONF_PORT, DEFAULT_PORT)): int,
275 vol.Optional(CONF_USERNAME, default=conf(CONF_USERNAME,
"")): str,
276 vol.Optional(CONF_PASSWORD, default=conf(CONF_PASSWORD,
"")): str,
280 description_placeholders=description_placeholders,
284 self, configure_unique_id: bool =
True
285 ) -> tuple[dict[str, str], dict[str, str]]:
286 """Fetch ONVIF device profiles."""
287 if LOGGER.isEnabledFor(logging.DEBUG):
289 "Fetching profiles from ONVIF device %s", pformat(self.
onvif_configonvif_config)
301 await device.update_xaddrs()
302 device_mgmt = await device.create_devicemgmt_service()
306 network_interfaces = await device_mgmt.GetNetworkInterfaces()
308 filter(
lambda interface: interface.Enabled, network_interfaces),
312 self.
device_iddevice_id = interface.Info.HwAddress
313 except Fault
as fault:
314 if "not implemented" not in fault.message:
317 "%s: Could not get network interfaces: %s",
323 device_info = await device_mgmt.GetDeviceInformation()
324 self.
device_iddevice_id = device_info.SerialNumber
329 if configure_unique_id:
336 CONF_USERNAME: self.
onvif_configonvif_config[CONF_USERNAME],
337 CONF_PASSWORD: self.
onvif_configonvif_config[CONF_PASSWORD],
341 media_service = await device.create_media_service()
342 profiles = await media_service.GetProfiles()
343 except AttributeError:
345 "%s: No ONVIF service found at %s:%s",
351 return {CONF_PORT:
"no_onvif_service"}, {}
354 description_placeholders = {
"error": stringified_error}
357 "%s: Could not authenticate with camera: %s",
361 return {CONF_PASSWORD:
"auth_failed"}, description_placeholders
363 "%s: Could not determine camera capabilities: %s",
368 return {
"base":
"onvif_error"}, description_placeholders
369 except GET_CAPABILITIES_EXCEPTIONS
as err:
371 "%s: Could not determine camera capabilities: %s",
379 profile.VideoEncoderConfiguration
380 and profile.VideoEncoderConfiguration.Encoding ==
"H264"
381 for profile
in profiles
390 """Handle ONVIF options."""
392 def __init__(self, config_entry: ConfigEntry) ->
None:
393 """Initialize ONVIF options flow."""
397 """Manage the ONVIF options."""
401 self, user_input: dict[str, Any] |
None =
None
402 ) -> ConfigFlowResult:
403 """Manage the ONVIF devices options."""
404 if user_input
is not None:
405 self.
optionsoptions[CONF_EXTRA_ARGUMENTS] = user_input[CONF_EXTRA_ARGUMENTS]
406 self.
optionsoptions[CONF_RTSP_TRANSPORT] = user_input[CONF_RTSP_TRANSPORT]
407 self.
optionsoptions[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = user_input.get(
408 CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
411 self.
optionsoptions[CONF_ENABLE_WEBHOOKS] = user_input.get(
412 CONF_ENABLE_WEBHOOKS,
414 CONF_ENABLE_WEBHOOKS, DEFAULT_ENABLE_WEBHOOKS
419 advanced_options = {}
423 CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
425 CONF_USE_WALLCLOCK_AS_TIMESTAMPS,
False
430 step_id=
"onvif_devices",
431 data_schema=vol.Schema(
434 CONF_EXTRA_ARGUMENTS,
436 CONF_EXTRA_ARGUMENTS, DEFAULT_ARGUMENTS
442 CONF_RTSP_TRANSPORT, next(iter(RTSP_TRANSPORTS))
444 ): vol.In(RTSP_TRANSPORTS),
446 CONF_ENABLE_WEBHOOKS,
448 CONF_ENABLE_WEBHOOKS, DEFAULT_ENABLE_WEBHOOKS
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
ConfigFlowResult async_step_device(self, dict[str, str]|None user_input=None)
ConfigFlowResult async_step_configure(self, dict[str, Any]|None user_input=None)
tuple[dict[str, str], dict[str, str]] async_setup_profiles(self, bool configure_unique_id=True)
ConfigFlowResult async_step_dhcp(self, dhcp.DhcpServiceInfo discovery_info)
OnvifOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
None __init__(self, ConfigEntry config_entry)
ConfigFlowResult async_step_onvif_devices(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_init(self, None user_input=None)
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
ConfigEntry _get_reauth_entry(self)
ConfigEntry|None async_set_unique_id(self, str|None unique_id=None, *bool raise_on_progress=True)
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=None)
ConfigFlowResult async_update_reload_and_abort(self, ConfigEntry entry, *str|None|UndefinedType unique_id=UNDEFINED, str|UndefinedType title=UNDEFINED, Mapping[str, Any]|UndefinedType data=UNDEFINED, Mapping[str, Any]|UndefinedType data_updates=UNDEFINED, Mapping[str, Any]|UndefinedType options=UNDEFINED, str|UndefinedType reason=UNDEFINED, bool reload_even_if_entry_is_unchanged=True)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
ConfigFlowResult async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
ConfigEntry config_entry(self)
None config_entry(self, ConfigEntry value)
bool show_advanced_options(self)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
DeviceEntry get_device(HomeAssistant hass, str unique_id)
web.Response get(self, web.Request request, str config_key)
list[dict[str, Any]] async_discovery(HomeAssistant hass)
list[Service] wsdiscovery()
str stringify_onvif_error(Exception error)
bool is_auth_error(Exception error)