Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for the Huawei LTE platform."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 import logging
7 from typing import TYPE_CHECKING, Any
8 from urllib.parse import urlparse
9 
10 from huawei_lte_api.Client import Client
11 from huawei_lte_api.Connection import Connection
12 from huawei_lte_api.exceptions import (
13  LoginErrorPasswordWrongException,
14  LoginErrorUsernamePasswordOverrunException,
15  LoginErrorUsernamePasswordWrongException,
16  LoginErrorUsernameWrongException,
17  ResponseErrorException,
18 )
19 from huawei_lte_api.Session import GetResponseType
20 from requests.exceptions import SSLError, Timeout
21 from url_normalize import url_normalize
22 import voluptuous as vol
23 
24 from homeassistant.components import ssdp
25 from homeassistant.config_entries import (
26  ConfigEntry,
27  ConfigFlow,
28  ConfigFlowResult,
29  OptionsFlow,
30 )
31 from homeassistant.const import (
32  CONF_MAC,
33  CONF_NAME,
34  CONF_PASSWORD,
35  CONF_RECIPIENT,
36  CONF_URL,
37  CONF_USERNAME,
38  CONF_VERIFY_SSL,
39 )
40 from homeassistant.core import callback
41 
42 from .const import (
43  CONF_MANUFACTURER,
44  CONF_TRACK_WIRED_CLIENTS,
45  CONF_UNAUTHENTICATED_MODE,
46  CONNECTION_TIMEOUT,
47  DEFAULT_DEVICE_NAME,
48  DEFAULT_NOTIFY_SERVICE_NAME,
49  DEFAULT_TRACK_WIRED_CLIENTS,
50  DEFAULT_UNAUTHENTICATED_MODE,
51  DOMAIN,
52 )
53 from .utils import get_device_macs, non_verifying_requests_session
54 
55 _LOGGER = logging.getLogger(__name__)
56 
57 
58 class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
59  """Handle Huawei LTE config flow."""
60 
61  VERSION = 3
62 
63  manufacturer: str | None = None
64  url: str | None = None
65 
66  @staticmethod
67  @callback
69  config_entry: ConfigEntry,
70  ) -> OptionsFlowHandler:
71  """Get options flow."""
72  return OptionsFlowHandler()
73 
75  self,
76  user_input: dict[str, Any] | None = None,
77  errors: dict[str, str] | None = None,
78  ) -> ConfigFlowResult:
79  if user_input is None:
80  user_input = {}
81  return self.async_show_formasync_show_formasync_show_form(
82  step_id="user",
83  data_schema=vol.Schema(
84  {
85  vol.Required(
86  CONF_URL,
87  default=user_input.get(CONF_URL, self.urlurl or ""),
88  ): str,
89  vol.Optional(
90  CONF_VERIFY_SSL,
91  default=user_input.get(
92  CONF_VERIFY_SSL,
93  False,
94  ),
95  ): bool,
96  vol.Optional(
97  CONF_USERNAME, default=user_input.get(CONF_USERNAME) or ""
98  ): str,
99  vol.Optional(
100  CONF_PASSWORD, default=user_input.get(CONF_PASSWORD) or ""
101  ): str,
102  }
103  ),
104  errors=errors or {},
105  )
106 
108  self,
109  user_input: dict[str, Any],
110  errors: dict[str, str] | None = None,
111  ) -> ConfigFlowResult:
112  return self.async_show_formasync_show_formasync_show_form(
113  step_id="reauth_confirm",
114  data_schema=vol.Schema(
115  {
116  vol.Optional(
117  CONF_USERNAME, default=user_input.get(CONF_USERNAME) or ""
118  ): str,
119  vol.Optional(
120  CONF_PASSWORD, default=user_input.get(CONF_PASSWORD) or ""
121  ): str,
122  }
123  ),
124  errors=errors or {},
125  )
126 
127  async def _connect(
128  self, user_input: dict[str, Any], errors: dict[str, str]
129  ) -> Connection | None:
130  """Try connecting with given data."""
131  username = user_input.get(CONF_USERNAME) or ""
132  password = user_input.get(CONF_PASSWORD) or ""
133 
134  def _get_connection() -> Connection:
135  if (
136  user_input[CONF_URL].startswith("https://")
137  and not user_input[CONF_VERIFY_SSL]
138  ):
139  requests_session = non_verifying_requests_session(user_input[CONF_URL])
140  else:
141  requests_session = None
142 
143  return Connection(
144  url=user_input[CONF_URL],
145  username=username,
146  password=password,
147  timeout=CONNECTION_TIMEOUT,
148  requests_session=requests_session,
149  )
150 
151  conn = None
152  try:
153  conn = await self.hass.async_add_executor_job(_get_connection)
154  except LoginErrorUsernameWrongException:
155  errors[CONF_USERNAME] = "incorrect_username"
156  except LoginErrorPasswordWrongException:
157  errors[CONF_PASSWORD] = "incorrect_password"
158  except LoginErrorUsernamePasswordWrongException:
159  errors[CONF_USERNAME] = "invalid_auth"
160  except LoginErrorUsernamePasswordOverrunException:
161  errors["base"] = "login_attempts_exceeded"
162  except ResponseErrorException:
163  _LOGGER.warning("Response error", exc_info=True)
164  errors["base"] = "response_error"
165  except SSLError:
166  _LOGGER.warning("SSL error", exc_info=True)
167  if user_input[CONF_VERIFY_SSL]:
168  errors[CONF_URL] = "ssl_error_try_unverified"
169  else:
170  errors[CONF_URL] = "ssl_error_try_plain"
171  except Timeout:
172  _LOGGER.warning("Connection timeout", exc_info=True)
173  errors[CONF_URL] = "connection_timeout"
174  except Exception: # noqa: BLE001
175  _LOGGER.warning("Unknown error connecting to device", exc_info=True)
176  errors[CONF_URL] = "unknown"
177  return conn
178 
179  @staticmethod
180  def _disconnect(conn: Connection) -> None:
181  try:
182  conn.close()
183  conn.requests_session.close()
184  except Exception: # noqa: BLE001
185  _LOGGER.debug("Disconnect error", exc_info=True)
186 
187  async def async_step_user(
188  self, user_input: dict[str, Any] | None = None
189  ) -> ConfigFlowResult:
190  """Handle user initiated config flow."""
191  if user_input is None:
192  return await self._async_show_user_form_async_show_user_form()
193 
194  errors = {}
195 
196  # Normalize URL
197  user_input[CONF_URL] = url_normalize(
198  user_input[CONF_URL], default_scheme="http"
199  )
200  if "://" not in user_input[CONF_URL]:
201  errors[CONF_URL] = "invalid_url"
202  return await self._async_show_user_form_async_show_user_form(
203  user_input=user_input, errors=errors
204  )
205 
206  def get_device_info(
207  conn: Connection,
208  ) -> tuple[GetResponseType, GetResponseType]:
209  """Get router info."""
210  client = Client(conn)
211  try:
212  device_info = client.device.information()
213  except Exception: # noqa: BLE001
214  _LOGGER.debug("Could not get device.information", exc_info=True)
215  try:
216  device_info = client.device.basic_information()
217  except Exception: # noqa: BLE001
218  _LOGGER.debug(
219  "Could not get device.basic_information", exc_info=True
220  )
221  device_info = {}
222  try:
223  wlan_settings = client.wlan.multi_basic_settings()
224  except Exception: # noqa: BLE001
225  _LOGGER.debug("Could not get wlan.multi_basic_settings", exc_info=True)
226  wlan_settings = {}
227  return device_info, wlan_settings
228 
229  conn = await self._connect_connect(user_input, errors)
230  if errors:
231  return await self._async_show_user_form_async_show_user_form(
232  user_input=user_input, errors=errors
233  )
234  assert conn
235 
236  info, wlan_settings = await self.hass.async_add_executor_job(
237  get_device_info, conn
238  )
239  await self.hass.async_add_executor_job(self._disconnect_disconnect, conn)
240 
241  user_input.update(
242  {
243  CONF_MAC: get_device_macs(info, wlan_settings),
244  CONF_MANUFACTURER: self.manufacturermanufacturer,
245  }
246  )
247 
248  if not self.unique_idunique_id:
249  if serial_number := info.get("SerialNumber"):
250  await self.async_set_unique_idasync_set_unique_id(serial_number)
251  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates=user_input)
252  else:
253  await self._async_handle_discovery_without_unique_id_async_handle_discovery_without_unique_id()
254 
255  title = (
256  self.context.get("title_placeholders", {}).get(CONF_NAME)
257  or info.get("DeviceName") # device.information
258  or info.get("devicename") # device.basic_information
259  or DEFAULT_DEVICE_NAME
260  )
261 
262  return self.async_create_entryasync_create_entryasync_create_entry(title=title, data=user_input)
263 
264  async def async_step_ssdp(
265  self, discovery_info: ssdp.SsdpServiceInfo
266  ) -> ConfigFlowResult:
267  """Handle SSDP initiated config flow."""
268 
269  if TYPE_CHECKING:
270  assert discovery_info.ssdp_location
271  url = url_normalize(
272  discovery_info.upnp.get(
273  ssdp.ATTR_UPNP_PRESENTATION_URL,
274  f"http://{urlparse(discovery_info.ssdp_location).hostname}/",
275  )
276  )
277 
278  unique_id = discovery_info.upnp.get(
279  ssdp.ATTR_UPNP_SERIAL, discovery_info.upnp[ssdp.ATTR_UPNP_UDN]
280  )
281  await self.async_set_unique_idasync_set_unique_id(unique_id)
282  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_URL: url})
283 
284  def _is_supported_device() -> bool:
285  """See if we are looking at a possibly supported device.
286 
287  Matching solely on SSDP data does not yield reliable enough results.
288  """
289  try:
290  with Connection(url=url, timeout=CONNECTION_TIMEOUT) as conn:
291  basic_info = Client(conn).device.basic_information()
292  except ResponseErrorException: # API compatible error
293  return True
294  except Exception: # API incompatible error # noqa: BLE001
295  return False
296  return isinstance(basic_info, dict) # Crude content check
297 
298  if not await self.hass.async_add_executor_job(_is_supported_device):
299  return self.async_abortasync_abortasync_abort(reason="unsupported_device")
300 
301  self.context.update(
302  {
303  "title_placeholders": {
304  CONF_NAME: discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME)
305  or "Huawei LTE"
306  }
307  }
308  )
309  self.manufacturermanufacturer = discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER)
310  self.urlurl = url
311  return await self._async_show_user_form_async_show_user_form()
312 
313  async def async_step_reauth(
314  self, entry_data: Mapping[str, Any]
315  ) -> ConfigFlowResult:
316  """Perform reauth upon an API authentication error."""
317  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
318 
320  self, user_input: dict[str, Any] | None = None
321  ) -> ConfigFlowResult:
322  """Dialog that informs the user that reauth is required."""
323  entry = self._get_reauth_entry_get_reauth_entry()
324  if not user_input:
325  return await self._async_show_reauth_form_async_show_reauth_form(
326  user_input={
327  CONF_USERNAME: entry.data[CONF_USERNAME],
328  CONF_PASSWORD: entry.data[CONF_PASSWORD],
329  }
330  )
331 
332  new_data = {**entry.data, **user_input}
333  errors: dict[str, str] = {}
334  conn = await self._connect_connect(new_data, errors)
335  if conn:
336  await self.hass.async_add_executor_job(self._disconnect_disconnect, conn)
337  if errors:
338  return await self._async_show_reauth_form_async_show_reauth_form(
339  user_input=user_input, errors=errors
340  )
341 
342  return self.async_update_reload_and_abortasync_update_reload_and_abort(entry, data=new_data)
343 
344 
346  """Huawei LTE options flow."""
347 
348  async def async_step_init(
349  self, user_input: dict[str, Any] | None = None
350  ) -> ConfigFlowResult:
351  """Handle options flow."""
352 
353  # Recipients are persisted as a list, but handled as comma separated string in UI
354 
355  if user_input is not None:
356  # Preserve existing options, for example *_from_yaml markers
357  data = {**self.config_entryconfig_entryconfig_entry.options, **user_input}
358  if not isinstance(data[CONF_RECIPIENT], list):
359  data[CONF_RECIPIENT] = [
360  x.strip() for x in data[CONF_RECIPIENT].split(",")
361  ]
362  return self.async_create_entryasync_create_entry(title="", data=data)
363 
364  data_schema = vol.Schema(
365  {
366  vol.Optional(
367  CONF_NAME,
368  default=self.config_entryconfig_entryconfig_entry.options.get(
369  CONF_NAME, DEFAULT_NOTIFY_SERVICE_NAME
370  ),
371  ): str,
372  vol.Optional(
373  CONF_RECIPIENT,
374  default=", ".join(
375  self.config_entryconfig_entryconfig_entry.options.get(CONF_RECIPIENT, [])
376  ),
377  ): str,
378  vol.Optional(
379  CONF_TRACK_WIRED_CLIENTS,
380  default=self.config_entryconfig_entryconfig_entry.options.get(
381  CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS
382  ),
383  ): bool,
384  vol.Optional(
385  CONF_UNAUTHENTICATED_MODE,
386  default=self.config_entryconfig_entryconfig_entry.options.get(
387  CONF_UNAUTHENTICATED_MODE, DEFAULT_UNAUTHENTICATED_MODE
388  ),
389  ): bool,
390  }
391  )
392  return self.async_show_formasync_show_form(step_id="init", data_schema=data_schema)
Connection|None _connect(self, dict[str, Any] user_input, dict[str, str] errors)
Definition: config_flow.py:129
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:315
OptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:70
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:189
ConfigFlowResult _async_show_user_form(self, dict[str, Any]|None user_input=None, dict[str, str]|None errors=None)
Definition: config_flow.py:78
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:321
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
Definition: config_flow.py:266
ConfigFlowResult _async_show_reauth_form(self, dict[str, Any] user_input, dict[str, str]|None errors=None)
Definition: config_flow.py:111
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:350
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_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)
_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)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
requests.Session non_verifying_requests_session(str url)
Definition: utils.py:36
list[str] get_device_macs(GetResponseType device_info, GetResponseType wlan_settings)
Definition: utils.py:19
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
DeviceInfo get_device_info(str coordinates, str name)
Definition: __init__.py:156