Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow to configure the AsusWrt integration."""
2 
3 from __future__ import annotations
4 
5 import logging
6 import os
7 import socket
8 from typing import Any, cast
9 
10 from pyasuswrt import AsusWrtError
11 import voluptuous as vol
12 
14  CONF_CONSIDER_HOME,
15  DEFAULT_CONSIDER_HOME,
16 )
17 from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
18 from homeassistant.const import (
19  CONF_BASE,
20  CONF_HOST,
21  CONF_MODE,
22  CONF_PASSWORD,
23  CONF_PORT,
24  CONF_PROTOCOL,
25  CONF_USERNAME,
26 )
27 from homeassistant.core import callback
28 from homeassistant.helpers import config_validation as cv
30  SchemaCommonFlowHandler,
31  SchemaFlowFormStep,
32  SchemaOptionsFlowHandler,
33 )
34 from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
35 from homeassistant.helpers.typing import VolDictType
36 
37 from .bridge import AsusWrtBridge
38 from .const import (
39  CONF_DNSMASQ,
40  CONF_INTERFACE,
41  CONF_REQUIRE_IP,
42  CONF_SSH_KEY,
43  CONF_TRACK_UNKNOWN,
44  DEFAULT_DNSMASQ,
45  DEFAULT_INTERFACE,
46  DEFAULT_TRACK_UNKNOWN,
47  DOMAIN,
48  MODE_AP,
49  MODE_ROUTER,
50  PROTOCOL_HTTP,
51  PROTOCOL_HTTPS,
52  PROTOCOL_SSH,
53  PROTOCOL_TELNET,
54 )
55 
56 ALLOWED_PROTOCOL = [
57  PROTOCOL_HTTPS,
58  PROTOCOL_SSH,
59  PROTOCOL_HTTP,
60  PROTOCOL_TELNET,
61 ]
62 
63 PASS_KEY = "pass_key"
64 PASS_KEY_MSG = "Only provide password or SSH key file"
65 
66 RESULT_CONN_ERROR = "cannot_connect"
67 RESULT_SUCCESS = "success"
68 RESULT_UNKNOWN = "unknown"
69 
70 _LOGGER = logging.getLogger(__name__)
71 
72 LEGACY_SCHEMA = vol.Schema(
73  {
74  vol.Required(CONF_MODE, default=MODE_ROUTER): vol.In(
75  {MODE_ROUTER: "Router", MODE_AP: "Access Point"}
76  ),
77  }
78 )
79 
80 OPTIONS_SCHEMA = vol.Schema(
81  {
82  vol.Optional(
83  CONF_CONSIDER_HOME, default=DEFAULT_CONSIDER_HOME.total_seconds()
84  ): vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900)),
85  vol.Optional(CONF_TRACK_UNKNOWN, default=DEFAULT_TRACK_UNKNOWN): bool,
86  }
87 )
88 
89 
90 async def get_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
91  """Get options schema."""
92  options_flow: SchemaOptionsFlowHandler
93  options_flow = cast(SchemaOptionsFlowHandler, handler.parent_handler)
94  used_protocol = options_flow.config_entry.data[CONF_PROTOCOL]
95  if used_protocol in [PROTOCOL_SSH, PROTOCOL_TELNET]:
96  data_schema = OPTIONS_SCHEMA.extend(
97  {
98  vol.Required(CONF_INTERFACE, default=DEFAULT_INTERFACE): str,
99  vol.Required(CONF_DNSMASQ, default=DEFAULT_DNSMASQ): str,
100  }
101  )
102  if options_flow.config_entry.data[CONF_MODE] == MODE_AP:
103  return data_schema.extend(
104  {
105  vol.Optional(CONF_REQUIRE_IP, default=True): bool,
106  }
107  )
108  return data_schema
109 
110  return OPTIONS_SCHEMA
111 
112 
113 OPTIONS_FLOW = {
114  "init": SchemaFlowFormStep(get_options_schema),
115 }
116 
117 
118 def _is_file(value: str) -> bool:
119  """Validate that the value is an existing file."""
120  file_in = os.path.expanduser(value)
121  return os.path.isfile(file_in) and os.access(file_in, os.R_OK)
122 
123 
124 def _get_ip(host: str) -> str | None:
125  """Get the ip address from the host name."""
126  try:
127  return socket.gethostbyname(host)
128  except socket.gaierror:
129  return None
130 
131 
132 class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN):
133  """Handle a config flow for AsusWRT."""
134 
135  VERSION = 1
136 
137  def __init__(self) -> None:
138  """Initialize the AsusWrt config flow."""
139  self._config_data_config_data: dict[str, Any] = {}
140 
141  @callback
142  def _show_setup_form(self, error: str | None = None) -> ConfigFlowResult:
143  """Show the setup form to the user."""
144 
145  user_input = self._config_data_config_data
146 
147  add_schema: VolDictType
148  if self.show_advanced_optionsshow_advanced_options:
149  add_schema = {
150  vol.Exclusive(CONF_PASSWORD, PASS_KEY, PASS_KEY_MSG): str,
151  vol.Optional(CONF_PORT): cv.port,
152  vol.Exclusive(CONF_SSH_KEY, PASS_KEY, PASS_KEY_MSG): str,
153  }
154  else:
155  add_schema = {vol.Required(CONF_PASSWORD): str}
156 
157  schema = {
158  vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str,
159  vol.Required(CONF_USERNAME, default=user_input.get(CONF_USERNAME, "")): str,
160  **add_schema,
161  vol.Required(
162  CONF_PROTOCOL,
163  default=user_input.get(CONF_PROTOCOL, PROTOCOL_HTTPS),
164  ): SelectSelector(
166  options=ALLOWED_PROTOCOL, translation_key="protocols"
167  )
168  ),
169  }
170 
171  return self.async_show_formasync_show_formasync_show_form(
172  step_id="user",
173  data_schema=vol.Schema(schema),
174  errors={CONF_BASE: error} if error else None,
175  )
176 
178  self, user_input: dict[str, Any]
179  ) -> tuple[str, str | None]:
180  """Attempt to connect the AsusWrt router."""
181 
182  api: AsusWrtBridge
183  host: str = user_input[CONF_HOST]
184  protocol = user_input[CONF_PROTOCOL]
185  error: str | None = None
186 
187  conf = {**user_input, CONF_MODE: MODE_ROUTER}
188  api = AsusWrtBridge.get_bridge(self.hass, conf)
189  try:
190  await api.async_connect()
191 
192  except (AsusWrtError, OSError):
193  _LOGGER.error(
194  "Error connecting to the AsusWrt router at %s using protocol %s",
195  host,
196  protocol,
197  )
198  error = RESULT_CONN_ERROR
199 
200  except Exception:
201  _LOGGER.exception(
202  "Unknown error connecting with AsusWrt router at %s using protocol %s",
203  host,
204  protocol,
205  )
206  error = RESULT_UNKNOWN
207 
208  if error is None:
209  if not api.is_connected:
210  _LOGGER.error(
211  "Error connecting to the AsusWrt router at %s using protocol %s",
212  host,
213  protocol,
214  )
215  error = RESULT_CONN_ERROR
216 
217  if error is not None:
218  return error, None
219 
220  _LOGGER.debug(
221  "Successfully connected to the AsusWrt router at %s using protocol %s",
222  host,
223  protocol,
224  )
225  unique_id = api.label_mac
226  await api.async_disconnect()
227 
228  return RESULT_SUCCESS, unique_id
229 
230  async def async_step_user(
231  self, user_input: dict[str, Any] | None = None
232  ) -> ConfigFlowResult:
233  """Handle a flow initiated by the user."""
234 
235  # if there's one entry without unique ID, we abort config flow
236  for unique_id in self._async_current_ids_async_current_ids():
237  if unique_id is None:
238  return self.async_abortasync_abortasync_abort(reason="no_unique_id")
239 
240  if user_input is None:
241  return self._show_setup_form_show_setup_form()
242 
243  self._config_data_config_data = user_input
244  pwd: str | None = user_input.get(CONF_PASSWORD)
245  ssh: str | None = user_input.get(CONF_SSH_KEY)
246  protocol: str = user_input[CONF_PROTOCOL]
247 
248  if not pwd and protocol != PROTOCOL_SSH:
249  return self._show_setup_form_show_setup_form(error="pwd_required")
250  if not (pwd or ssh):
251  return self._show_setup_form_show_setup_form(error="pwd_or_ssh")
252  if ssh and not await self.hass.async_add_executor_job(_is_file, ssh):
253  return self._show_setup_form_show_setup_form(error="ssh_not_file")
254 
255  host: str = user_input[CONF_HOST]
256  if not await self.hass.async_add_executor_job(_get_ip, host):
257  return self._show_setup_form_show_setup_form(error="invalid_host")
258 
259  result, unique_id = await self._async_check_connection_async_check_connection(user_input)
260  if result == RESULT_SUCCESS:
261  if unique_id:
262  await self.async_set_unique_idasync_set_unique_id(unique_id)
263  # we allow to configure a single instance without unique id
264  elif self._async_current_entries_async_current_entries():
265  return self.async_abortasync_abortasync_abort(reason="invalid_unique_id")
266  else:
267  _LOGGER.warning(
268  "This device does not provide a valid Unique ID."
269  " Configuration of multiple instance will not be possible"
270  )
271 
272  if protocol in [PROTOCOL_SSH, PROTOCOL_TELNET]:
273  return await self.async_step_legacyasync_step_legacy()
274  return await self._async_save_entry_async_save_entry()
275 
276  return self._show_setup_form_show_setup_form(error=result)
277 
278  async def async_step_legacy(
279  self, user_input: dict[str, Any] | None = None
280  ) -> ConfigFlowResult:
281  """Handle a flow for legacy settings."""
282  if user_input is None:
283  return self.async_show_formasync_show_formasync_show_form(step_id="legacy", data_schema=LEGACY_SCHEMA)
284 
285  self._config_data_config_data.update(user_input)
286  return await self._async_save_entry_async_save_entry()
287 
288  async def _async_save_entry(self) -> ConfigFlowResult:
289  """Save entry data if unique id is valid."""
290  return self.async_create_entryasync_create_entryasync_create_entry(
291  title=self._config_data_config_data[CONF_HOST],
292  data=self._config_data_config_data,
293  )
294 
295  @staticmethod
296  @callback
298  config_entry: ConfigEntry,
299  ) -> SchemaOptionsFlowHandler:
300  """Get options flow for this handler."""
301  return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:232
SchemaOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:299
ConfigFlowResult async_step_legacy(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:280
tuple[str, str|None] _async_check_connection(self, dict[str, Any] user_input)
Definition: config_flow.py:179
ConfigFlowResult _show_setup_form(self, str|None error=None)
Definition: config_flow.py:142
set[str|None] _async_current_ids(self, bool include_ignore=True)
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_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)
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)
vol.Schema get_options_schema(SchemaCommonFlowHandler handler)
Definition: config_flow.py:90
IssData update(pyiss.ISS iss)
Definition: __init__.py:33