1 """Config flow to configure the FRITZ!Box Tools integration."""
3 from __future__
import annotations
5 from collections.abc
import Mapping
9 from typing
import Any, Self
10 from urllib.parse
import ParseResult, urlparse
12 from fritzconnection
import FritzConnection
13 from fritzconnection.core.exceptions
import FritzConnectionException
14 import voluptuous
as vol
19 DEFAULT_CONSIDER_HOME,
39 DEFAULT_CONF_OLD_DISCOVERY,
48 ERROR_UPNP_NOT_CONFIGURED,
49 FRITZ_AUTH_EXCEPTIONS,
52 _LOGGER = logging.getLogger(__name__)
56 """Handle a FRITZ!Box Tools config flow."""
65 config_entry: ConfigEntry,
66 ) -> FritzBoxToolsOptionsFlowHandler:
67 """Get the options flow for this handler."""
71 """Initialize FRITZ!Box Tools flow."""
72 self.
_name_name: str =
""
75 self.
_port_port: int |
None =
None
77 self.
_model_model: str =
""
80 """Initialize FRITZ!Box Tools class."""
81 return await self.hass.async_add_executor_job(self.
fritz_tools_initfritz_tools_init)
84 """Initialize FRITZ!Box Tools class."""
87 connection = FritzConnection(
88 address=self.
_host_host,
96 except FRITZ_AUTH_EXCEPTIONS:
97 return ERROR_AUTH_INVALID
98 except FritzConnectionException:
99 return ERROR_CANNOT_CONNECT
101 _LOGGER.exception(
"Unexpected exception")
104 self.
_model_model = connection.call_action(
"DeviceInfo:1",
"GetInfo")[
"NewModelName"]
107 "X_AVM-DE_UPnP1" in connection.services
108 and not connection.call_action(
"X_AVM-DE_UPnP1",
"GetInfo")[
"NewEnable"]
110 return ERROR_UPNP_NOT_CONFIGURED
115 """Check if entry is configured."""
116 current_host = await self.hass.async_add_executor_job(
117 socket.gethostbyname, self.
_host_host
121 entry_host = await self.hass.async_add_executor_job(
122 socket.gethostbyname, entry.data[CONF_HOST]
124 if entry_host == current_host:
130 """Async create flow handler entry."""
132 title=self.
_name_name,
134 CONF_HOST: self.
_host_host,
136 CONF_PORT: self.
_port_port,
141 CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(),
142 CONF_OLD_DISCOVERY: DEFAULT_CONF_OLD_DISCOVERY,
147 """Determine port from user_input."""
148 if port := user_input.get(CONF_PORT):
150 return DEFAULT_HTTPS_PORT
if user_input[CONF_SSL]
else DEFAULT_HTTP_PORT
153 self, discovery_info: ssdp.SsdpServiceInfo
154 ) -> ConfigFlowResult:
155 """Handle a flow initialized by discovery."""
156 ssdp_location: ParseResult = urlparse(discovery_info.ssdp_location
or "")
157 host = ssdp_location.hostname
158 if not host
or ipaddress.ip_address(host).is_link_local:
163 discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME)
164 or discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME]
168 if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN):
169 if uuid.startswith(
"uuid:"):
174 if self.hass.config_entries.flow.async_has_matching_flow(self):
178 if uuid
and not entry.unique_id:
179 self.hass.config_entries.async_update_entry(entry, unique_id=uuid)
184 "title_placeholders": {
"name": self.
_name_name.replace(
"FRITZ!Box ",
"")},
185 "configuration_url": f
"http://{self._host}",
192 """Return True if other_flow is matching this flow."""
193 return other_flow._host == self.
_host_host
196 self, user_input: dict[str, Any] |
None =
None
197 ) -> ConfigFlowResult:
198 """Handle user-confirmation of discovered node."""
199 if user_input
is None:
212 errors[
"base"] = error
218 self, errors: dict[str, str] |
None =
None
219 ) -> ConfigFlowResult:
220 """Show the setup form to the user."""
222 advanced_data_schema: VolDictType = {}
224 advanced_data_schema = {
225 vol.Optional(CONF_PORT): vol.Coerce(int),
230 data_schema=vol.Schema(
232 vol.Optional(CONF_HOST, default=DEFAULT_HOST): str,
233 **advanced_data_schema,
234 vol.Required(CONF_USERNAME): str,
235 vol.Required(CONF_PASSWORD): str,
236 vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
243 self, errors: dict[str, str] |
None =
None
244 ) -> ConfigFlowResult:
245 """Show the setup form to the user."""
248 data_schema=vol.Schema(
250 vol.Required(CONF_USERNAME): str,
251 vol.Required(CONF_PASSWORD): str,
252 vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
255 description_placeholders={
"name": self.
_name_name},
260 self, user_input: dict[str, Any] |
None =
None
261 ) -> ConfigFlowResult:
262 """Handle a flow initiated by the user."""
263 if user_input
is None:
265 self.
_host_host = user_input[CONF_HOST]
266 self.
_username_username = user_input[CONF_USERNAME]
267 self.
_password_password = user_input[CONF_PASSWORD]
268 self.
_use_tls_use_tls = user_input[CONF_SSL]
276 error =
"already_configured"
284 self, entry_data: Mapping[str, Any]
285 ) -> ConfigFlowResult:
286 """Handle flow upon an API authentication error."""
287 self.
_host_host = entry_data[CONF_HOST]
288 self.
_port_port = entry_data[CONF_PORT]
289 self.
_username_username = entry_data[CONF_USERNAME]
290 self.
_password_password = entry_data[CONF_PASSWORD]
291 self.
_use_tls_use_tls = entry_data[CONF_SSL]
296 self, user_input: dict[str, Any], errors: dict[str, str] |
None =
None
297 ) -> ConfigFlowResult:
298 """Show the reauth form to the user."""
299 default_username = user_input.get(CONF_USERNAME)
301 step_id=
"reauth_confirm",
302 data_schema=vol.Schema(
304 vol.Required(CONF_USERNAME, default=default_username): str,
305 vol.Required(CONF_PASSWORD): str,
308 description_placeholders={
"host": self.
_host_host},
313 self, user_input: dict[str, Any] |
None =
None
314 ) -> ConfigFlowResult:
315 """Dialog that informs the user that reauth is required."""
316 if user_input
is None:
318 user_input={CONF_USERNAME: self.
_username_username}
321 self.
_username_username = user_input[CONF_USERNAME]
322 self.
_password_password = user_input[CONF_PASSWORD]
326 user_input=user_input, errors={
"base": error}
332 CONF_HOST: self.
_host_host,
334 CONF_PORT: self.
_port_port,
341 self, user_input: dict[str, Any], errors: dict[str, str] |
None =
None
342 ) -> ConfigFlowResult:
343 """Show the reconfigure form to the user."""
344 advanced_data_schema: VolDictType = {}
346 advanced_data_schema = {
347 vol.Optional(CONF_PORT, default=user_input[CONF_PORT]): vol.Coerce(int),
351 step_id=
"reconfigure",
352 data_schema=vol.Schema(
354 vol.Required(CONF_HOST, default=user_input[CONF_HOST]): str,
355 **advanced_data_schema,
356 vol.Required(CONF_SSL, default=user_input[CONF_SSL]): bool,
359 description_placeholders={
"host": user_input[CONF_HOST]},
364 self, user_input: dict[str, Any] |
None =
None
365 ) -> ConfigFlowResult:
366 """Handle reconfigure flow."""
367 if user_input
is None:
371 CONF_HOST: reconfigure_entry_data[CONF_HOST],
372 CONF_PORT: reconfigure_entry_data[CONF_PORT],
373 CONF_SSL: reconfigure_entry_data.get(CONF_SSL, DEFAULT_SSL),
377 self.
_host_host = user_input[CONF_HOST]
378 self.
_use_tls_use_tls = user_input[CONF_SSL]
382 self.
_username_username = reconfigure_entry.data[CONF_USERNAME]
383 self.
_password_password = reconfigure_entry.data[CONF_PASSWORD]
386 user_input={**user_input, CONF_PORT: self.
_port_port}, errors={
"base": error}
392 CONF_HOST: self.
_host_host,
393 CONF_PORT: self.
_port_port,
400 """Handle an options flow."""
403 self, user_input: dict[str, Any] |
None =
None
404 ) -> ConfigFlowResult:
405 """Handle options flow."""
407 if user_input
is not None:
411 data_schema = vol.Schema(
416 CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()
418 ): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900)),
421 default=options.get(CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY),
425 return self.
async_show_formasync_show_form(step_id=
"init", data_schema=data_schema)
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)
ConfigEntry _get_reconfigure_entry(self)
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)
IssData update(pyiss.ISS iss)