1 """Config Flow to configure UniFi Protect Integration."""
3 from __future__
import annotations
5 from collections.abc
import Mapping
7 from pathlib
import Path
10 from aiohttp
import CookieJar
11 from uiprotect
import ProtectApiClient
12 from uiprotect.data
import NVR
13 from uiprotect.exceptions
import ClientError, NotAuthorized
14 from unifi_discovery
import async_console_is_alive
15 import voluptuous
as vol
36 async_create_clientsession,
37 async_get_clientsession,
54 MIN_REQUIRED_PROTECT_V,
57 from .data
import async_last_update_was_successful
58 from .discovery
import async_start_discovery
59 from .utils
import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass
61 _LOGGER = logging.getLogger(__name__)
63 ENTRY_FAILURE_STATES = (
64 ConfigEntryState.SETUP_ERROR,
65 ConfigEntryState.SETUP_RETRY,
70 """Get the documentation url for creating a local user."""
72 return f
"{integration.documentation}#local-user"
76 """Check if a host is a unifi direct connect domain."""
77 return host.endswith(
".ui.direct")
84 """Check if a console is offline.
86 We define offline by the config entry
87 is in a failure/retry state or the updates
88 are failing and the console is unreachable
89 since protect may be updating.
92 entry.state
in ENTRY_FAILURE_STATES
94 )
and not await async_console_is_alive(
100 """Handle a UniFi Protect config flow."""
105 """Init the config flow."""
110 self, discovery_info: dhcp.DhcpServiceInfo
111 ) -> ConfigFlowResult:
112 """Handle discovery via dhcp."""
113 _LOGGER.debug(
"Starting discovery via: %s", discovery_info)
117 self, discovery_info: ssdp.SsdpServiceInfo
118 ) -> ConfigFlowResult:
119 """Handle a discovered UniFi device."""
120 _LOGGER.debug(
"Starting discovery via: %s", discovery_info)
124 """Ensure discovery is active."""
132 self, discovery_info: DiscoveryInfoType
133 ) -> ConfigFlowResult:
134 """Handle integration discovery."""
138 source_ip = discovery_info[
"source_ip"]
139 direct_connect_domain = discovery_info[
"direct_connect_domain"]
141 if entry.source == SOURCE_IGNORE:
142 if entry.unique_id == mac:
145 entry_host = entry.data[CONF_HOST]
147 if entry.unique_id == mac:
150 entry_has_direct_connect
151 and direct_connect_domain
152 and entry_host != direct_connect_domain
154 new_host = direct_connect_domain
156 not entry_has_direct_connect
158 and entry_host != source_ip
163 self.hass.config_entries.async_update_entry(
164 entry, data={**entry.data, CONF_HOST: new_host}
167 if entry_host
in (direct_connect_domain, source_ip)
or (
168 entry_has_direct_connect
176 self, user_input: dict[str, Any] |
None =
None
177 ) -> ConfigFlowResult:
178 """Confirm discovery."""
179 errors: dict[str, str] = {}
181 if user_input
is not None:
182 user_input[CONF_PORT] = DEFAULT_PORT
184 if discovery_info[
"direct_connect_domain"]:
185 user_input[CONF_HOST] = discovery_info[
"direct_connect_domain"]
186 user_input[CONF_VERIFY_SSL] =
True
188 if not nvr_data
or errors:
189 user_input[CONF_HOST] = discovery_info[
"source_ip"]
190 user_input[CONF_VERIFY_SSL] =
False
192 if nvr_data
and not errors:
196 "name": discovery_info[
"hostname"]
197 or discovery_info[
"platform"]
198 or f
"NVR {_async_short_mac(discovery_info['hw_addr'])}",
199 "ip_address": discovery_info[
"source_ip"],
201 self.context[
"title_placeholders"] = placeholders
202 user_input = user_input
or {}
204 step_id=
"discovery_confirm",
205 description_placeholders={
211 data_schema=vol.Schema(
214 CONF_USERNAME, default=user_input.get(CONF_USERNAME)
216 vol.Required(CONF_PASSWORD): str,
225 config_entry: ConfigEntry,
227 """Get the options flow for this handler."""
234 data={**data, CONF_ID: title},
236 CONF_DISABLE_RTSP:
False,
237 CONF_ALL_UPDATES:
False,
238 CONF_OVERRIDE_CHOST:
False,
239 CONF_MAX_MEDIA: DEFAULT_MAX_MEDIA,
240 CONF_ALLOW_EA:
False,
246 user_input: dict[str, Any],
247 ) -> tuple[NVR |
None, dict[str, str]]:
249 self.hass, cookie_jar=CookieJar(unsafe=
True)
252 host = user_input[CONF_HOST]
253 port = user_input.get(CONF_PORT, DEFAULT_PORT)
254 verify_ssl = user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL)
256 protect = ProtectApiClient(
260 username=user_input[CONF_USERNAME],
261 password=user_input[CONF_PASSWORD],
262 verify_ssl=verify_ssl,
263 cache_dir=Path(self.hass.config.path(STORAGE_DIR,
"unifiprotect")),
264 config_dir=Path(self.hass.config.path(STORAGE_DIR,
"unifiprotect")),
270 bootstrap = await protect.get_bootstrap()
271 nvr_data = bootstrap.nvr
272 except NotAuthorized
as ex:
274 errors[CONF_PASSWORD] =
"invalid_auth"
275 except ClientError
as ex:
277 errors[
"base"] =
"cannot_connect"
279 if nvr_data.version < MIN_REQUIRED_PROTECT_V:
281 OUTDATED_LOG_MESSAGE,
283 MIN_REQUIRED_PROTECT_V,
285 errors[
"base"] =
"protect_version"
287 auth_user = bootstrap.users.get(bootstrap.auth_user_id)
288 if auth_user
and auth_user.cloud_account:
289 errors[
"base"] =
"cloud_user"
291 return nvr_data, errors
294 self, entry_data: Mapping[str, Any]
295 ) -> ConfigFlowResult:
296 """Perform reauth upon an API authentication error."""
300 self, user_input: dict[str, Any] |
None =
None
301 ) -> ConfigFlowResult:
302 """Confirm reauth."""
303 errors: dict[str, str] = {}
307 form_data = {**reauth_entry.data}
308 if user_input
is not None:
309 form_data.update(user_input)
316 self.context[
"title_placeholders"] = {
317 "name": reauth_entry.title,
318 "ip_address": reauth_entry.data[CONF_HOST],
321 step_id=
"reauth_confirm",
322 data_schema=vol.Schema(
325 CONF_USERNAME, default=form_data.get(CONF_USERNAME)
327 vol.Required(CONF_PASSWORD): str,
334 self, user_input: dict[str, Any] |
None =
None
335 ) -> ConfigFlowResult:
336 """Handle a flow initiated by the user."""
338 errors: dict[str, str] = {}
339 if user_input
is not None:
342 if nvr_data
and not errors:
348 user_input = user_input
or {}
351 description_placeholders={
356 data_schema=vol.Schema(
358 vol.Required(CONF_HOST, default=user_input.get(CONF_HOST)): str,
360 CONF_PORT, default=user_input.get(CONF_PORT, DEFAULT_PORT)
364 default=user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL),
367 CONF_USERNAME, default=user_input.get(CONF_USERNAME)
369 vol.Required(CONF_PASSWORD): str,
377 """Handle options."""
380 self, user_input: dict[str, Any] |
None =
None
381 ) -> ConfigFlowResult:
382 """Manage the options."""
383 if user_input
is not None:
388 data_schema=vol.Schema(
401 CONF_OVERRIDE_CHOST,
False
407 CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA
409 ): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)),
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_integration_discovery(self, DiscoveryInfoType discovery_info)
ConfigFlowResult async_step_dhcp(self, dhcp.DhcpServiceInfo discovery_info)
ConfigFlowResult _async_discovery_handoff(self)
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
OptionsFlow async_get_options_flow(ConfigEntry config_entry)
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_discovery_confirm(self, dict[str, Any]|None user_input=None)
tuple[NVR|None, dict[str, str]] _async_get_nvr_data(self, dict[str, Any] user_input)
ConfigFlowResult _async_create_entry(self, str title, dict[str, Any] data)
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)
_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)
bool async_last_update_was_successful(HomeAssistant hass, PowerwallConfigEntry entry)
aiohttp.ClientSession async_create_clientsession()
bool _async_console_is_offline(HomeAssistant hass, ConfigEntry entry)
bool _host_is_direct_connect(str host)
str async_local_user_documentation_url(HomeAssistant hass)
None async_start_discovery(HomeAssistant hass)
str _async_unifi_mac_from_hass(str mac)
str|None _async_resolve(HomeAssistant hass, str host)
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
Integration async_get_integration(HomeAssistant hass, str domain)
bool is_ip_address(str address)