Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for fritzbox_callmonitor."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 from enum import StrEnum
7 from typing import Any, cast
8 
9 from fritzconnection import FritzConnection
10 from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError
11 from requests.exceptions import ConnectionError as RequestsConnectionError
12 import voluptuous as vol
13 
14 from homeassistant.config_entries import (
15  SOURCE_IMPORT,
16  ConfigEntry,
17  ConfigFlow,
18  ConfigFlowResult,
19  OptionsFlow,
20 )
21 from homeassistant.const import (
22  CONF_HOST,
23  CONF_NAME,
24  CONF_PASSWORD,
25  CONF_PORT,
26  CONF_USERNAME,
27 )
28 from homeassistant.core import callback
29 
30 from .base import FritzBoxPhonebook
31 from .const import (
32  CONF_PHONEBOOK,
33  CONF_PREFIXES,
34  DEFAULT_HOST,
35  DEFAULT_PHONEBOOK,
36  DEFAULT_PORT,
37  DEFAULT_USERNAME,
38  DOMAIN,
39  FRITZ_ATTR_NAME,
40  FRITZ_ATTR_SERIAL_NUMBER,
41  SERIAL_NUMBER,
42 )
43 
44 DATA_SCHEMA_USER = vol.Schema(
45  {
46  vol.Required(CONF_HOST, default=DEFAULT_HOST): str,
47  vol.Required(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int),
48  vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str,
49  vol.Required(CONF_PASSWORD): str,
50  }
51 )
52 
53 
54 class ConnectResult(StrEnum):
55  """FritzBoxPhonebook connection result."""
56 
57  INVALID_AUTH = "invalid_auth"
58  INSUFFICIENT_PERMISSIONS = "insufficient_permissions"
59  MALFORMED_PREFIXES = "malformed_prefixes"
60  NO_DEVIES_FOUND = "no_devices_found"
61  SUCCESS = "success"
62 
63 
65  """Handle a fritzbox_callmonitor config flow."""
66 
67  VERSION = 1
68 
69  _entry: ConfigEntry
70  _host: str
71  _port: int
72  _username: str
73  _password: str
74  _phonebook_name: str
75  _phonebook_id: int
76  _phonebook_ids: list[int]
77  _fritzbox_phonebook: FritzBoxPhonebook
78  _serial_number: str
79 
80  def __init__(self) -> None:
81  """Initialize flow."""
82  self._phonebook_names_phonebook_names: list[str] | None = None
83 
84  def _get_config_entry(self) -> ConfigFlowResult:
85  """Create and return an config entry."""
86  return self.async_create_entryasync_create_entryasync_create_entry(
87  title=self._phonebook_name_phonebook_name,
88  data={
89  CONF_HOST: self._host_host,
90  CONF_PORT: self._port_port,
91  CONF_USERNAME: self._username_username,
92  CONF_PASSWORD: self._password_password,
93  CONF_PHONEBOOK: self._phonebook_id_phonebook_id,
94  SERIAL_NUMBER: self._serial_number_serial_number,
95  },
96  )
97 
98  def _try_connect(self) -> ConnectResult:
99  """Try to connect and check auth."""
100  self._fritzbox_phonebook_fritzbox_phonebook = FritzBoxPhonebook(
101  host=self._host_host,
102  username=self._username_username,
103  password=self._password_password,
104  )
105 
106  try:
107  self._fritzbox_phonebook_fritzbox_phonebook.init_phonebook()
108  self._phonebook_ids_phonebook_ids = self._fritzbox_phonebook_fritzbox_phonebook.get_phonebook_ids()
109 
110  fritz_connection = FritzConnection(
111  address=self._host_host, user=self._username_username, password=self._password_password
112  )
113  info = fritz_connection.updatecheck
114  except RequestsConnectionError:
115  return ConnectResult.NO_DEVIES_FOUND
116  except FritzSecurityError:
117  return ConnectResult.INSUFFICIENT_PERMISSIONS
118  except FritzConnectionException:
119  return ConnectResult.INVALID_AUTH
120 
121  self._serial_number_serial_number = info[FRITZ_ATTR_SERIAL_NUMBER]
122  return ConnectResult.SUCCESS
123 
124  async def _get_name_of_phonebook(self, phonebook_id: int) -> str:
125  """Return name of phonebook for given phonebook_id."""
126  phonebook_info = await self.hass.async_add_executor_job(
127  self._fritzbox_phonebook_fritzbox_phonebook.fph.phonebook_info, phonebook_id
128  )
129  return cast(str, phonebook_info[FRITZ_ATTR_NAME])
130 
131  async def _get_list_of_phonebook_names(self) -> list[str]:
132  """Return list of names for all available phonebooks."""
133  return [
134  await self._get_name_of_phonebook_get_name_of_phonebook(phonebook_id)
135  for phonebook_id in self._phonebook_ids_phonebook_ids
136  ]
137 
138  @staticmethod
139  @callback
141  config_entry: ConfigEntry,
142  ) -> FritzBoxCallMonitorOptionsFlowHandler:
143  """Get the options flow for this handler."""
145 
146  async def async_step_user(
147  self, user_input: dict[str, Any] | None = None
148  ) -> ConfigFlowResult:
149  """Handle a flow initialized by the user."""
150 
151  if user_input is None:
152  return self.async_show_formasync_show_formasync_show_form(
153  step_id="user", data_schema=DATA_SCHEMA_USER, errors={}
154  )
155 
156  self._host_host = user_input[CONF_HOST]
157  self._port_port = user_input[CONF_PORT]
158  self._password_password = user_input[CONF_PASSWORD]
159  self._username_username = user_input[CONF_USERNAME]
160 
161  result = await self.hass.async_add_executor_job(self._try_connect_try_connect)
162 
163  if result == ConnectResult.INVALID_AUTH:
164  return self.async_show_formasync_show_formasync_show_form(
165  step_id="user",
166  data_schema=DATA_SCHEMA_USER,
167  errors={"base": ConnectResult.INVALID_AUTH},
168  )
169 
170  if result != ConnectResult.SUCCESS:
171  return self.async_abortasync_abortasync_abort(reason=result)
172 
173  if self.context["source"] == SOURCE_IMPORT:
174  self._phonebook_id_phonebook_id = user_input[CONF_PHONEBOOK]
175  self._phonebook_name_phonebook_name = user_input[CONF_NAME]
176 
177  elif len(self._phonebook_ids_phonebook_ids) > 1:
178  return await self.async_step_phonebookasync_step_phonebook()
179 
180  else:
181  self._phonebook_id_phonebook_id = DEFAULT_PHONEBOOK
182  self._phonebook_name_phonebook_name = await self._get_name_of_phonebook_get_name_of_phonebook(self._phonebook_id_phonebook_id)
183 
184  await self.async_set_unique_idasync_set_unique_id(f"{self._serial_number}-{self._phonebook_id}")
185  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
186 
187  return self._get_config_entry_get_config_entry()
188 
190  self, user_input: dict[str, Any] | None = None
191  ) -> ConfigFlowResult:
192  """Handle a flow to chose one of multiple available phonebooks."""
193 
194  if self._phonebook_names_phonebook_names is None:
195  self._phonebook_names_phonebook_names = await self._get_list_of_phonebook_names_get_list_of_phonebook_names()
196 
197  if user_input is None:
198  return self.async_show_formasync_show_formasync_show_form(
199  step_id="phonebook",
200  data_schema=vol.Schema(
201  {vol.Required(CONF_PHONEBOOK): vol.In(self._phonebook_names_phonebook_names)}
202  ),
203  errors={},
204  )
205 
206  self._phonebook_name_phonebook_name = user_input[CONF_PHONEBOOK]
207  self._phonebook_id_phonebook_id = self._phonebook_names_phonebook_names.index(self._phonebook_name_phonebook_name)
208 
209  await self.async_set_unique_idasync_set_unique_id(f"{self._serial_number}-{self._phonebook_id}")
210  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
211 
212  return self._get_config_entry_get_config_entry()
213 
214  async def async_step_reauth(
215  self, entry_data: Mapping[str, Any]
216  ) -> ConfigFlowResult:
217  """Handle flow upon an API authentication error."""
218  self._entry_entry = self._get_reauth_entry_get_reauth_entry()
219  self._host_host = entry_data[CONF_HOST]
220  self._port_port = entry_data[CONF_PORT]
221  self._username_username = entry_data[CONF_USERNAME]
222  self._password_password = entry_data[CONF_PASSWORD]
223  self._phonebook_id_phonebook_id = entry_data[CONF_PHONEBOOK]
224 
225  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
226 
228  self, user_input: dict[str, Any], errors: dict[str, str] | None = None
229  ) -> ConfigFlowResult:
230  """Show the reauth form to the user."""
231  default_username = user_input.get(CONF_USERNAME)
232  return self.async_show_formasync_show_formasync_show_form(
233  step_id="reauth_confirm",
234  data_schema=vol.Schema(
235  {
236  vol.Required(CONF_USERNAME, default=default_username): str,
237  vol.Required(CONF_PASSWORD): str,
238  }
239  ),
240  description_placeholders={"host": self._host_host},
241  errors=errors or {},
242  )
243 
245  self, user_input: dict[str, Any] | None = None
246  ) -> ConfigFlowResult:
247  """Dialog that informs the user that reauth is required."""
248  if user_input is None:
249  return self._show_setup_form_reauth_confirm_show_setup_form_reauth_confirm(
250  user_input={CONF_USERNAME: self._username_username}
251  )
252 
253  self._username_username = user_input[CONF_USERNAME]
254  self._password_password = user_input[CONF_PASSWORD]
255 
256  if (
257  error := await self.hass.async_add_executor_job(self._try_connect_try_connect)
258  ) is not ConnectResult.SUCCESS:
259  return self._show_setup_form_reauth_confirm_show_setup_form_reauth_confirm(
260  user_input=user_input, errors={"base": error}
261  )
262 
263  self.hass.config_entries.async_update_entry(
264  self._entry_entry,
265  data={
266  CONF_HOST: self._host_host,
267  CONF_PORT: self._port_port,
268  CONF_USERNAME: self._username_username,
269  CONF_PASSWORD: self._password_password,
270  CONF_PHONEBOOK: self._phonebook_id_phonebook_id,
271  SERIAL_NUMBER: self._serial_number_serial_number,
272  },
273  )
274  await self.hass.config_entries.async_reload(self._entry_entry.entry_id)
275  return self.async_abortasync_abortasync_abort(reason="reauth_successful")
276 
277 
279  """Handle a fritzbox_callmonitor options flow."""
280 
281  @classmethod
282  def _are_prefixes_valid(cls, prefixes: str | None) -> bool:
283  """Check if prefixes are valid."""
284  return bool(prefixes.strip()) if prefixes else prefixes is None
285 
286  @classmethod
287  def _get_list_of_prefixes(cls, prefixes: str | None) -> list[str] | None:
288  """Get list of prefixes."""
289  if prefixes is None:
290  return None
291  return [prefix.strip() for prefix in prefixes.split(",")]
292 
293  def _get_option_schema_prefixes(self) -> vol.Schema:
294  """Get option schema for entering prefixes."""
295  return vol.Schema(
296  {
297  vol.Optional(
298  CONF_PREFIXES,
299  description={
300  "suggested_value": self.config_entryconfig_entryconfig_entry.options.get(CONF_PREFIXES)
301  },
302  ): str
303  }
304  )
305 
306  async def async_step_init(
307  self, user_input: dict[str, Any] | None = None
308  ) -> ConfigFlowResult:
309  """Manage the options."""
310 
311  option_schema_prefixes = self._get_option_schema_prefixes_get_option_schema_prefixes()
312 
313  if user_input is None:
314  return self.async_show_formasync_show_form(
315  step_id="init",
316  data_schema=option_schema_prefixes,
317  errors={},
318  )
319 
320  prefixes: str | None = user_input.get(CONF_PREFIXES)
321 
322  if not self._are_prefixes_valid_are_prefixes_valid(prefixes):
323  return self.async_show_formasync_show_form(
324  step_id="init",
325  data_schema=option_schema_prefixes,
326  errors={"base": ConnectResult.MALFORMED_PREFIXES},
327  )
328 
329  return self.async_create_entryasync_create_entry(
330  title="", data={CONF_PREFIXES: self._get_list_of_prefixes_get_list_of_prefixes(prefixes)}
331  )
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:148
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:216
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:246
ConfigFlowResult _show_setup_form_reauth_confirm(self, dict[str, Any] user_input, dict[str, str]|None errors=None)
Definition: config_flow.py:229
FritzBoxCallMonitorOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:142
ConfigFlowResult async_step_phonebook(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:191
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:308
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)
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)
_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)