Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Keenetic NDMS2."""
2 
3 from __future__ import annotations
4 
5 from typing import Any, cast
6 from urllib.parse import urlparse
7 
8 from ndms2_client import Client, ConnectionException, InterfaceInfo, TelnetConnection
9 import voluptuous as vol
10 
11 from homeassistant.components import ssdp
12 from homeassistant.config_entries import (
13  ConfigEntry,
14  ConfigFlow,
15  ConfigFlowResult,
16  OptionsFlow,
17 )
18 from homeassistant.const import (
19  CONF_HOST,
20  CONF_PASSWORD,
21  CONF_PORT,
22  CONF_SCAN_INTERVAL,
23  CONF_USERNAME,
24 )
25 from homeassistant.core import callback
26 from homeassistant.helpers import config_validation as cv
27 from homeassistant.helpers.typing import VolDictType
28 
29 from .const import (
30  CONF_CONSIDER_HOME,
31  CONF_INCLUDE_ARP,
32  CONF_INCLUDE_ASSOCIATED,
33  CONF_INTERFACES,
34  CONF_TRY_HOTSPOT,
35  DEFAULT_CONSIDER_HOME,
36  DEFAULT_INTERFACE,
37  DEFAULT_SCAN_INTERVAL,
38  DEFAULT_TELNET_PORT,
39  DOMAIN,
40  ROUTER,
41 )
42 from .router import KeeneticRouter
43 
44 
45 class KeeneticFlowHandler(ConfigFlow, domain=DOMAIN):
46  """Handle a config flow."""
47 
48  VERSION = 1
49 
50  host: str | bytes | None = None
51 
52  @staticmethod
53  @callback
55  config_entry: ConfigEntry,
56  ) -> KeeneticOptionsFlowHandler:
57  """Get the options flow for this handler."""
59 
60  async def async_step_user(
61  self, user_input: dict[str, Any] | None = None
62  ) -> ConfigFlowResult:
63  """Handle a flow initialized by the user."""
64  errors = {}
65  if user_input is not None:
66  host = self.hosthost or user_input[CONF_HOST]
67  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: host})
68 
69  _client = Client(
70  TelnetConnection(
71  host,
72  user_input[CONF_PORT],
73  user_input[CONF_USERNAME],
74  user_input[CONF_PASSWORD],
75  timeout=10,
76  )
77  )
78 
79  try:
80  router_info = await self.hass.async_add_executor_job(
81  _client.get_router_info
82  )
83  except ConnectionException:
84  errors["base"] = "cannot_connect"
85  else:
86  return self.async_create_entryasync_create_entryasync_create_entry(
87  title=router_info.name, data={CONF_HOST: host, **user_input}
88  )
89 
90  host_schema: VolDictType = (
91  {vol.Required(CONF_HOST): str} if not self.hosthost else {}
92  )
93 
94  return self.async_show_formasync_show_formasync_show_form(
95  step_id="user",
96  data_schema=vol.Schema(
97  {
98  **host_schema,
99  vol.Required(CONF_USERNAME): str,
100  vol.Required(CONF_PASSWORD): str,
101  vol.Optional(CONF_PORT, default=DEFAULT_TELNET_PORT): int,
102  }
103  ),
104  errors=errors,
105  )
106 
107  async def async_step_ssdp(
108  self, discovery_info: ssdp.SsdpServiceInfo
109  ) -> ConfigFlowResult:
110  """Handle a discovered device."""
111  friendly_name = discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "")
112 
113  # Filter out items not having "keenetic" in their name
114  if "keenetic" not in friendly_name.lower():
115  return self.async_abortasync_abortasync_abort(reason="not_keenetic_ndms2")
116 
117  # Filters out items having no/empty UDN
118  if not discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN):
119  return self.async_abortasync_abortasync_abort(reason="no_udn")
120 
121  # We can cast the hostname to str because the ssdp_location is not bytes and
122  # not a relative url
123  host = cast(str, urlparse(discovery_info.ssdp_location).hostname)
124  await self.async_set_unique_idasync_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_UDN])
125  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: host})
126 
127  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: host})
128 
129  self.hosthost = host
130  self.context["title_placeholders"] = {
131  "name": friendly_name,
132  "host": host,
133  }
134 
135  return await self.async_step_userasync_step_userasync_step_user()
136 
137 
139  """Handle options."""
140 
141  def __init__(self) -> None:
142  """Initialize options flow."""
143  self._interface_options_interface_options: dict[str, str] = {}
144 
145  async def async_step_init(
146  self, user_input: dict[str, Any] | None = None
147  ) -> ConfigFlowResult:
148  """Manage the options."""
149  router: KeeneticRouter = self.hass.data[DOMAIN][self.config_entryconfig_entryconfig_entry.entry_id][
150  ROUTER
151  ]
152 
153  interfaces: list[InterfaceInfo] = await self.hass.async_add_executor_job(
154  router.client.get_interfaces
155  )
156 
157  self._interface_options_interface_options = {
158  interface.name: (interface.description or interface.name)
159  for interface in interfaces
160  if interface.type.lower() == "bridge"
161  }
162  return await self.async_step_userasync_step_user()
163 
164  async def async_step_user(
165  self, user_input: dict[str, Any] | None = None
166  ) -> ConfigFlowResult:
167  """Manage the device tracker options."""
168  if user_input is not None:
169  return self.async_create_entryasync_create_entry(title="", data=user_input)
170 
171  options = vol.Schema(
172  {
173  vol.Required(
174  CONF_SCAN_INTERVAL,
175  default=self.config_entryconfig_entryconfig_entry.options.get(
176  CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
177  ),
178  ): int,
179  vol.Required(
180  CONF_CONSIDER_HOME,
181  default=self.config_entryconfig_entryconfig_entry.options.get(
182  CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME
183  ),
184  ): int,
185  vol.Required(
186  CONF_INTERFACES,
187  default=self.config_entryconfig_entryconfig_entry.options.get(
188  CONF_INTERFACES, [DEFAULT_INTERFACE]
189  ),
190  ): cv.multi_select(self._interface_options_interface_options),
191  vol.Optional(
192  CONF_TRY_HOTSPOT,
193  default=self.config_entryconfig_entryconfig_entry.options.get(CONF_TRY_HOTSPOT, True),
194  ): bool,
195  vol.Optional(
196  CONF_INCLUDE_ARP,
197  default=self.config_entryconfig_entryconfig_entry.options.get(CONF_INCLUDE_ARP, True),
198  ): bool,
199  vol.Optional(
200  CONF_INCLUDE_ASSOCIATED,
201  default=self.config_entryconfig_entryconfig_entry.options.get(
202  CONF_INCLUDE_ASSOCIATED, True
203  ),
204  ): bool,
205  }
206  )
207 
208  return self.async_show_formasync_show_form(step_id="user", data_schema=options)
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
Definition: config_flow.py:109
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:62
KeeneticOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:56
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:147
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:166
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_step_user(self, dict[str, Any]|None user_input=None)
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)
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)