Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for AVM FRITZ!SmartHome."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 import ipaddress
7 from typing import Any, Self
8 from urllib.parse import urlparse
9 
10 from pyfritzhome import Fritzhome, LoginError
11 from requests.exceptions import HTTPError
12 import voluptuous as vol
13 
14 from homeassistant.components import ssdp
15 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
16 from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
17 
18 from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN
19 
20 DATA_SCHEMA_USER = vol.Schema(
21  {
22  vol.Required(CONF_HOST, default=DEFAULT_HOST): str,
23  vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str,
24  vol.Required(CONF_PASSWORD): str,
25  }
26 )
27 
28 DATA_SCHEMA_CONFIRM = vol.Schema(
29  {
30  vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str,
31  vol.Required(CONF_PASSWORD): str,
32  }
33 )
34 
35 RESULT_INVALID_AUTH = "invalid_auth"
36 RESULT_NO_DEVICES_FOUND = "no_devices_found"
37 RESULT_NOT_SUPPORTED = "not_supported"
38 RESULT_SUCCESS = "success"
39 
40 
41 class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
42  """Handle a AVM FRITZ!SmartHome config flow."""
43 
44  VERSION = 1
45 
46  _name: str
47 
48  def __init__(self) -> None:
49  """Initialize flow."""
50  self._host_host: str | None = None
51  self._password_password: str | None = None
52  self._username_username: str | None = None
53 
54  def _get_entry(self, name: str) -> ConfigFlowResult:
55  return self.async_create_entryasync_create_entryasync_create_entry(
56  title=name,
57  data={
58  CONF_HOST: self._host_host,
59  CONF_PASSWORD: self._password_password,
60  CONF_USERNAME: self._username_username,
61  },
62  )
63 
64  async def async_try_connect(self) -> str:
65  """Try to connect and check auth."""
66  return await self.hass.async_add_executor_job(self._try_connect_try_connect)
67 
68  def _try_connect(self) -> str:
69  """Try to connect and check auth."""
70  fritzbox = Fritzhome(
71  host=self._host_host, user=self._username_username, password=self._password_password
72  )
73  try:
74  fritzbox.login()
75  fritzbox.get_device_elements()
76  fritzbox.logout()
77  except LoginError:
78  return RESULT_INVALID_AUTH
79  except HTTPError:
80  return RESULT_NOT_SUPPORTED
81  except OSError:
82  return RESULT_NO_DEVICES_FOUND
83  return RESULT_SUCCESS
84 
85  async def async_step_user(
86  self, user_input: dict[str, Any] | None = None
87  ) -> ConfigFlowResult:
88  """Handle a flow initialized by the user."""
89  errors = {}
90 
91  if user_input is not None:
92  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
93 
94  self._host_host = user_input[CONF_HOST]
95  self._name_name = str(user_input[CONF_HOST])
96  self._password_password = user_input[CONF_PASSWORD]
97  self._username_username = user_input[CONF_USERNAME]
98 
99  result = await self.async_try_connectasync_try_connect()
100 
101  if result == RESULT_SUCCESS:
102  return self._get_entry_get_entry(self._name_name)
103  if result != RESULT_INVALID_AUTH:
104  return self.async_abortasync_abortasync_abort(reason=result)
105  errors["base"] = result
106 
107  return self.async_show_formasync_show_formasync_show_form(
108  step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors
109  )
110 
111  async def async_step_ssdp(
112  self, discovery_info: ssdp.SsdpServiceInfo
113  ) -> ConfigFlowResult:
114  """Handle a flow initialized by discovery."""
115  host = urlparse(discovery_info.ssdp_location).hostname
116  assert isinstance(host, str)
117 
118  if (
119  ipaddress.ip_address(host).version == 6
120  and ipaddress.ip_address(host).is_link_local
121  ):
122  return self.async_abortasync_abortasync_abort(reason="ignore_ip6_link_local")
123 
124  if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN):
125  if uuid.startswith("uuid:"):
126  uuid = uuid[5:]
127  await self.async_set_unique_idasync_set_unique_id(uuid)
128  self._abort_if_unique_id_configured_abort_if_unique_id_configured({CONF_HOST: host})
129 
130  self._host_host = host
131  if self.hass.config_entries.flow.async_has_matching_flow(self):
132  return self.async_abortasync_abortasync_abort(reason="already_in_progress")
133 
134  # update old and user-configured config entries
135  for entry in self._async_current_entries_async_current_entries(include_ignore=False):
136  if entry.data[CONF_HOST] == host:
137  if uuid and not entry.unique_id:
138  self.hass.config_entries.async_update_entry(entry, unique_id=uuid)
139  return self.async_abortasync_abortasync_abort(reason="already_configured")
140 
141  self._name_name = str(discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or host)
142 
143  self.context["title_placeholders"] = {"name": self._name_name}
144  return await self.async_step_confirmasync_step_confirm()
145 
146  def is_matching(self, other_flow: Self) -> bool:
147  """Return True if other_flow is matching this flow."""
148  return other_flow._host == self._host_host # noqa: SLF001
149 
151  self, user_input: dict[str, Any] | None = None
152  ) -> ConfigFlowResult:
153  """Handle user-confirmation of discovered node."""
154  errors = {}
155 
156  if user_input is not None:
157  self._password_password = user_input[CONF_PASSWORD]
158  self._username_username = user_input[CONF_USERNAME]
159  result = await self.async_try_connectasync_try_connect()
160 
161  if result == RESULT_SUCCESS:
162  return self._get_entry_get_entry(self._name_name)
163  if result != RESULT_INVALID_AUTH:
164  return self.async_abortasync_abortasync_abort(reason=result)
165  errors["base"] = result
166 
167  return self.async_show_formasync_show_formasync_show_form(
168  step_id="confirm",
169  data_schema=DATA_SCHEMA_CONFIRM,
170  description_placeholders={"name": self._name_name},
171  errors=errors,
172  )
173 
174  async def async_step_reauth(
175  self, entry_data: Mapping[str, Any]
176  ) -> ConfigFlowResult:
177  """Trigger a reauthentication flow."""
178  self._host_host = entry_data[CONF_HOST]
179  self._name_name = str(entry_data[CONF_HOST])
180  self._username_username = entry_data[CONF_USERNAME]
181 
182  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
183 
185  self, user_input: dict[str, Any] | None = None
186  ) -> ConfigFlowResult:
187  """Handle reauthorization flow."""
188  errors = {}
189 
190  if user_input is not None:
191  self._password_password = user_input[CONF_PASSWORD]
192  self._username_username = user_input[CONF_USERNAME]
193 
194  result = await self.async_try_connectasync_try_connect()
195 
196  if result == RESULT_SUCCESS:
197  return self.async_update_reload_and_abortasync_update_reload_and_abort(
198  self._get_reauth_entry_get_reauth_entry(),
199  data={
200  CONF_HOST: self._host_host,
201  CONF_PASSWORD: self._password_password,
202  CONF_USERNAME: self._username_username,
203  },
204  )
205  if result != RESULT_INVALID_AUTH:
206  return self.async_abortasync_abortasync_abort(reason=result)
207  errors["base"] = result
208 
209  return self.async_show_formasync_show_formasync_show_form(
210  step_id="reauth_confirm",
211  data_schema=vol.Schema(
212  {
213  vol.Required(CONF_USERNAME, default=self._username_username): str,
214  vol.Required(CONF_PASSWORD): str,
215  }
216  ),
217  description_placeholders={"name": self._name_name},
218  errors=errors,
219  )
220 
222  self, user_input: dict[str, Any] | None = None
223  ) -> ConfigFlowResult:
224  """Handle a reconfiguration flow initialized by the user."""
225  errors = {}
226 
227  if user_input is not None:
228  self._host_host = user_input[CONF_HOST]
229 
230  reconfigure_entry = self._get_reconfigure_entry_get_reconfigure_entry()
231  self._username_username = reconfigure_entry.data[CONF_USERNAME]
232  self._password_password = reconfigure_entry.data[CONF_PASSWORD]
233 
234  result = await self.async_try_connectasync_try_connect()
235 
236  if result == RESULT_SUCCESS:
237  return self.async_update_reload_and_abortasync_update_reload_and_abort(
238  reconfigure_entry,
239  data_updates={CONF_HOST: self._host_host},
240  )
241  errors["base"] = result
242 
243  host = self._get_reconfigure_entry_get_reconfigure_entry().data[CONF_HOST]
244  return self.async_show_formasync_show_formasync_show_form(
245  step_id="reconfigure",
246  data_schema=vol.Schema(
247  {
248  vol.Required(CONF_HOST, default=host): str,
249  }
250  ),
251  description_placeholders={"name": host},
252  errors=errors,
253  )
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
Definition: config_flow.py:113
ConfigFlowResult async_step_reconfigure(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:223
ConfigFlowResult async_step_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:152
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:176
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:87
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:186
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)
None _async_abort_entries_match(self, dict[str, Any]|None match_dict=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)
str
_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)