Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Nmap Tracker integration."""
2 
3 from __future__ import annotations
4 
5 from ipaddress import ip_address, ip_network, summarize_address_range
6 from typing import Any
7 
8 import voluptuous as vol
9 
10 from homeassistant.components import network
12  CONF_CONSIDER_HOME,
13  CONF_SCAN_INTERVAL,
14  DEFAULT_CONSIDER_HOME,
15 )
16 from homeassistant.components.network import MDNS_TARGET_IP
17 from homeassistant.config_entries import (
18  ConfigEntry,
19  ConfigFlow,
20  ConfigFlowResult,
21  OptionsFlow,
22 )
23 from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS
24 from homeassistant.core import HomeAssistant, callback
26 from homeassistant.helpers.typing import VolDictType
27 
28 from .const import (
29  CONF_HOME_INTERVAL,
30  CONF_OPTIONS,
31  DEFAULT_OPTIONS,
32  DOMAIN,
33  TRACKER_SCAN_INTERVAL,
34 )
35 
36 MAX_SCAN_INTERVAL = 3600
37 MAX_CONSIDER_HOME = MAX_SCAN_INTERVAL * 6
38 DEFAULT_NETWORK_PREFIX = 24
39 
40 
41 async def async_get_network(hass: HomeAssistant) -> str:
42  """Search adapters for the network."""
43  # We want the local ip that is most likely to be
44  # on the LAN and not the WAN so we use MDNS_TARGET_IP
45  local_ip = await network.async_get_source_ip(hass, MDNS_TARGET_IP)
46  network_prefix = DEFAULT_NETWORK_PREFIX
47  for adapter in await network.async_get_adapters(hass):
48  for ipv4 in adapter["ipv4"]:
49  if ipv4["address"] == local_ip:
50  network_prefix = ipv4["network_prefix"]
51  break
52  return str(ip_network(f"{local_ip}/{network_prefix}", False))
53 
54 
55 def _normalize_ips_and_network(hosts_str: str) -> list[str] | None:
56  """Check if a list of hosts are all ips or ip networks."""
57 
58  normalized_hosts = []
59  hosts = [host for host in cv.ensure_list_csv(hosts_str) if host != ""]
60 
61  for host in sorted(hosts):
62  try:
63  start, end = host.split("-", 1)
64  if "." not in end:
65  ip_1, ip_2, ip_3, _ = start.split(".", 3)
66  end = f"{ip_1}.{ip_2}.{ip_3}.{end}"
67  summarize_address_range(ip_address(start), ip_address(end))
68  except ValueError:
69  pass
70  else:
71  normalized_hosts.append(host)
72  continue
73 
74  try:
75  normalized_hosts.append(str(ip_address(host)))
76  except ValueError:
77  pass
78  else:
79  continue
80 
81  try:
82  normalized_hosts.append(str(ip_network(host)))
83  except ValueError:
84  return None
85 
86  return normalized_hosts
87 
88 
89 def normalize_input(user_input: dict[str, Any]) -> dict[str, str]:
90  """Validate hosts and exclude are valid."""
91  errors = {}
92  normalized_hosts = _normalize_ips_and_network(user_input[CONF_HOSTS])
93  if not normalized_hosts:
94  errors[CONF_HOSTS] = "invalid_hosts"
95  else:
96  user_input[CONF_HOSTS] = ",".join(normalized_hosts)
97 
98  normalized_exclude = _normalize_ips_and_network(user_input[CONF_EXCLUDE])
99  if normalized_exclude is None:
100  errors[CONF_EXCLUDE] = "invalid_hosts"
101  else:
102  user_input[CONF_EXCLUDE] = ",".join(normalized_exclude)
103 
104  return errors
105 
106 
108  hass: HomeAssistant, user_input: dict[str, Any], include_options: bool
109 ) -> vol.Schema:
110  hosts = user_input.get(CONF_HOSTS, await async_get_network(hass))
111  exclude = user_input.get(
112  CONF_EXCLUDE, await network.async_get_source_ip(hass, MDNS_TARGET_IP)
113  )
114  schema: VolDictType = {
115  vol.Required(CONF_HOSTS, default=hosts): str,
116  vol.Required(
117  CONF_HOME_INTERVAL, default=user_input.get(CONF_HOME_INTERVAL, 0)
118  ): int,
119  vol.Optional(CONF_EXCLUDE, default=exclude): str,
120  vol.Optional(
121  CONF_OPTIONS, default=user_input.get(CONF_OPTIONS, DEFAULT_OPTIONS)
122  ): str,
123  }
124  if include_options:
125  schema.update(
126  {
127  vol.Optional(
128  CONF_SCAN_INTERVAL,
129  default=user_input.get(CONF_SCAN_INTERVAL, TRACKER_SCAN_INTERVAL),
130  ): vol.All(vol.Coerce(int), vol.Range(min=10, max=MAX_SCAN_INTERVAL)),
131  vol.Optional(
132  CONF_CONSIDER_HOME,
133  default=user_input.get(CONF_CONSIDER_HOME)
134  or DEFAULT_CONSIDER_HOME.total_seconds(),
135  ): vol.All(vol.Coerce(int), vol.Range(min=1, max=MAX_CONSIDER_HOME)),
136  }
137  )
138  return vol.Schema(schema)
139 
140 
142  """Handle a option flow for homekit."""
143 
144  def __init__(self, config_entry: ConfigEntry) -> None:
145  """Initialize options flow."""
146  self.optionsoptions = dict(config_entry.options)
147 
148  async def async_step_init(
149  self, user_input: dict[str, Any] | None = None
150  ) -> ConfigFlowResult:
151  """Handle options flow."""
152  errors = {}
153  if user_input is not None:
154  errors = normalize_input(user_input)
155  self.optionsoptions.update(user_input)
156 
157  if not errors:
158  return self.async_create_entryasync_create_entry(
159  title=f"Nmap Tracker {self.options[CONF_HOSTS]}", data=self.optionsoptions
160  )
161 
162  return self.async_show_formasync_show_form(
163  step_id="init",
164  data_schema=await _async_build_schema_with_user_input(
165  self.hass, self.optionsoptions, True
166  ),
167  errors=errors,
168  )
169 
170 
171 class NmapTrackerConfigFlow(ConfigFlow, domain=DOMAIN):
172  """Handle a config flow for Nmap Tracker."""
173 
174  VERSION = 1
175 
176  def __init__(self) -> None:
177  """Initialize config flow."""
178  self.options: dict[str, Any] = {}
179 
180  async def async_step_user(
181  self, user_input: dict[str, Any] | None = None
182  ) -> ConfigFlowResult:
183  """Handle the initial step."""
184  errors = {}
185  if user_input is not None:
186  if not self._async_is_unique_host_list_async_is_unique_host_list(user_input):
187  return self.async_abortasync_abortasync_abort(reason="already_configured")
188 
189  errors = normalize_input(user_input)
190  self.options.update(user_input)
191 
192  if not errors:
193  return self.async_create_entryasync_create_entryasync_create_entry(
194  title=f"Nmap Tracker {user_input[CONF_HOSTS]}",
195  data={},
196  options=user_input,
197  )
198 
199  return self.async_show_formasync_show_formasync_show_form(
200  step_id="user",
201  data_schema=await _async_build_schema_with_user_input(
202  self.hass, self.options, False
203  ),
204  errors=errors,
205  )
206 
207  def _async_is_unique_host_list(self, user_input: dict[str, Any]) -> bool:
208  hosts = _normalize_ips_and_network(user_input[CONF_HOSTS])
209  for entry in self._async_current_entries_async_current_entries():
210  if _normalize_ips_and_network(entry.options[CONF_HOSTS]) == hosts:
211  return False
212  return True
213 
214  @staticmethod
215  @callback
216  def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler:
217  """Get the options flow for this handler."""
218  return OptionsFlowHandler(config_entry)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:182
OptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:216
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:150
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)
_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)
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
dict[str, str] normalize_input(dict[str, Any] user_input)
Definition: config_flow.py:89
vol.Schema _async_build_schema_with_user_input(HomeAssistant hass, dict[str, Any] user_input, bool include_options)
Definition: config_flow.py:109
list[str]|None _normalize_ips_and_network(str hosts_str)
Definition: config_flow.py:55