Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow to configure the FRITZ!Box Tools integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 import ipaddress
7 import logging
8 import socket
9 from typing import Any, Self
10 from urllib.parse import ParseResult, urlparse
11 
12 from fritzconnection import FritzConnection
13 from fritzconnection.core.exceptions import FritzConnectionException
14 import voluptuous as vol
15 
16 from homeassistant.components import ssdp
18  CONF_CONSIDER_HOME,
19  DEFAULT_CONSIDER_HOME,
20 )
21 from homeassistant.config_entries import (
22  ConfigEntry,
23  ConfigFlow,
24  ConfigFlowResult,
25  OptionsFlow,
26 )
27 from homeassistant.const import (
28  CONF_HOST,
29  CONF_PASSWORD,
30  CONF_PORT,
31  CONF_SSL,
32  CONF_USERNAME,
33 )
34 from homeassistant.core import callback
35 from homeassistant.helpers.typing import VolDictType
36 
37 from .const import (
38  CONF_OLD_DISCOVERY,
39  DEFAULT_CONF_OLD_DISCOVERY,
40  DEFAULT_HOST,
41  DEFAULT_HTTP_PORT,
42  DEFAULT_HTTPS_PORT,
43  DEFAULT_SSL,
44  DOMAIN,
45  ERROR_AUTH_INVALID,
46  ERROR_CANNOT_CONNECT,
47  ERROR_UNKNOWN,
48  ERROR_UPNP_NOT_CONFIGURED,
49  FRITZ_AUTH_EXCEPTIONS,
50 )
51 
52 _LOGGER = logging.getLogger(__name__)
53 
54 
55 class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
56  """Handle a FRITZ!Box Tools config flow."""
57 
58  VERSION = 1
59 
60  _host: str
61 
62  @staticmethod
63  @callback
65  config_entry: ConfigEntry,
66  ) -> FritzBoxToolsOptionsFlowHandler:
67  """Get the options flow for this handler."""
69 
70  def __init__(self) -> None:
71  """Initialize FRITZ!Box Tools flow."""
72  self._name_name: str = ""
73  self._password_password: str = ""
74  self._use_tls_use_tls: bool = False
75  self._port_port: int | None = None
76  self._username_username: str = ""
77  self._model_model: str = ""
78 
79  async def async_fritz_tools_init(self) -> str | None:
80  """Initialize FRITZ!Box Tools class."""
81  return await self.hass.async_add_executor_job(self.fritz_tools_initfritz_tools_init)
82 
83  def fritz_tools_init(self) -> str | None:
84  """Initialize FRITZ!Box Tools class."""
85 
86  try:
87  connection = FritzConnection(
88  address=self._host_host,
89  port=self._port_port,
90  user=self._username_username,
91  password=self._password_password,
92  use_tls=self._use_tls_use_tls,
93  timeout=60.0,
94  pool_maxsize=30,
95  )
96  except FRITZ_AUTH_EXCEPTIONS:
97  return ERROR_AUTH_INVALID
98  except FritzConnectionException:
99  return ERROR_CANNOT_CONNECT
100  except Exception:
101  _LOGGER.exception("Unexpected exception")
102  return ERROR_UNKNOWN
103 
104  self._model_model = connection.call_action("DeviceInfo:1", "GetInfo")["NewModelName"]
105 
106  if (
107  "X_AVM-DE_UPnP1" in connection.services
108  and not connection.call_action("X_AVM-DE_UPnP1", "GetInfo")["NewEnable"]
109  ):
110  return ERROR_UPNP_NOT_CONFIGURED
111 
112  return None
113 
114  async def async_check_configured_entry(self) -> ConfigEntry | None:
115  """Check if entry is configured."""
116  current_host = await self.hass.async_add_executor_job(
117  socket.gethostbyname, self._host_host
118  )
119 
120  for entry in self._async_current_entries_async_current_entries(include_ignore=False):
121  entry_host = await self.hass.async_add_executor_job(
122  socket.gethostbyname, entry.data[CONF_HOST]
123  )
124  if entry_host == current_host:
125  return entry
126  return None
127 
128  @callback
129  def _async_create_entry(self) -> ConfigFlowResult:
130  """Async create flow handler entry."""
131  return self.async_create_entryasync_create_entryasync_create_entry(
132  title=self._name_name,
133  data={
134  CONF_HOST: self._host_host,
135  CONF_PASSWORD: self._password_password,
136  CONF_PORT: self._port_port,
137  CONF_USERNAME: self._username_username,
138  CONF_SSL: self._use_tls_use_tls,
139  },
140  options={
141  CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(),
142  CONF_OLD_DISCOVERY: DEFAULT_CONF_OLD_DISCOVERY,
143  },
144  )
145 
146  def _determine_port(self, user_input: dict[str, Any]) -> int:
147  """Determine port from user_input."""
148  if port := user_input.get(CONF_PORT):
149  return int(port)
150  return DEFAULT_HTTPS_PORT if user_input[CONF_SSL] else DEFAULT_HTTP_PORT
151 
152  async def async_step_ssdp(
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:
159  return self.async_abortasync_abortasync_abort(reason="ignore_ip6_link_local")
160 
161  self._host_host = host
162  self._name_name = (
163  discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME)
164  or discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME]
165  )
166 
167  uuid: str | None
168  if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN):
169  if uuid.startswith("uuid:"):
170  uuid = uuid[5:]
171  await self.async_set_unique_idasync_set_unique_id(uuid)
172  self._abort_if_unique_id_configured_abort_if_unique_id_configured({CONF_HOST: self._host_host})
173 
174  if self.hass.config_entries.flow.async_has_matching_flow(self):
175  return self.async_abortasync_abortasync_abort(reason="already_in_progress")
176 
177  if entry := await self.async_check_configured_entryasync_check_configured_entry():
178  if uuid and not entry.unique_id:
179  self.hass.config_entries.async_update_entry(entry, unique_id=uuid)
180  return self.async_abortasync_abortasync_abort(reason="already_configured")
181 
182  self.context.update(
183  {
184  "title_placeholders": {"name": self._name_name.replace("FRITZ!Box ", "")},
185  "configuration_url": f"http://{self._host}",
186  }
187  )
188 
189  return await self.async_step_confirmasync_step_confirm()
190 
191  def is_matching(self, other_flow: Self) -> bool:
192  """Return True if other_flow is matching this flow."""
193  return other_flow._host == self._host_host # noqa: SLF001
194 
196  self, user_input: dict[str, Any] | None = None
197  ) -> ConfigFlowResult:
198  """Handle user-confirmation of discovered node."""
199  if user_input is None:
200  return self._show_setup_form_confirm_show_setup_form_confirm()
201 
202  errors = {}
203 
204  self._username_username = user_input[CONF_USERNAME]
205  self._password_password = user_input[CONF_PASSWORD]
206  self._use_tls_use_tls = user_input[CONF_SSL]
207  self._port_port = self._determine_port_determine_port(user_input)
208 
209  error = await self.async_fritz_tools_initasync_fritz_tools_init()
210 
211  if error:
212  errors["base"] = error
213  return self._show_setup_form_confirm_show_setup_form_confirm(errors)
214 
215  return self._async_create_entry_async_create_entry()
216 
218  self, errors: dict[str, str] | None = None
219  ) -> ConfigFlowResult:
220  """Show the setup form to the user."""
221 
222  advanced_data_schema: VolDictType = {}
223  if self.show_advanced_optionsshow_advanced_options:
224  advanced_data_schema = {
225  vol.Optional(CONF_PORT): vol.Coerce(int),
226  }
227 
228  return self.async_show_formasync_show_formasync_show_form(
229  step_id="user",
230  data_schema=vol.Schema(
231  {
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,
237  }
238  ),
239  errors=errors or {},
240  )
241 
243  self, errors: dict[str, str] | None = None
244  ) -> ConfigFlowResult:
245  """Show the setup form to the user."""
246  return self.async_show_formasync_show_formasync_show_form(
247  step_id="confirm",
248  data_schema=vol.Schema(
249  {
250  vol.Required(CONF_USERNAME): str,
251  vol.Required(CONF_PASSWORD): str,
252  vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
253  }
254  ),
255  description_placeholders={"name": self._name_name},
256  errors=errors or {},
257  )
258 
259  async def async_step_user(
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:
264  return self._show_setup_form_init_show_setup_form_init()
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]
269 
270  self._port_port = self._determine_port_determine_port(user_input)
271 
272  if not (error := await self.async_fritz_tools_initasync_fritz_tools_init()):
273  self._name_name = self._model_model
274 
275  if await self.async_check_configured_entryasync_check_configured_entry():
276  error = "already_configured"
277 
278  if error:
279  return self._show_setup_form_init_show_setup_form_init({"base": error})
280 
281  return self._async_create_entry_async_create_entry()
282 
283  async def async_step_reauth(
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]
292 
293  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
294 
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)
300  return self.async_show_formasync_show_formasync_show_form(
301  step_id="reauth_confirm",
302  data_schema=vol.Schema(
303  {
304  vol.Required(CONF_USERNAME, default=default_username): str,
305  vol.Required(CONF_PASSWORD): str,
306  }
307  ),
308  description_placeholders={"host": self._host_host},
309  errors=errors or {},
310  )
311 
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:
317  return self._show_setup_form_reauth_confirm_show_setup_form_reauth_confirm(
318  user_input={CONF_USERNAME: self._username_username}
319  )
320 
321  self._username_username = user_input[CONF_USERNAME]
322  self._password_password = user_input[CONF_PASSWORD]
323 
324  if error := await self.async_fritz_tools_initasync_fritz_tools_init():
325  return self._show_setup_form_reauth_confirm_show_setup_form_reauth_confirm(
326  user_input=user_input, errors={"base": error}
327  )
328 
329  return self.async_update_reload_and_abortasync_update_reload_and_abort(
330  self._get_reauth_entry_get_reauth_entry(),
331  data={
332  CONF_HOST: self._host_host,
333  CONF_PASSWORD: self._password_password,
334  CONF_PORT: self._port_port,
335  CONF_USERNAME: self._username_username,
336  CONF_SSL: self._use_tls_use_tls,
337  },
338  )
339 
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 = {}
345  if self.show_advanced_optionsshow_advanced_options:
346  advanced_data_schema = {
347  vol.Optional(CONF_PORT, default=user_input[CONF_PORT]): vol.Coerce(int),
348  }
349 
350  return self.async_show_formasync_show_formasync_show_form(
351  step_id="reconfigure",
352  data_schema=vol.Schema(
353  {
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,
357  }
358  ),
359  description_placeholders={"host": user_input[CONF_HOST]},
360  errors=errors or {},
361  )
362 
364  self, user_input: dict[str, Any] | None = None
365  ) -> ConfigFlowResult:
366  """Handle reconfigure flow."""
367  if user_input is None:
368  reconfigure_entry_data = self._get_reconfigure_entry_get_reconfigure_entry().data
369  return self._show_setup_form_reconfigure_show_setup_form_reconfigure(
370  {
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),
374  }
375  )
376 
377  self._host_host = user_input[CONF_HOST]
378  self._use_tls_use_tls = user_input[CONF_SSL]
379  self._port_port = self._determine_port_determine_port(user_input)
380 
381  reconfigure_entry = self._get_reconfigure_entry_get_reconfigure_entry()
382  self._username_username = reconfigure_entry.data[CONF_USERNAME]
383  self._password_password = reconfigure_entry.data[CONF_PASSWORD]
384  if error := await self.async_fritz_tools_initasync_fritz_tools_init():
385  return self._show_setup_form_reconfigure_show_setup_form_reconfigure(
386  user_input={**user_input, CONF_PORT: self._port_port}, errors={"base": error}
387  )
388 
389  return self.async_update_reload_and_abortasync_update_reload_and_abort(
390  reconfigure_entry,
391  data_updates={
392  CONF_HOST: self._host_host,
393  CONF_PORT: self._port_port,
394  CONF_SSL: self._use_tls_use_tls,
395  },
396  )
397 
398 
400  """Handle an options flow."""
401 
402  async def async_step_init(
403  self, user_input: dict[str, Any] | None = None
404  ) -> ConfigFlowResult:
405  """Handle options flow."""
406 
407  if user_input is not None:
408  return self.async_create_entryasync_create_entry(title="", data=user_input)
409 
410  options = self.config_entryconfig_entryconfig_entry.options
411  data_schema = vol.Schema(
412  {
413  vol.Optional(
414  CONF_CONSIDER_HOME,
415  default=options.get(
416  CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()
417  ),
418  ): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900)),
419  vol.Optional(
420  CONF_OLD_DISCOVERY,
421  default=options.get(CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY),
422  ): bool,
423  }
424  )
425  return self.async_show_formasync_show_form(step_id="init", data_schema=data_schema)
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:285
ConfigFlowResult async_step_reconfigure(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:365
ConfigFlowResult _show_setup_form_init(self, dict[str, str]|None errors=None)
Definition: config_flow.py:219
ConfigFlowResult _show_setup_form_confirm(self, dict[str, str]|None errors=None)
Definition: config_flow.py:244
ConfigFlowResult async_step_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:197
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:261
ConfigFlowResult _show_setup_form_reconfigure(self, dict[str, Any] user_input, dict[str, str]|None errors=None)
Definition: config_flow.py:342
ConfigFlowResult _show_setup_form_reauth_confirm(self, dict[str, Any] user_input, dict[str, str]|None errors=None)
Definition: config_flow.py:297
FritzBoxToolsOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:66
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
Definition: config_flow.py:154
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:314
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:404
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
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)
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)
Definition: __init__.py:33