1 """Config flow for KNX."""
3 from __future__
import annotations
5 from abc
import ABC, abstractmethod
6 from collections.abc
import AsyncGenerator
7 from typing
import Any, Final
9 import voluptuous
as vol
11 from xknx.exceptions.exception
import (
13 InvalidSecureConfiguration,
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
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,
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,
57 CONF_KNX_TUNNELING_TCP,
58 CONF_KNX_TUNNELING_TCP_SECURE,
66 from .storage.keyring
import DEFAULT_KNX_KEYRING_FILENAME, save_uploaded_knxkeys_file
67 from .validation
import ia_validator, ip_v4_validator
69 CONF_KNX_GATEWAY: Final =
"gateway"
70 CONF_MAX_RATE_LIMIT: Final = 60
73 individual_address=DEFAULT_ROUTING_IA,
75 multicast_group=DEFAULT_MCAST_GRP,
76 multicast_port=DEFAULT_MCAST_PORT,
77 rate_limit=CONF_KNX_DEFAULT_RATE_LIMIT,
79 state_updater=CONF_KNX_DEFAULT_STATE_UPDATER,
80 telegram_log_size=TELEGRAM_LOG_DEFAULT,
83 CONF_KEYRING_FILE: Final =
"knxkeys_file"
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)",
92 OPTION_MANUAL_TUNNEL: Final =
"Manual"
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
107 """Base class for KNX flows."""
109 def __init__(self, initial_data: KNXConfigEntryData) ->
None:
110 """Initialize KNXCommonFlow."""
113 self.
new_titlenew_title: str |
None =
None
115 self.
_keyring_keyring: Keyring |
None =
None
122 self.
_async_scan_gen_async_scan_gen: AsyncGenerator[GatewayDescriptor] |
None =
None
126 """Finish the flow."""
130 """Return the configured connection type."""
132 if _new_type
is None:
133 return self.
initial_datainitial_data[CONF_KNX_CONNECTION_TYPE]
138 """Return the configured tunnel endpoint individual address."""
140 CONF_KNX_TUNNEL_ENDPOINT_IA,
145 self, user_input: dict |
None =
None
146 ) -> ConfigFlowResult:
147 """Handle connection type configuration."""
148 if user_input
is not None:
156 connection_type = user_input[CONF_KNX_CONNECTION_TYPE]
157 if connection_type == CONF_KNX_ROUTING:
160 if connection_type == CONF_KNX_TUNNELING:
164 if gateway.supports_tunnelling
167 key=
lambda tunnel: tunnel.individual_address.raw
168 if tunnel.individual_address
175 connection_type=CONF_KNX_AUTOMATIC,
176 tunnel_endpoint_ia=
None,
178 self.
new_titlenew_title = CONF_KNX_AUTOMATIC.capitalize()
181 supported_connection_types = {
182 CONF_KNX_TUNNELING: CONF_KNX_TUNNELING.capitalize(),
183 CONF_KNX_ROUTING: CONF_KNX_ROUTING.capitalize(),
186 if isinstance(self, OptionsFlow)
and (
187 knx_module := self.hass.data.get(KNX_MODULE_KEY)
189 xknx = knx_module.xknx
193 xknx, stop_on_found=0, timeout_in_seconds=2
199 except StopAsyncIteration:
203 supported_connection_types = {
204 CONF_KNX_AUTOMATIC: CONF_KNX_AUTOMATIC.capitalize()
205 } | supported_connection_types
208 vol.Required(CONF_KNX_CONNECTION_TYPE): vol.In(supported_connection_types)
211 step_id=
"connection_type", data_schema=vol.Schema(fields)
215 self, user_input: dict |
None =
None
216 ) -> ConfigFlowResult:
217 """Select a tunnel from a list.
219 Will be skipped if the gateway scan was unsuccessful
220 or if only one gateway was found.
222 if user_input
is not None:
223 if user_input[CONF_KNX_GATEWAY] == OPTION_MANUAL_TUNNEL:
231 if user_input[CONF_KNX_GATEWAY] ==
str(tunnel)
234 CONF_KNX_TUNNELING_TCP_SECURE
236 else CONF_KNX_TUNNELING_TCP
238 else CONF_KNX_TUNNELING
244 connection_type=connection_type,
245 device_authentication=
None,
248 tunnel_endpoint_ia=
None,
250 if connection_type == CONF_KNX_TUNNELING_TCP_SECURE:
252 self.
new_titlenew_title = f
"Tunneling @ {self._selected_tunnel}"
260 str(tunnel): f
"{tunnel}{' 🔐' if tunnel.tunnelling_requires_secure else ''}"
263 tunnel_options |= {OPTION_MANUAL_TUNNEL: OPTION_MANUAL_TUNNEL}
264 fields = {vol.Required(CONF_KNX_GATEWAY): vol.In(tunnel_options)}
267 step_id=
"tunnel", data_schema=vol.Schema(fields), errors=errors
271 self, user_input: dict |
None =
None
272 ) -> ConfigFlowResult:
273 """Manually configure tunnel connection parameters. Fields default to preselected gateway if one was found."""
276 if user_input
is not None:
278 _host = user_input[CONF_HOST]
279 _host_ip = await xknx_validate_ip(_host)
281 except (vol.Invalid, XKNXException):
282 errors[CONF_HOST] =
"invalid_ip_address"
285 if _local := user_input.get(CONF_KNX_LOCAL_IP):
287 _local_ip = await xknx_validate_ip(_local)
289 except (vol.Invalid, XKNXException):
290 errors[CONF_KNX_LOCAL_IP] =
"invalid_ip_address"
292 selected_tunnelling_type = user_input[CONF_KNX_TUNNELING_TYPE]
297 gateway_port=user_input[CONF_PORT],
299 route_back=user_input[CONF_KNX_ROUTE_BACK],
301 except CommunicationError:
302 errors[
"base"] =
"cannot_connect"
304 if bool(self.
_selected_tunnel_selected_tunnel.tunnelling_requires_secure)
is not (
305 selected_tunnelling_type == CONF_KNX_TUNNELING_TCP_SECURE
307 selected_tunnelling_type == CONF_KNX_TUNNELING_TCP
310 errors[CONF_KNX_TUNNELING_TYPE] =
"unsupported_tunnel_type"
314 connection_type=selected_tunnelling_type,
316 port=user_input[CONF_PORT],
317 route_back=user_input[CONF_KNX_ROUTE_BACK],
319 device_authentication=
None,
322 tunnel_endpoint_ia=
None,
325 if selected_tunnelling_type == CONF_KNX_TUNNELING_TCP_SECURE:
329 f
"{'UDP' if selected_tunnelling_type == CONF_KNX_TUNNELING else 'TCP'} "
334 _reconfiguring_existing_tunnel = (
336 in CONF_KNX_TUNNELING_TYPE_LABELS
338 ip_address: str |
None
340 (isinstance(self, ConfigFlow)
or not _reconfiguring_existing_tunnel)
347 default_type = CONF_KNX_TUNNELING_TCP_SECURE
349 default_type = CONF_KNX_TUNNELING_TCP
351 default_type = CONF_KNX_TUNNELING
354 user_input[CONF_HOST]
359 user_input[CONF_PORT]
364 user_input[CONF_KNX_TUNNELING_TYPE]
366 else self.
initial_datainitial_data[CONF_KNX_CONNECTION_TYPE]
367 if _reconfiguring_existing_tunnel
368 else CONF_KNX_TUNNELING
374 fields: VolDictType = {
375 vol.Required(CONF_KNX_TUNNELING_TYPE, default=default_type): vol.In(
376 CONF_KNX_TUNNELING_TYPE_LABELS
378 vol.Required(CONF_HOST, default=ip_address): _IP_SELECTOR,
379 vol.Required(CONF_PORT, default=port): _PORT_SELECTOR,
381 CONF_KNX_ROUTE_BACK, default=_route_back
382 ): selector.BooleanSelector(),
385 fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR
387 if not self.
_found_tunnels_found_tunnels
and not errors.get(
"base"):
388 errors[
"base"] =
"no_tunnel_discovered"
390 step_id=
"manual_tunnel", data_schema=vol.Schema(fields), errors=errors
394 self, user_input: dict |
None =
None
395 ) -> ConfigFlowResult:
396 """Configure ip secure tunnelling manually."""
399 if user_input
is not None:
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,
406 self.
new_titlenew_title = f
"Secure Tunneling @ {self.new_entry_data[CONF_HOST]}"
411 CONF_KNX_SECURE_USER_ID,
414 selector.NumberSelector(
415 selector.NumberSelectorConfig(
416 min=1, max=127, mode=selector.NumberSelectorMode.BOX
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),
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),
436 step_id=
"secure_tunnel_manual",
437 data_schema=vol.Schema(fields),
442 self, user_input: dict |
None =
None
443 ) -> ConfigFlowResult:
444 """Configure ip secure routing manually."""
447 if user_input
is not None:
449 key_bytes = bytes.fromhex(user_input[CONF_KNX_ROUTING_BACKBONE_KEY])
450 if len(key_bytes) != 16:
453 errors[CONF_KNX_ROUTING_BACKBONE_KEY] =
"invalid_backbone_key"
456 backbone_key=user_input[CONF_KNX_ROUTING_BACKBONE_KEY],
457 sync_latency_tolerance=user_input[
458 CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE
461 self.
new_titlenew_title = f
"Secure Routing as {self.new_entry_data[CONF_KNX_INDIVIDUAL_ADDRESS]}"
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),
472 CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE,
473 default=self.
initial_datainitial_data.
get(CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE)
476 selector.NumberSelector(
477 selector.NumberSelectorConfig(
480 unit_of_measurement=
"ms",
481 mode=selector.NumberSelectorMode.BOX,
489 step_id=
"secure_routing_manual",
490 data_schema=vol.Schema(fields),
495 self, user_input: dict[str, Any] |
None =
None
496 ) -> ConfigFlowResult:
497 """Manage upload of new KNX Keyring file."""
498 errors: dict[str, str] = {}
500 if user_input
is not None:
501 password = user_input[CONF_KNX_KNXKEY_PASSWORD]
505 uploaded_file_id=user_input[CONF_KEYRING_FILE],
508 except InvalidSecureConfiguration:
509 errors[CONF_KNX_KNXKEY_PASSWORD] =
"keyfile_invalid_signature"
511 if not errors
and self.
_keyring_keyring:
513 knxkeys_filename=f
"{DOMAIN}/{DEFAULT_KNX_KEYRING_FILENAME}",
514 knxkeys_password=password,
516 sync_latency_tolerance=
None,
519 if self.
connection_typeconnection_type
in (CONF_KNX_ROUTING, CONF_KNX_ROUTING_SECURE):
525 str(_if.individual_address)
for _if
in self.
_keyring_keyring.interfaces
532 vol.Required(CONF_KEYRING_FILE): selector.FileSelector(
533 config=selector.FileSelectorConfig(accept=
".knxkeys")
536 CONF_KNX_KNXKEY_PASSWORD,
538 ): selector.TextSelector(),
541 step_id=
"secure_knxkeys",
542 data_schema=vol.Schema(fields),
547 self, user_input: dict |
None =
None
548 ) -> ConfigFlowResult:
549 """Select if a specific tunnel should be used from knxkeys file."""
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:
557 tunnel_endpoint_ia=
None,
560 selected_tunnel_ia = user_input[CONF_KNX_TUNNEL_ENDPOINT_IA]
562 tunnel_endpoint_ia=selected_tunnel_ia,
565 device_authentication=
None,
571 if str(_if.individual_address) == selected_tunnel_ia
575 _tunnel_identifier = selected_tunnel_ia
or self.
new_entry_datanew_entry_data.
get(
578 _tunnel_suffix = f
" @ {_tunnel_identifier}" if _tunnel_identifier
else ""
580 f
"{'Secure ' if _if_user_id else ''}Tunneling{_tunnel_suffix}"
594 errors[
"base"] =
"keyfile_no_tunnel_for_host"
595 description_placeholders = {CONF_HOST:
str(host_ia)}
599 tunnel_endpoint_options = [
600 selector.SelectOptionDict(
601 value=CONF_KNX_AUTOMATIC, label=CONF_KNX_AUTOMATIC.capitalize()
604 tunnel_endpoint_options.extend(
605 selector.SelectOptionDict(
606 value=
str(endpoint.individual_address),
608 f
"{endpoint.individual_address} "
609 f
"{'🔐 ' if endpoint.user_id else ''}"
610 f
"(Data Secure GAs: {len(endpoint.group_addresses)})"
616 step_id=
"knxkeys_tunnel_select",
617 data_schema=vol.Schema(
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,
630 description_placeholders=description_placeholders,
634 self, user_input: dict |
None =
None
635 ) -> ConfigFlowResult:
638 _individual_address = (
639 user_input[CONF_KNX_INDIVIDUAL_ADDRESS]
641 else self.
initial_datainitial_data[CONF_KNX_INDIVIDUAL_ADDRESS]
644 user_input[CONF_KNX_MCAST_GRP]
649 user_input[CONF_KNX_MCAST_PORT]
654 if user_input
is not None:
658 errors[CONF_KNX_INDIVIDUAL_ADDRESS] =
"invalid_individual_address"
662 errors[CONF_KNX_MCAST_GRP] =
"invalid_ip_address"
663 if _local := user_input.get(CONF_KNX_LOCAL_IP):
665 _local_ip = await xknx_validate_ip(_local)
667 except (vol.Invalid, XKNXException):
668 errors[CONF_KNX_LOCAL_IP] =
"invalid_ip_address"
672 CONF_KNX_ROUTING_SECURE
673 if user_input[CONF_KNX_ROUTING_SECURE]
674 else CONF_KNX_ROUTING
677 connection_type=connection_type,
678 individual_address=_individual_address,
679 multicast_group=_multicast_group,
680 multicast_port=_multicast_port,
682 device_authentication=
None,
685 tunnel_endpoint_ia=
None,
687 if connection_type == CONF_KNX_ROUTING_SECURE:
688 self.
new_titlenew_title = f
"Secure Routing as {_individual_address}"
690 self.
new_titlenew_title = f
"Routing as {_individual_address}"
693 routers = [router
for router
in self.
_found_gateways_found_gateways
if router.supports_routing]
695 errors[
"base"] =
"no_router_discovered"
696 default_secure_routing_enable = any(
697 router
for router
in routers
if router.routing_requires_secure
700 fields: VolDictType = {
702 CONF_KNX_INDIVIDUAL_ADDRESS, default=_individual_address
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,
712 fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR
715 step_id=
"routing", data_schema=vol.Schema(fields), errors=errors
719 self, user_input: dict |
None =
None
720 ) -> ConfigFlowResult:
721 """Show the key source menu."""
723 step_id=
"secure_key_source_menu_tunnel",
724 menu_options=[
"secure_knxkeys",
"secure_tunnel_manual"],
728 self, user_input: dict |
None =
None
729 ) -> ConfigFlowResult:
730 """Show the key source menu."""
732 step_id=
"secure_key_source_menu_routing",
733 menu_options=[
"secure_knxkeys",
"secure_routing_manual"],
738 """Handle a KNX config flow."""
743 """Initialize KNX options flow."""
744 super().
__init__(initial_data=DEFAULT_ENTRY_DATA)
749 """Get the options flow for this handler."""
754 """Create the ConfigEntry."""
755 title = self.
new_titlenew_title
or f
"KNX {self.new_entry_data[CONF_KNX_CONNECTION_TYPE]}"
762 """Handle a flow initialized by the user."""
767 """Handle KNX options."""
769 general_settings: dict
771 def __init__(self, config_entry: ConfigEntry) ->
None:
772 """Initialize KNX options flow."""
773 super().
__init__(initial_data=config_entry.data)
777 """Update the ConfigEntry and finish the flow."""
779 self.hass.config_entries.async_update_entry(
782 title=self.
new_titlenew_title
or UNDEFINED,
787 self, user_input: dict[str, Any] |
None =
None
788 ) -> ConfigFlowResult:
789 """Manage KNX options."""
794 "communication_settings",
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],
813 CONF_KNX_STATE_UPDATER,
815 CONF_KNX_STATE_UPDATER, CONF_KNX_DEFAULT_STATE_UPDATER
817 ): selector.BooleanSelector(),
821 CONF_KNX_RATE_LIMIT, CONF_KNX_DEFAULT_RATE_LIMIT
824 selector.NumberSelector(
825 selector.NumberSelectorConfig(
827 max=CONF_MAX_RATE_LIMIT,
828 mode=selector.NumberSelectorMode.BOX,
834 CONF_KNX_TELEGRAM_LOG_SIZE,
836 CONF_KNX_TELEGRAM_LOG_SIZE, TELEGRAM_LOG_DEFAULT
839 selector.NumberSelector(
840 selector.NumberSelectorConfig(
842 max=TELEGRAM_LOG_MAX,
843 mode=selector.NumberSelectorMode.BOX,
850 step_id=
"communication_settings",
851 data_schema=vol.Schema(data_schema),
853 description_placeholders={
854 "telegram_log_size_max": f
"{TELEGRAM_LOG_MAX}",
ConfigFlowResult finish_flow(self)
ConfigFlowResult async_step_secure_key_source_menu_routing(self, dict|None user_input=None)
None __init__(self, KNXConfigEntryData initial_data)
ConfigFlowResult async_step_routing(self, dict|None user_input=None)
ConfigFlowResult async_step_secure_knxkeys(self, dict[str, Any]|None user_input=None)
str connection_type(self)
ConfigFlowResult async_step_knxkeys_tunnel_select(self, dict|None user_input=None)
ConfigFlowResult async_step_tunnel(self, dict|None user_input=None)
str|None tunnel_endpoint_ia(self)
ConfigFlowResult async_step_connection_type(self, dict|None user_input=None)
ConfigFlowResult async_step_secure_key_source_menu_tunnel(self, dict|None user_input=None)
ConfigFlowResult async_step_secure_tunnel_manual(self, dict|None user_input=None)
ConfigFlowResult async_step_manual_tunnel(self, dict|None user_input=None)
ConfigFlowResult async_step_secure_routing_manual(self, dict|None user_input=None)
ConfigFlowResult async_step_user(self, dict|None user_input=None)
ConfigFlowResult finish_flow(self)
KNXOptionsFlow async_get_options_flow(ConfigEntry config_entry)
None __init__(self, ConfigEntry config_entry)
ConfigFlowResult async_step_communication_settings(self, dict[str, Any]|None user_input=None)
ConfigFlowResult finish_flow(self)
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
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)
ConfigEntry config_entry(self)
None config_entry(self, ConfigEntry value)
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_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)
Keyring save_uploaded_knxkeys_file(HomeAssistant hass, str uploaded_file_id, str password)
str ip_v4_validator(Any value, bool|None multicast=None)