Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for KNX."""
2 
3 from __future__ import annotations
4 
5 from abc import ABC, abstractmethod
6 from collections.abc import AsyncGenerator
7 from typing import Any, Final
8 
9 import voluptuous as vol
10 from xknx import XKNX
11 from xknx.exceptions.exception import (
12  CommunicationError,
13  InvalidSecureConfiguration,
14  XKNXException,
15 )
16 from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
17 from xknx.io.gateway_scanner import GatewayDescriptor, GatewayScanner
18 from xknx.io.self_description import request_description
19 from xknx.io.util import validate_ip as xknx_validate_ip
20 from xknx.secure.keyring import Keyring, XMLInterface
21 
22 from homeassistant.config_entries import (
23  ConfigEntry,
24  ConfigEntryBaseFlow,
25  ConfigFlow,
26  ConfigFlowResult,
27  OptionsFlow,
28 )
29 from homeassistant.const import CONF_HOST, CONF_PORT
30 from homeassistant.core import callback
31 from homeassistant.helpers import selector
32 from homeassistant.helpers.typing import UNDEFINED, VolDictType
33 
34 from .const import (
35  CONF_KNX_AUTOMATIC,
36  CONF_KNX_CONNECTION_TYPE,
37  CONF_KNX_DEFAULT_RATE_LIMIT,
38  CONF_KNX_DEFAULT_STATE_UPDATER,
39  CONF_KNX_INDIVIDUAL_ADDRESS,
40  CONF_KNX_KNXKEY_PASSWORD,
41  CONF_KNX_LOCAL_IP,
42  CONF_KNX_MCAST_GRP,
43  CONF_KNX_MCAST_PORT,
44  CONF_KNX_RATE_LIMIT,
45  CONF_KNX_ROUTE_BACK,
46  CONF_KNX_ROUTING,
47  CONF_KNX_ROUTING_BACKBONE_KEY,
48  CONF_KNX_ROUTING_SECURE,
49  CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE,
50  CONF_KNX_SECURE_DEVICE_AUTHENTICATION,
51  CONF_KNX_SECURE_USER_ID,
52  CONF_KNX_SECURE_USER_PASSWORD,
53  CONF_KNX_STATE_UPDATER,
54  CONF_KNX_TELEGRAM_LOG_SIZE,
55  CONF_KNX_TUNNEL_ENDPOINT_IA,
56  CONF_KNX_TUNNELING,
57  CONF_KNX_TUNNELING_TCP,
58  CONF_KNX_TUNNELING_TCP_SECURE,
59  DEFAULT_ROUTING_IA,
60  DOMAIN,
61  KNX_MODULE_KEY,
62  TELEGRAM_LOG_DEFAULT,
63  TELEGRAM_LOG_MAX,
64  KNXConfigEntryData,
65 )
66 from .storage.keyring import DEFAULT_KNX_KEYRING_FILENAME, save_uploaded_knxkeys_file
67 from .validation import ia_validator, ip_v4_validator
68 
69 CONF_KNX_GATEWAY: Final = "gateway"
70 CONF_MAX_RATE_LIMIT: Final = 60
71 
72 DEFAULT_ENTRY_DATA = KNXConfigEntryData(
73  individual_address=DEFAULT_ROUTING_IA,
74  local_ip=None,
75  multicast_group=DEFAULT_MCAST_GRP,
76  multicast_port=DEFAULT_MCAST_PORT,
77  rate_limit=CONF_KNX_DEFAULT_RATE_LIMIT,
78  route_back=False,
79  state_updater=CONF_KNX_DEFAULT_STATE_UPDATER,
80  telegram_log_size=TELEGRAM_LOG_DEFAULT,
81 )
82 
83 CONF_KEYRING_FILE: Final = "knxkeys_file"
84 
85 CONF_KNX_TUNNELING_TYPE: Final = "tunneling_type"
86 CONF_KNX_TUNNELING_TYPE_LABELS: Final = {
87  CONF_KNX_TUNNELING: "UDP (Tunnelling v1)",
88  CONF_KNX_TUNNELING_TCP: "TCP (Tunnelling v2)",
89  CONF_KNX_TUNNELING_TCP_SECURE: "Secure Tunnelling (TCP)",
90 }
91 
92 OPTION_MANUAL_TUNNEL: Final = "Manual"
93 
94 _IA_SELECTOR = selector.TextSelector()
95 _IP_SELECTOR = selector.TextSelector()
96 _PORT_SELECTOR = vol.All(
97  selector.NumberSelector(
98  selector.NumberSelectorConfig(
99  min=1, max=65535, mode=selector.NumberSelectorMode.BOX
100  ),
101  ),
102  vol.Coerce(int),
103 )
104 
105 
107  """Base class for KNX flows."""
108 
109  def __init__(self, initial_data: KNXConfigEntryData) -> None:
110  """Initialize KNXCommonFlow."""
111  self.initial_datainitial_data = initial_data
112  self.new_entry_datanew_entry_data = KNXConfigEntryData()
113  self.new_titlenew_title: str | None = None
114 
115  self._keyring_keyring: Keyring | None = None
116  self._found_gateways_found_gateways: list[GatewayDescriptor] = []
117  self._found_tunnels_found_tunnels: list[GatewayDescriptor] = []
118  self._selected_tunnel_selected_tunnel: GatewayDescriptor | None = None
119  self._tunnel_endpoints_tunnel_endpoints: list[XMLInterface] = []
120 
121  self._gatewayscanner_gatewayscanner: GatewayScanner | None = None
122  self._async_scan_gen_async_scan_gen: AsyncGenerator[GatewayDescriptor] | None = None
123 
124  @abstractmethod
125  def finish_flow(self) -> ConfigFlowResult:
126  """Finish the flow."""
127 
128  @property
129  def connection_type(self) -> str:
130  """Return the configured connection type."""
131  _new_type = self.new_entry_datanew_entry_data.get(CONF_KNX_CONNECTION_TYPE)
132  if _new_type is None:
133  return self.initial_datainitial_data[CONF_KNX_CONNECTION_TYPE]
134  return _new_type
135 
136  @property
137  def tunnel_endpoint_ia(self) -> str | None:
138  """Return the configured tunnel endpoint individual address."""
139  return self.new_entry_datanew_entry_data.get(
140  CONF_KNX_TUNNEL_ENDPOINT_IA,
141  self.initial_datainitial_data.get(CONF_KNX_TUNNEL_ENDPOINT_IA),
142  )
143 
145  self, user_input: dict | None = None
146  ) -> ConfigFlowResult:
147  """Handle connection type configuration."""
148  if user_input is not None:
149  if self._async_scan_gen_async_scan_gen:
150  await self._async_scan_gen_async_scan_gen.aclose() # stop the scan
151  self._async_scan_gen_async_scan_gen = None
152  if self._gatewayscanner_gatewayscanner:
153  self._found_gateways_found_gateways = list(
154  self._gatewayscanner_gatewayscanner.found_gateways.values()
155  )
156  connection_type = user_input[CONF_KNX_CONNECTION_TYPE]
157  if connection_type == CONF_KNX_ROUTING:
158  return await self.async_step_routingasync_step_routing()
159 
160  if connection_type == CONF_KNX_TUNNELING:
161  self._found_tunnels_found_tunnels = [
162  gateway
163  for gateway in self._found_gateways_found_gateways
164  if gateway.supports_tunnelling
165  ]
166  self._found_tunnels_found_tunnels.sort(
167  key=lambda tunnel: tunnel.individual_address.raw
168  if tunnel.individual_address
169  else 0
170  )
171  return await self.async_step_tunnelasync_step_tunnel()
172 
173  # Automatic connection type
174  self.new_entry_datanew_entry_data = KNXConfigEntryData(
175  connection_type=CONF_KNX_AUTOMATIC,
176  tunnel_endpoint_ia=None,
177  )
178  self.new_titlenew_title = CONF_KNX_AUTOMATIC.capitalize()
179  return self.finish_flowfinish_flow()
180 
181  supported_connection_types = {
182  CONF_KNX_TUNNELING: CONF_KNX_TUNNELING.capitalize(),
183  CONF_KNX_ROUTING: CONF_KNX_ROUTING.capitalize(),
184  }
185 
186  if isinstance(self, OptionsFlow) and (
187  knx_module := self.hass.data.get(KNX_MODULE_KEY)
188  ):
189  xknx = knx_module.xknx
190  else:
191  xknx = XKNX()
192  self._gatewayscanner_gatewayscanner = GatewayScanner(
193  xknx, stop_on_found=0, timeout_in_seconds=2
194  )
195  # keep a reference to the generator to scan in background until user selects a connection type
196  self._async_scan_gen_async_scan_gen = self._gatewayscanner_gatewayscanner.async_scan()
197  try:
198  await anext(self._async_scan_gen_async_scan_gen)
199  except StopAsyncIteration:
200  pass # scan finished, no interfaces discovered
201  else:
202  # add automatic at first position only if a gateway responded
203  supported_connection_types = {
204  CONF_KNX_AUTOMATIC: CONF_KNX_AUTOMATIC.capitalize()
205  } | supported_connection_types
206 
207  fields = {
208  vol.Required(CONF_KNX_CONNECTION_TYPE): vol.In(supported_connection_types)
209  }
210  return self.async_show_formasync_show_form(
211  step_id="connection_type", data_schema=vol.Schema(fields)
212  )
213 
214  async def async_step_tunnel(
215  self, user_input: dict | None = None
216  ) -> ConfigFlowResult:
217  """Select a tunnel from a list.
218 
219  Will be skipped if the gateway scan was unsuccessful
220  or if only one gateway was found.
221  """
222  if user_input is not None:
223  if user_input[CONF_KNX_GATEWAY] == OPTION_MANUAL_TUNNEL:
224  if self._found_tunnels_found_tunnels:
225  self._selected_tunnel_selected_tunnel = self._found_tunnels_found_tunnels[0]
226  return await self.async_step_manual_tunnelasync_step_manual_tunnel()
227 
228  self._selected_tunnel_selected_tunnel = next(
229  tunnel
230  for tunnel in self._found_tunnels_found_tunnels
231  if user_input[CONF_KNX_GATEWAY] == str(tunnel)
232  )
233  connection_type = (
234  CONF_KNX_TUNNELING_TCP_SECURE
235  if self._selected_tunnel_selected_tunnel.tunnelling_requires_secure
236  else CONF_KNX_TUNNELING_TCP
237  if self._selected_tunnel_selected_tunnel.supports_tunnelling_tcp
238  else CONF_KNX_TUNNELING
239  )
240  self.new_entry_datanew_entry_data = KNXConfigEntryData(
241  host=self._selected_tunnel_selected_tunnel.ip_addr,
242  port=self._selected_tunnel_selected_tunnel.port,
243  route_back=False,
244  connection_type=connection_type,
245  device_authentication=None,
246  user_id=None,
247  user_password=None,
248  tunnel_endpoint_ia=None,
249  )
250  if connection_type == CONF_KNX_TUNNELING_TCP_SECURE:
251  return await self.async_step_secure_key_source_menu_tunnelasync_step_secure_key_source_menu_tunnel()
252  self.new_titlenew_title = f"Tunneling @ {self._selected_tunnel}"
253  return self.finish_flowfinish_flow()
254 
255  if not self._found_tunnels_found_tunnels:
256  return await self.async_step_manual_tunnelasync_step_manual_tunnel()
257 
258  errors: dict = {}
259  tunnel_options = {
260  str(tunnel): f"{tunnel}{' 🔐' if tunnel.tunnelling_requires_secure else ''}"
261  for tunnel in self._found_tunnels_found_tunnels
262  }
263  tunnel_options |= {OPTION_MANUAL_TUNNEL: OPTION_MANUAL_TUNNEL}
264  fields = {vol.Required(CONF_KNX_GATEWAY): vol.In(tunnel_options)}
265 
266  return self.async_show_formasync_show_form(
267  step_id="tunnel", data_schema=vol.Schema(fields), errors=errors
268  )
269 
271  self, user_input: dict | None = None
272  ) -> ConfigFlowResult:
273  """Manually configure tunnel connection parameters. Fields default to preselected gateway if one was found."""
274  errors: dict = {}
275 
276  if user_input is not None:
277  try:
278  _host = user_input[CONF_HOST]
279  _host_ip = await xknx_validate_ip(_host)
280  ip_v4_validator(_host_ip, multicast=False)
281  except (vol.Invalid, XKNXException):
282  errors[CONF_HOST] = "invalid_ip_address"
283 
284  _local_ip = None
285  if _local := user_input.get(CONF_KNX_LOCAL_IP):
286  try:
287  _local_ip = await xknx_validate_ip(_local)
288  ip_v4_validator(_local_ip, multicast=False)
289  except (vol.Invalid, XKNXException):
290  errors[CONF_KNX_LOCAL_IP] = "invalid_ip_address"
291 
292  selected_tunnelling_type = user_input[CONF_KNX_TUNNELING_TYPE]
293  if not errors:
294  try:
295  self._selected_tunnel_selected_tunnel = await request_description(
296  gateway_ip=_host_ip,
297  gateway_port=user_input[CONF_PORT],
298  local_ip=_local_ip,
299  route_back=user_input[CONF_KNX_ROUTE_BACK],
300  )
301  except CommunicationError:
302  errors["base"] = "cannot_connect"
303  else:
304  if bool(self._selected_tunnel_selected_tunnel.tunnelling_requires_secure) is not (
305  selected_tunnelling_type == CONF_KNX_TUNNELING_TCP_SECURE
306  ) or (
307  selected_tunnelling_type == CONF_KNX_TUNNELING_TCP
308  and not self._selected_tunnel_selected_tunnel.supports_tunnelling_tcp
309  ):
310  errors[CONF_KNX_TUNNELING_TYPE] = "unsupported_tunnel_type"
311 
312  if not errors:
313  self.new_entry_datanew_entry_data = KNXConfigEntryData(
314  connection_type=selected_tunnelling_type,
315  host=_host,
316  port=user_input[CONF_PORT],
317  route_back=user_input[CONF_KNX_ROUTE_BACK],
318  local_ip=_local,
319  device_authentication=None,
320  user_id=None,
321  user_password=None,
322  tunnel_endpoint_ia=None,
323  )
324 
325  if selected_tunnelling_type == CONF_KNX_TUNNELING_TCP_SECURE:
326  return await self.async_step_secure_key_source_menu_tunnelasync_step_secure_key_source_menu_tunnel()
327  self.new_titlenew_title = (
328  "Tunneling "
329  f"{'UDP' if selected_tunnelling_type == CONF_KNX_TUNNELING else 'TCP'} "
330  f"@ {_host}"
331  )
332  return self.finish_flowfinish_flow()
333 
334  _reconfiguring_existing_tunnel = (
335  self.initial_datainitial_data.get(CONF_KNX_CONNECTION_TYPE)
336  in CONF_KNX_TUNNELING_TYPE_LABELS
337  )
338  ip_address: str | None
339  if ( # initial attempt on ConfigFlow or coming from automatic / routing
340  (isinstance(self, ConfigFlow) or not _reconfiguring_existing_tunnel)
341  and not user_input
342  and self._selected_tunnel_selected_tunnel is not None
343  ): # default to first found tunnel
344  ip_address = self._selected_tunnel_selected_tunnel.ip_addr
345  port = self._selected_tunnel_selected_tunnel.port
346  if self._selected_tunnel_selected_tunnel.tunnelling_requires_secure:
347  default_type = CONF_KNX_TUNNELING_TCP_SECURE
348  elif self._selected_tunnel_selected_tunnel.supports_tunnelling_tcp:
349  default_type = CONF_KNX_TUNNELING_TCP
350  else:
351  default_type = CONF_KNX_TUNNELING
352  else: # OptionFlow, no tunnel discovered or user input
353  ip_address = (
354  user_input[CONF_HOST]
355  if user_input
356  else self.initial_datainitial_data.get(CONF_HOST)
357  )
358  port = (
359  user_input[CONF_PORT]
360  if user_input
361  else self.initial_datainitial_data.get(CONF_PORT, DEFAULT_MCAST_PORT)
362  )
363  default_type = (
364  user_input[CONF_KNX_TUNNELING_TYPE]
365  if user_input
366  else self.initial_datainitial_data[CONF_KNX_CONNECTION_TYPE]
367  if _reconfiguring_existing_tunnel
368  else CONF_KNX_TUNNELING
369  )
370  _route_back: bool = self.initial_datainitial_data.get(
371  CONF_KNX_ROUTE_BACK, not bool(self._selected_tunnel_selected_tunnel)
372  )
373 
374  fields: VolDictType = {
375  vol.Required(CONF_KNX_TUNNELING_TYPE, default=default_type): vol.In(
376  CONF_KNX_TUNNELING_TYPE_LABELS
377  ),
378  vol.Required(CONF_HOST, default=ip_address): _IP_SELECTOR,
379  vol.Required(CONF_PORT, default=port): _PORT_SELECTOR,
380  vol.Required(
381  CONF_KNX_ROUTE_BACK, default=_route_back
382  ): selector.BooleanSelector(),
383  }
384  if self.show_advanced_optionsshow_advanced_options:
385  fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR
386 
387  if not self._found_tunnels_found_tunnels and not errors.get("base"):
388  errors["base"] = "no_tunnel_discovered"
389  return self.async_show_formasync_show_form(
390  step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors
391  )
392 
394  self, user_input: dict | None = None
395  ) -> ConfigFlowResult:
396  """Configure ip secure tunnelling manually."""
397  errors: dict = {}
398 
399  if user_input is not None:
400  self.new_entry_datanew_entry_data |= KNXConfigEntryData(
401  device_authentication=user_input[CONF_KNX_SECURE_DEVICE_AUTHENTICATION],
402  user_id=user_input[CONF_KNX_SECURE_USER_ID],
403  user_password=user_input[CONF_KNX_SECURE_USER_PASSWORD],
404  tunnel_endpoint_ia=None,
405  )
406  self.new_titlenew_title = f"Secure Tunneling @ {self.new_entry_data[CONF_HOST]}"
407  return self.finish_flowfinish_flow()
408 
409  fields = {
410  vol.Required(
411  CONF_KNX_SECURE_USER_ID,
412  default=self.initial_datainitial_data.get(CONF_KNX_SECURE_USER_ID, 2),
413  ): vol.All(
414  selector.NumberSelector(
415  selector.NumberSelectorConfig(
416  min=1, max=127, mode=selector.NumberSelectorMode.BOX
417  ),
418  ),
419  vol.Coerce(int),
420  ),
421  vol.Required(
422  CONF_KNX_SECURE_USER_PASSWORD,
423  default=self.initial_datainitial_data.get(CONF_KNX_SECURE_USER_PASSWORD),
424  ): selector.TextSelector(
425  selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD),
426  ),
427  vol.Required(
428  CONF_KNX_SECURE_DEVICE_AUTHENTICATION,
429  default=self.initial_datainitial_data.get(CONF_KNX_SECURE_DEVICE_AUTHENTICATION),
430  ): selector.TextSelector(
431  selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD),
432  ),
433  }
434 
435  return self.async_show_formasync_show_form(
436  step_id="secure_tunnel_manual",
437  data_schema=vol.Schema(fields),
438  errors=errors,
439  )
440 
442  self, user_input: dict | None = None
443  ) -> ConfigFlowResult:
444  """Configure ip secure routing manually."""
445  errors: dict = {}
446 
447  if user_input is not None:
448  try:
449  key_bytes = bytes.fromhex(user_input[CONF_KNX_ROUTING_BACKBONE_KEY])
450  if len(key_bytes) != 16:
451  raise ValueError # noqa: TRY301
452  except ValueError:
453  errors[CONF_KNX_ROUTING_BACKBONE_KEY] = "invalid_backbone_key"
454  if not errors:
455  self.new_entry_datanew_entry_data |= KNXConfigEntryData(
456  backbone_key=user_input[CONF_KNX_ROUTING_BACKBONE_KEY],
457  sync_latency_tolerance=user_input[
458  CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE
459  ],
460  )
461  self.new_titlenew_title = f"Secure Routing as {self.new_entry_data[CONF_KNX_INDIVIDUAL_ADDRESS]}"
462  return self.finish_flowfinish_flow()
463 
464  fields = {
465  vol.Required(
466  CONF_KNX_ROUTING_BACKBONE_KEY,
467  default=self.initial_datainitial_data.get(CONF_KNX_ROUTING_BACKBONE_KEY),
468  ): selector.TextSelector(
469  selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD),
470  ),
471  vol.Required(
472  CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE,
473  default=self.initial_datainitial_data.get(CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE)
474  or 1000,
475  ): vol.All(
476  selector.NumberSelector(
477  selector.NumberSelectorConfig(
478  min=400,
479  max=4000,
480  unit_of_measurement="ms",
481  mode=selector.NumberSelectorMode.BOX,
482  ),
483  ),
484  vol.Coerce(int),
485  ),
486  }
487 
488  return self.async_show_formasync_show_form(
489  step_id="secure_routing_manual",
490  data_schema=vol.Schema(fields),
491  errors=errors,
492  )
493 
495  self, user_input: dict[str, Any] | None = None
496  ) -> ConfigFlowResult:
497  """Manage upload of new KNX Keyring file."""
498  errors: dict[str, str] = {}
499 
500  if user_input is not None:
501  password = user_input[CONF_KNX_KNXKEY_PASSWORD]
502  try:
503  self._keyring_keyring = await save_uploaded_knxkeys_file(
504  self.hass,
505  uploaded_file_id=user_input[CONF_KEYRING_FILE],
506  password=password,
507  )
508  except InvalidSecureConfiguration:
509  errors[CONF_KNX_KNXKEY_PASSWORD] = "keyfile_invalid_signature"
510 
511  if not errors and self._keyring_keyring:
512  self.new_entry_datanew_entry_data |= KNXConfigEntryData(
513  knxkeys_filename=f"{DOMAIN}/{DEFAULT_KNX_KEYRING_FILENAME}",
514  knxkeys_password=password,
515  backbone_key=None,
516  sync_latency_tolerance=None,
517  )
518  # Routing
519  if self.connection_typeconnection_type in (CONF_KNX_ROUTING, CONF_KNX_ROUTING_SECURE):
520  return self.finish_flowfinish_flow()
521 
522  # Tunneling / Automatic
523  # skip selection step if we have a keyfile update that includes a configured tunnel
524  if self.tunnel_endpoint_iatunnel_endpoint_ia is not None and self.tunnel_endpoint_iatunnel_endpoint_ia in [
525  str(_if.individual_address) for _if in self._keyring_keyring.interfaces
526  ]:
527  return self.finish_flowfinish_flow()
528  if not errors:
529  return await self.async_step_knxkeys_tunnel_selectasync_step_knxkeys_tunnel_select()
530 
531  fields = {
532  vol.Required(CONF_KEYRING_FILE): selector.FileSelector(
533  config=selector.FileSelectorConfig(accept=".knxkeys")
534  ),
535  vol.Required(
536  CONF_KNX_KNXKEY_PASSWORD,
537  default=self.initial_datainitial_data.get(CONF_KNX_KNXKEY_PASSWORD),
538  ): selector.TextSelector(),
539  }
540  return self.async_show_formasync_show_form(
541  step_id="secure_knxkeys",
542  data_schema=vol.Schema(fields),
543  errors=errors,
544  )
545 
547  self, user_input: dict | None = None
548  ) -> ConfigFlowResult:
549  """Select if a specific tunnel should be used from knxkeys file."""
550  errors = {}
551  description_placeholders = {}
552  if user_input is not None:
553  selected_tunnel_ia: str | None = None
554  _if_user_id: int | None = None
555  if user_input[CONF_KNX_TUNNEL_ENDPOINT_IA] == CONF_KNX_AUTOMATIC:
556  self.new_entry_datanew_entry_data |= KNXConfigEntryData(
557  tunnel_endpoint_ia=None,
558  )
559  else:
560  selected_tunnel_ia = user_input[CONF_KNX_TUNNEL_ENDPOINT_IA]
561  self.new_entry_datanew_entry_data |= KNXConfigEntryData(
562  tunnel_endpoint_ia=selected_tunnel_ia,
563  user_id=None,
564  user_password=None,
565  device_authentication=None,
566  )
567  _if_user_id = next(
568  (
569  _if.user_id
570  for _if in self._tunnel_endpoints_tunnel_endpoints
571  if str(_if.individual_address) == selected_tunnel_ia
572  ),
573  None,
574  )
575  _tunnel_identifier = selected_tunnel_ia or self.new_entry_datanew_entry_data.get(
576  CONF_HOST
577  )
578  _tunnel_suffix = f" @ {_tunnel_identifier}" if _tunnel_identifier else ""
579  self.new_titlenew_title = (
580  f"{'Secure ' if _if_user_id else ''}Tunneling{_tunnel_suffix}"
581  )
582  return self.finish_flowfinish_flow()
583 
584  # this step is only called from async_step_secure_knxkeys so self._keyring is always set
585  assert self._keyring_keyring
586 
587  # Filter for selected tunnel
588  if self._selected_tunnel_selected_tunnel is not None:
589  if host_ia := self._selected_tunnel_selected_tunnel.individual_address:
590  self._tunnel_endpoints_tunnel_endpoints = self._keyring_keyring.get_tunnel_interfaces_by_host(
591  host=host_ia
592  )
593  if not self._tunnel_endpoints_tunnel_endpoints:
594  errors["base"] = "keyfile_no_tunnel_for_host"
595  description_placeholders = {CONF_HOST: str(host_ia)}
596  else:
597  self._tunnel_endpoints_tunnel_endpoints = self._keyring_keyring.interfaces
598 
599  tunnel_endpoint_options = [
600  selector.SelectOptionDict(
601  value=CONF_KNX_AUTOMATIC, label=CONF_KNX_AUTOMATIC.capitalize()
602  )
603  ]
604  tunnel_endpoint_options.extend(
605  selector.SelectOptionDict(
606  value=str(endpoint.individual_address),
607  label=(
608  f"{endpoint.individual_address} "
609  f"{'🔐 ' if endpoint.user_id else ''}"
610  f"(Data Secure GAs: {len(endpoint.group_addresses)})"
611  ),
612  )
613  for endpoint in self._tunnel_endpoints_tunnel_endpoints
614  )
615  return self.async_show_formasync_show_form(
616  step_id="knxkeys_tunnel_select",
617  data_schema=vol.Schema(
618  {
619  vol.Required(
620  CONF_KNX_TUNNEL_ENDPOINT_IA, default=CONF_KNX_AUTOMATIC
621  ): selector.SelectSelector(
622  selector.SelectSelectorConfig(
623  options=tunnel_endpoint_options,
624  mode=selector.SelectSelectorMode.LIST,
625  )
626  ),
627  }
628  ),
629  errors=errors,
630  description_placeholders=description_placeholders,
631  )
632 
634  self, user_input: dict | None = None
635  ) -> ConfigFlowResult:
636  """Routing setup."""
637  errors: dict = {}
638  _individual_address = (
639  user_input[CONF_KNX_INDIVIDUAL_ADDRESS]
640  if user_input
641  else self.initial_datainitial_data[CONF_KNX_INDIVIDUAL_ADDRESS]
642  )
643  _multicast_group = (
644  user_input[CONF_KNX_MCAST_GRP]
645  if user_input
646  else self.initial_datainitial_data[CONF_KNX_MCAST_GRP]
647  )
648  _multicast_port = (
649  user_input[CONF_KNX_MCAST_PORT]
650  if user_input
651  else self.initial_datainitial_data[CONF_KNX_MCAST_PORT]
652  )
653 
654  if user_input is not None:
655  try:
656  ia_validator(_individual_address)
657  except vol.Invalid:
658  errors[CONF_KNX_INDIVIDUAL_ADDRESS] = "invalid_individual_address"
659  try:
660  ip_v4_validator(_multicast_group, multicast=True)
661  except vol.Invalid:
662  errors[CONF_KNX_MCAST_GRP] = "invalid_ip_address"
663  if _local := user_input.get(CONF_KNX_LOCAL_IP):
664  try:
665  _local_ip = await xknx_validate_ip(_local)
666  ip_v4_validator(_local_ip, multicast=False)
667  except (vol.Invalid, XKNXException):
668  errors[CONF_KNX_LOCAL_IP] = "invalid_ip_address"
669 
670  if not errors:
671  connection_type = (
672  CONF_KNX_ROUTING_SECURE
673  if user_input[CONF_KNX_ROUTING_SECURE]
674  else CONF_KNX_ROUTING
675  )
676  self.new_entry_datanew_entry_data = KNXConfigEntryData(
677  connection_type=connection_type,
678  individual_address=_individual_address,
679  multicast_group=_multicast_group,
680  multicast_port=_multicast_port,
681  local_ip=_local,
682  device_authentication=None,
683  user_id=None,
684  user_password=None,
685  tunnel_endpoint_ia=None,
686  )
687  if connection_type == CONF_KNX_ROUTING_SECURE:
688  self.new_titlenew_title = f"Secure Routing as {_individual_address}"
689  return await self.async_step_secure_key_source_menu_routingasync_step_secure_key_source_menu_routing()
690  self.new_titlenew_title = f"Routing as {_individual_address}"
691  return self.finish_flowfinish_flow()
692 
693  routers = [router for router in self._found_gateways_found_gateways if router.supports_routing]
694  if not routers:
695  errors["base"] = "no_router_discovered"
696  default_secure_routing_enable = any(
697  router for router in routers if router.routing_requires_secure
698  )
699 
700  fields: VolDictType = {
701  vol.Required(
702  CONF_KNX_INDIVIDUAL_ADDRESS, default=_individual_address
703  ): _IA_SELECTOR,
704  vol.Required(
705  CONF_KNX_ROUTING_SECURE, default=default_secure_routing_enable
706  ): selector.BooleanSelector(),
707  vol.Required(CONF_KNX_MCAST_GRP, default=_multicast_group): _IP_SELECTOR,
708  vol.Required(CONF_KNX_MCAST_PORT, default=_multicast_port): _PORT_SELECTOR,
709  }
710  if self.show_advanced_optionsshow_advanced_options:
711  # Optional with default doesn't work properly in flow UI
712  fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR
713 
714  return self.async_show_formasync_show_form(
715  step_id="routing", data_schema=vol.Schema(fields), errors=errors
716  )
717 
719  self, user_input: dict | None = None
720  ) -> ConfigFlowResult:
721  """Show the key source menu."""
722  return self.async_show_menuasync_show_menu(
723  step_id="secure_key_source_menu_tunnel",
724  menu_options=["secure_knxkeys", "secure_tunnel_manual"],
725  )
726 
728  self, user_input: dict | None = None
729  ) -> ConfigFlowResult:
730  """Show the key source menu."""
731  return self.async_show_menuasync_show_menu(
732  step_id="secure_key_source_menu_routing",
733  menu_options=["secure_knxkeys", "secure_routing_manual"],
734  )
735 
736 
737 class KNXConfigFlow(KNXCommonFlow, ConfigFlow, domain=DOMAIN):
738  """Handle a KNX config flow."""
739 
740  VERSION = 1
741 
742  def __init__(self) -> None:
743  """Initialize KNX options flow."""
744  super().__init__(initial_data=DEFAULT_ENTRY_DATA)
745 
746  @staticmethod
747  @callback
748  def async_get_options_flow(config_entry: ConfigEntry) -> KNXOptionsFlow:
749  """Get the options flow for this handler."""
750  return KNXOptionsFlow(config_entry)
751 
752  @callback
753  def finish_flow(self) -> ConfigFlowResult:
754  """Create the ConfigEntry."""
755  title = self.new_titlenew_title or f"KNX {self.new_entry_data[CONF_KNX_CONNECTION_TYPE]}"
756  return self.async_create_entryasync_create_entryasync_create_entry(
757  title=title,
758  data=DEFAULT_ENTRY_DATA | self.new_entry_datanew_entry_data,
759  )
760 
761  async def async_step_user(self, user_input: dict | None = None) -> ConfigFlowResult:
762  """Handle a flow initialized by the user."""
763  return await self.async_step_connection_typeasync_step_connection_type()
764 
765 
767  """Handle KNX options."""
768 
769  general_settings: dict
770 
771  def __init__(self, config_entry: ConfigEntry) -> None:
772  """Initialize KNX options flow."""
773  super().__init__(initial_data=config_entry.data) # type: ignore[arg-type]
774 
775  @callback
776  def finish_flow(self) -> ConfigFlowResult:
777  """Update the ConfigEntry and finish the flow."""
778  new_data = DEFAULT_ENTRY_DATA | self.initial_datainitial_data | self.new_entry_datanew_entry_datanew_entry_data
779  self.hass.config_entries.async_update_entry(
780  self.config_entryconfig_entryconfig_entry,
781  data=new_data,
782  title=self.new_titlenew_title or UNDEFINED,
783  )
784  return self.async_create_entryasync_create_entry(title="", data={})
785 
786  async def async_step_init(
787  self, user_input: dict[str, Any] | None = None
788  ) -> ConfigFlowResult:
789  """Manage KNX options."""
790  return self.async_show_menuasync_show_menu(
791  step_id="init",
792  menu_options=[
793  "connection_type",
794  "communication_settings",
795  "secure_knxkeys",
796  ],
797  )
798 
800  self, user_input: dict[str, Any] | None = None
801  ) -> ConfigFlowResult:
802  """Manage KNX communication settings."""
803  if user_input is not None:
805  state_updater=user_input[CONF_KNX_STATE_UPDATER],
806  rate_limit=user_input[CONF_KNX_RATE_LIMIT],
807  telegram_log_size=user_input[CONF_KNX_TELEGRAM_LOG_SIZE],
808  )
809  return self.finish_flowfinish_flowfinish_flow()
810 
811  data_schema = {
812  vol.Required(
813  CONF_KNX_STATE_UPDATER,
814  default=self.initial_datainitial_data.get(
815  CONF_KNX_STATE_UPDATER, CONF_KNX_DEFAULT_STATE_UPDATER
816  ),
817  ): selector.BooleanSelector(),
818  vol.Required(
819  CONF_KNX_RATE_LIMIT,
820  default=self.initial_datainitial_data.get(
821  CONF_KNX_RATE_LIMIT, CONF_KNX_DEFAULT_RATE_LIMIT
822  ),
823  ): vol.All(
824  selector.NumberSelector(
825  selector.NumberSelectorConfig(
826  min=0,
827  max=CONF_MAX_RATE_LIMIT,
828  mode=selector.NumberSelectorMode.BOX,
829  ),
830  ),
831  vol.Coerce(int),
832  ),
833  vol.Required(
834  CONF_KNX_TELEGRAM_LOG_SIZE,
835  default=self.initial_datainitial_data.get(
836  CONF_KNX_TELEGRAM_LOG_SIZE, TELEGRAM_LOG_DEFAULT
837  ),
838  ): vol.All(
839  selector.NumberSelector(
840  selector.NumberSelectorConfig(
841  min=0,
842  max=TELEGRAM_LOG_MAX,
843  mode=selector.NumberSelectorMode.BOX,
844  ),
845  ),
846  vol.Coerce(int),
847  ),
848  }
849  return self.async_show_formasync_show_form(
850  step_id="communication_settings",
851  data_schema=vol.Schema(data_schema),
852  last_step=True,
853  description_placeholders={
854  "telegram_log_size_max": f"{TELEGRAM_LOG_MAX}",
855  },
856  )
ConfigFlowResult async_step_secure_key_source_menu_routing(self, dict|None user_input=None)
Definition: config_flow.py:729
None __init__(self, KNXConfigEntryData initial_data)
Definition: config_flow.py:109
ConfigFlowResult async_step_routing(self, dict|None user_input=None)
Definition: config_flow.py:635
ConfigFlowResult async_step_secure_knxkeys(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:496
ConfigFlowResult async_step_knxkeys_tunnel_select(self, dict|None user_input=None)
Definition: config_flow.py:548
ConfigFlowResult async_step_tunnel(self, dict|None user_input=None)
Definition: config_flow.py:216
ConfigFlowResult async_step_connection_type(self, dict|None user_input=None)
Definition: config_flow.py:146
ConfigFlowResult async_step_secure_key_source_menu_tunnel(self, dict|None user_input=None)
Definition: config_flow.py:720
ConfigFlowResult async_step_secure_tunnel_manual(self, dict|None user_input=None)
Definition: config_flow.py:395
ConfigFlowResult async_step_manual_tunnel(self, dict|None user_input=None)
Definition: config_flow.py:272
ConfigFlowResult async_step_secure_routing_manual(self, dict|None user_input=None)
Definition: config_flow.py:443
ConfigFlowResult async_step_user(self, dict|None user_input=None)
Definition: config_flow.py:761
KNXOptionsFlow async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:748
None __init__(self, ConfigEntry config_entry)
Definition: config_flow.py:771
ConfigFlowResult async_step_communication_settings(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:801
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:788
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)
None config_entry(self, ConfigEntry value)
bool show_advanced_options(self)
str
_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_show_menu(self, *str|None step_id=None, Container[str] menu_options, Mapping[str, str]|None description_placeholders=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)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
Keyring save_uploaded_knxkeys_file(HomeAssistant hass, str uploaded_file_id, str password)
Definition: keyring.py:25
str ip_v4_validator(Any value, bool|None multicast=None)
Definition: validation.py:86