mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Refactor KNX config flow and validate user input (#69698)
* validate config flow user input * test flow for invalid user input * validate multicast address blocks * Update homeassistant/components/knx/config_flow.py Co-authored-by: Marvin Wichmann <me@marvin-wichmann.de> Co-authored-by: Marvin Wichmann <me@marvin-wichmann.de>
This commit is contained in:
parent
4853ce208f
commit
b3d1574a71
@ -24,7 +24,6 @@ from .const import (
|
|||||||
CONF_KNX_DEFAULT_RATE_LIMIT,
|
CONF_KNX_DEFAULT_RATE_LIMIT,
|
||||||
CONF_KNX_DEFAULT_STATE_UPDATER,
|
CONF_KNX_DEFAULT_STATE_UPDATER,
|
||||||
CONF_KNX_INDIVIDUAL_ADDRESS,
|
CONF_KNX_INDIVIDUAL_ADDRESS,
|
||||||
CONF_KNX_INITIAL_CONNECTION_TYPES,
|
|
||||||
CONF_KNX_KNXKEY_FILENAME,
|
CONF_KNX_KNXKEY_FILENAME,
|
||||||
CONF_KNX_KNXKEY_PASSWORD,
|
CONF_KNX_KNXKEY_PASSWORD,
|
||||||
CONF_KNX_LOCAL_IP,
|
CONF_KNX_LOCAL_IP,
|
||||||
@ -44,18 +43,19 @@ from .const import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
KNXConfigEntryData,
|
KNXConfigEntryData,
|
||||||
)
|
)
|
||||||
|
from .schema import ia_validator, ip_v4_validator
|
||||||
|
|
||||||
CONF_KNX_GATEWAY: Final = "gateway"
|
CONF_KNX_GATEWAY: Final = "gateway"
|
||||||
CONF_MAX_RATE_LIMIT: Final = 60
|
CONF_MAX_RATE_LIMIT: Final = 60
|
||||||
CONF_DEFAULT_LOCAL_IP: Final = "0.0.0.0"
|
CONF_DEFAULT_LOCAL_IP: Final = "0.0.0.0"
|
||||||
|
|
||||||
DEFAULT_ENTRY_DATA: KNXConfigEntryData = {
|
DEFAULT_ENTRY_DATA = KNXConfigEntryData(
|
||||||
CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER,
|
individual_address=XKNX.DEFAULT_ADDRESS,
|
||||||
CONF_KNX_RATE_LIMIT: CONF_KNX_DEFAULT_RATE_LIMIT,
|
multicast_group=DEFAULT_MCAST_GRP,
|
||||||
CONF_KNX_INDIVIDUAL_ADDRESS: XKNX.DEFAULT_ADDRESS,
|
multicast_port=DEFAULT_MCAST_PORT,
|
||||||
CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP,
|
state_updater=CONF_KNX_DEFAULT_STATE_UPDATER,
|
||||||
CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT,
|
rate_limit=CONF_KNX_DEFAULT_RATE_LIMIT,
|
||||||
}
|
)
|
||||||
|
|
||||||
CONF_KNX_TUNNELING_TYPE: Final = "tunneling_type"
|
CONF_KNX_TUNNELING_TYPE: Final = "tunneling_type"
|
||||||
CONF_KNX_LABEL_TUNNELING_TCP: Final = "TCP"
|
CONF_KNX_LABEL_TUNNELING_TCP: Final = "TCP"
|
||||||
@ -101,10 +101,9 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
connection_type = user_input[CONF_KNX_CONNECTION_TYPE]
|
connection_type = user_input[CONF_KNX_CONNECTION_TYPE]
|
||||||
if connection_type == CONF_KNX_AUTOMATIC:
|
if connection_type == CONF_KNX_AUTOMATIC:
|
||||||
entry_data: KNXConfigEntryData = {
|
entry_data = DEFAULT_ENTRY_DATA | KNXConfigEntryData(
|
||||||
**DEFAULT_ENTRY_DATA, # type: ignore[misc]
|
connection_type=CONF_KNX_AUTOMATIC
|
||||||
CONF_KNX_CONNECTION_TYPE: user_input[CONF_KNX_CONNECTION_TYPE],
|
)
|
||||||
}
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=CONF_KNX_AUTOMATIC.capitalize(),
|
title=CONF_KNX_AUTOMATIC.capitalize(),
|
||||||
data=entry_data,
|
data=entry_data,
|
||||||
@ -118,13 +117,15 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
return await self.async_step_manual_tunnel()
|
return await self.async_step_manual_tunnel()
|
||||||
|
|
||||||
errors: dict = {}
|
supported_connection_types = {
|
||||||
supported_connection_types = CONF_KNX_INITIAL_CONNECTION_TYPES.copy()
|
CONF_KNX_TUNNELING: CONF_KNX_TUNNELING.capitalize(),
|
||||||
gateways = await scan_for_gateways()
|
CONF_KNX_ROUTING: CONF_KNX_ROUTING.capitalize(),
|
||||||
|
}
|
||||||
if gateways:
|
if gateways := await scan_for_gateways():
|
||||||
# add automatic only if a gateway responded
|
# add automatic at first position only if a gateway responded
|
||||||
supported_connection_types.insert(0, CONF_KNX_AUTOMATIC)
|
supported_connection_types = {
|
||||||
|
CONF_KNX_AUTOMATIC: CONF_KNX_AUTOMATIC.capitalize()
|
||||||
|
} | supported_connection_types
|
||||||
self._found_tunnels = [
|
self._found_tunnels = [
|
||||||
gateway for gateway in gateways if gateway.supports_tunnelling
|
gateway for gateway in gateways if gateway.supports_tunnelling
|
||||||
]
|
]
|
||||||
@ -132,10 +133,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
fields = {
|
fields = {
|
||||||
vol.Required(CONF_KNX_CONNECTION_TYPE): vol.In(supported_connection_types)
|
vol.Required(CONF_KNX_CONNECTION_TYPE): vol.In(supported_connection_types)
|
||||||
}
|
}
|
||||||
|
return self.async_show_form(step_id="type", data_schema=vol.Schema(fields))
|
||||||
return self.async_show_form(
|
|
||||||
step_id="type", data_schema=vol.Schema(fields), errors=errors
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_tunnel(self, user_input: dict | None = None) -> FlowResult:
|
async def async_step_tunnel(self, user_input: dict | None = None) -> FlowResult:
|
||||||
"""Select a tunnel from a list. Will be skipped if the gateway scan was unsuccessful or if only one gateway was found."""
|
"""Select a tunnel from a list. Will be skipped if the gateway scan was unsuccessful or if only one gateway was found."""
|
||||||
@ -164,37 +162,48 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self, user_input: dict | None = None
|
self, user_input: dict | None = None
|
||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
"""Manually configure tunnel connection parameters. Fields default to preselected gateway if one was found."""
|
"""Manually configure tunnel connection parameters. Fields default to preselected gateway if one was found."""
|
||||||
|
errors: dict = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
connection_type = user_input[CONF_KNX_TUNNELING_TYPE]
|
try:
|
||||||
|
_host = ip_v4_validator(user_input[CONF_HOST], multicast=False)
|
||||||
|
except vol.Invalid:
|
||||||
|
errors[CONF_HOST] = "invalid_ip_address"
|
||||||
|
|
||||||
entry_data: KNXConfigEntryData = {
|
if _local_ip := user_input.get(CONF_KNX_LOCAL_IP):
|
||||||
**DEFAULT_ENTRY_DATA, # type: ignore[misc]
|
try:
|
||||||
CONF_HOST: user_input[CONF_HOST],
|
_local_ip = ip_v4_validator(_local_ip, multicast=False)
|
||||||
CONF_PORT: user_input[CONF_PORT],
|
except vol.Invalid:
|
||||||
CONF_KNX_ROUTE_BACK: (
|
errors[CONF_KNX_LOCAL_IP] = "invalid_ip_address"
|
||||||
connection_type == CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK
|
|
||||||
),
|
|
||||||
CONF_KNX_LOCAL_IP: user_input.get(CONF_KNX_LOCAL_IP),
|
|
||||||
CONF_KNX_CONNECTION_TYPE: (
|
|
||||||
CONF_KNX_TUNNELING_TCP
|
|
||||||
if connection_type == CONF_KNX_LABEL_TUNNELING_TCP
|
|
||||||
else CONF_KNX_TUNNELING
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
if connection_type == CONF_KNX_LABEL_TUNNELING_TCP_SECURE:
|
if not errors:
|
||||||
self._tunneling_config = entry_data
|
connection_type = user_input[CONF_KNX_TUNNELING_TYPE]
|
||||||
return self.async_show_menu(
|
entry_data = DEFAULT_ENTRY_DATA | KNXConfigEntryData(
|
||||||
step_id="secure_tunneling",
|
host=_host,
|
||||||
menu_options=["secure_knxkeys", "secure_manual"],
|
port=user_input[CONF_PORT],
|
||||||
|
route_back=(
|
||||||
|
connection_type == CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK
|
||||||
|
),
|
||||||
|
local_ip=_local_ip,
|
||||||
|
connection_type=(
|
||||||
|
CONF_KNX_TUNNELING_TCP
|
||||||
|
if connection_type == CONF_KNX_LABEL_TUNNELING_TCP
|
||||||
|
else CONF_KNX_TUNNELING
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.async_create_entry(
|
if connection_type == CONF_KNX_LABEL_TUNNELING_TCP_SECURE:
|
||||||
title=f"{CONF_KNX_TUNNELING.capitalize()} @ {user_input[CONF_HOST]}",
|
self._tunneling_config = entry_data
|
||||||
data=entry_data,
|
return self.async_show_menu(
|
||||||
)
|
step_id="secure_tunneling",
|
||||||
|
menu_options=["secure_knxkeys", "secure_manual"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=f"Tunneling @ {_host}",
|
||||||
|
data=entry_data,
|
||||||
|
)
|
||||||
|
|
||||||
errors: dict = {}
|
|
||||||
connection_methods: list[str] = [
|
connection_methods: list[str] = [
|
||||||
CONF_KNX_LABEL_TUNNELING_TCP,
|
CONF_KNX_LABEL_TUNNELING_TCP,
|
||||||
CONF_KNX_LABEL_TUNNELING_UDP,
|
CONF_KNX_LABEL_TUNNELING_UDP,
|
||||||
@ -231,20 +240,15 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
assert self._tunneling_config
|
assert self._tunneling_config
|
||||||
entry_data: KNXConfigEntryData = {
|
entry_data = self._tunneling_config | KNXConfigEntryData(
|
||||||
**self._tunneling_config, # type: ignore[misc]
|
connection_type=CONF_KNX_TUNNELING_TCP_SECURE,
|
||||||
CONF_KNX_SECURE_USER_ID: user_input[CONF_KNX_SECURE_USER_ID],
|
device_authentication=user_input[CONF_KNX_SECURE_DEVICE_AUTHENTICATION],
|
||||||
CONF_KNX_SECURE_USER_PASSWORD: user_input[
|
user_id=user_input[CONF_KNX_SECURE_USER_ID],
|
||||||
CONF_KNX_SECURE_USER_PASSWORD
|
user_password=user_input[CONF_KNX_SECURE_USER_PASSWORD],
|
||||||
],
|
)
|
||||||
CONF_KNX_SECURE_DEVICE_AUTHENTICATION: user_input[
|
|
||||||
CONF_KNX_SECURE_DEVICE_AUTHENTICATION
|
|
||||||
],
|
|
||||||
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE,
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=f"Secure {CONF_KNX_TUNNELING.capitalize()} @ {self._tunneling_config[CONF_HOST]}",
|
title=f"Secure Tunneling @ {self._tunneling_config[CONF_HOST]}",
|
||||||
data=entry_data,
|
data=entry_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -272,33 +276,29 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
|
assert self._tunneling_config
|
||||||
|
storage_key = CONST_KNX_STORAGE_KEY + user_input[CONF_KNX_KNXKEY_FILENAME]
|
||||||
try:
|
try:
|
||||||
assert self._tunneling_config
|
|
||||||
storage_key: str = (
|
|
||||||
CONST_KNX_STORAGE_KEY + user_input[CONF_KNX_KNXKEY_FILENAME]
|
|
||||||
)
|
|
||||||
load_key_ring(
|
load_key_ring(
|
||||||
self.hass.config.path(
|
path=self.hass.config.path(STORAGE_DIR, storage_key),
|
||||||
STORAGE_DIR,
|
password=user_input[CONF_KNX_KNXKEY_PASSWORD],
|
||||||
storage_key,
|
)
|
||||||
),
|
except FileNotFoundError:
|
||||||
user_input[CONF_KNX_KNXKEY_PASSWORD],
|
errors[CONF_KNX_KNXKEY_FILENAME] = "file_not_found"
|
||||||
|
except InvalidSignature:
|
||||||
|
errors[CONF_KNX_KNXKEY_PASSWORD] = "invalid_signature"
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
entry_data = self._tunneling_config | KNXConfigEntryData(
|
||||||
|
connection_type=CONF_KNX_TUNNELING_TCP_SECURE,
|
||||||
|
knxkeys_filename=storage_key,
|
||||||
|
knxkeys_password=user_input[CONF_KNX_KNXKEY_PASSWORD],
|
||||||
)
|
)
|
||||||
entry_data: KNXConfigEntryData = {
|
|
||||||
**self._tunneling_config, # type: ignore[misc]
|
|
||||||
CONF_KNX_KNXKEY_FILENAME: storage_key,
|
|
||||||
CONF_KNX_KNXKEY_PASSWORD: user_input[CONF_KNX_KNXKEY_PASSWORD],
|
|
||||||
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE,
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=f"Secure {CONF_KNX_TUNNELING.capitalize()} @ {self._tunneling_config[CONF_HOST]}",
|
title=f"Secure Tunneling @ {self._tunneling_config[CONF_HOST]}",
|
||||||
data=entry_data,
|
data=entry_data,
|
||||||
)
|
)
|
||||||
except InvalidSignature:
|
|
||||||
errors["base"] = "invalid_signature"
|
|
||||||
except FileNotFoundError:
|
|
||||||
errors["base"] = "file_not_found"
|
|
||||||
|
|
||||||
fields = {
|
fields = {
|
||||||
vol.Required(CONF_KNX_KNXKEY_FILENAME): selector.selector({"text": {}}),
|
vol.Required(CONF_KNX_KNXKEY_FILENAME): selector.selector({"text": {}}),
|
||||||
@ -311,33 +311,55 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
async def async_step_routing(self, user_input: dict | None = None) -> FlowResult:
|
async def async_step_routing(self, user_input: dict | None = None) -> FlowResult:
|
||||||
"""Routing setup."""
|
"""Routing setup."""
|
||||||
if user_input is not None:
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=CONF_KNX_ROUTING.capitalize(),
|
|
||||||
data={
|
|
||||||
**DEFAULT_ENTRY_DATA,
|
|
||||||
CONF_KNX_MCAST_GRP: user_input[CONF_KNX_MCAST_GRP],
|
|
||||||
CONF_KNX_MCAST_PORT: user_input[CONF_KNX_MCAST_PORT],
|
|
||||||
CONF_KNX_INDIVIDUAL_ADDRESS: user_input[
|
|
||||||
CONF_KNX_INDIVIDUAL_ADDRESS
|
|
||||||
],
|
|
||||||
CONF_KNX_LOCAL_IP: user_input.get(CONF_KNX_LOCAL_IP),
|
|
||||||
CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
errors: dict = {}
|
errors: dict = {}
|
||||||
|
_individual_address = (
|
||||||
|
user_input[CONF_KNX_INDIVIDUAL_ADDRESS]
|
||||||
|
if user_input
|
||||||
|
else XKNX.DEFAULT_ADDRESS
|
||||||
|
)
|
||||||
|
_multicast_group = (
|
||||||
|
user_input[CONF_KNX_MCAST_GRP] if user_input else DEFAULT_MCAST_GRP
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
ia_validator(_individual_address)
|
||||||
|
except vol.Invalid:
|
||||||
|
errors[CONF_KNX_INDIVIDUAL_ADDRESS] = "invalid_individual_address"
|
||||||
|
try:
|
||||||
|
ip_v4_validator(_multicast_group, multicast=True)
|
||||||
|
except vol.Invalid:
|
||||||
|
errors[CONF_KNX_MCAST_GRP] = "invalid_ip_address"
|
||||||
|
if _local_ip := user_input.get(CONF_KNX_LOCAL_IP):
|
||||||
|
try:
|
||||||
|
ip_v4_validator(_local_ip, multicast=False)
|
||||||
|
except vol.Invalid:
|
||||||
|
errors[CONF_KNX_LOCAL_IP] = "invalid_ip_address"
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
entry_data = DEFAULT_ENTRY_DATA | KNXConfigEntryData(
|
||||||
|
connection_type=CONF_KNX_ROUTING,
|
||||||
|
individual_address=_individual_address,
|
||||||
|
multicast_group=_multicast_group,
|
||||||
|
multicast_port=user_input[CONF_KNX_MCAST_PORT],
|
||||||
|
local_ip=_local_ip,
|
||||||
|
)
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=CONF_KNX_ROUTING.capitalize(), data=entry_data
|
||||||
|
)
|
||||||
|
|
||||||
fields = {
|
fields = {
|
||||||
vol.Required(
|
vol.Required(
|
||||||
CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS
|
CONF_KNX_INDIVIDUAL_ADDRESS, default=_individual_address
|
||||||
): _IA_SELECTOR,
|
): _IA_SELECTOR,
|
||||||
vol.Required(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): _IP_SELECTOR,
|
vol.Required(CONF_KNX_MCAST_GRP, default=_multicast_group): _IP_SELECTOR,
|
||||||
vol.Required(
|
vol.Required(
|
||||||
CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT
|
CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT
|
||||||
): _PORT_SELECTOR,
|
): _PORT_SELECTOR,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.show_advanced_options:
|
if self.show_advanced_options:
|
||||||
|
# Optional with default doesn't work properly in flow UI
|
||||||
fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR
|
fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
@ -477,38 +499,34 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
|||||||
last_step=True,
|
last_step=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
entry_data = {
|
_local_ip = self.general_settings.get(CONF_KNX_LOCAL_IP)
|
||||||
**DEFAULT_ENTRY_DATA,
|
entry_data = (
|
||||||
**self.general_settings,
|
DEFAULT_ENTRY_DATA
|
||||||
CONF_KNX_LOCAL_IP: self.general_settings.get(CONF_KNX_LOCAL_IP)
|
| self.general_settings
|
||||||
if self.general_settings.get(CONF_KNX_LOCAL_IP) != CONF_DEFAULT_LOCAL_IP
|
| KNXConfigEntryData(
|
||||||
else None,
|
host=self.current_config.get(CONF_HOST, ""),
|
||||||
CONF_HOST: self.current_config.get(CONF_HOST, ""),
|
local_ip=_local_ip if _local_ip != CONF_DEFAULT_LOCAL_IP else None,
|
||||||
}
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
connection_type = user_input[CONF_KNX_TUNNELING_TYPE]
|
connection_type = user_input[CONF_KNX_TUNNELING_TYPE]
|
||||||
entry_data = {
|
entry_data = entry_data | KNXConfigEntryData(
|
||||||
**entry_data,
|
host=user_input[CONF_HOST],
|
||||||
CONF_HOST: user_input[CONF_HOST],
|
port=user_input[CONF_PORT],
|
||||||
CONF_PORT: user_input[CONF_PORT],
|
route_back=(connection_type == CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK),
|
||||||
CONF_KNX_ROUTE_BACK: (
|
connection_type=(
|
||||||
connection_type == CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK
|
|
||||||
),
|
|
||||||
CONF_KNX_CONNECTION_TYPE: (
|
|
||||||
CONF_KNX_TUNNELING_TCP
|
CONF_KNX_TUNNELING_TCP
|
||||||
if connection_type == CONF_KNX_LABEL_TUNNELING_TCP
|
if connection_type == CONF_KNX_LABEL_TUNNELING_TCP
|
||||||
else CONF_KNX_TUNNELING
|
else CONF_KNX_TUNNELING
|
||||||
),
|
),
|
||||||
}
|
)
|
||||||
|
|
||||||
entry_title = str(entry_data[CONF_KNX_CONNECTION_TYPE]).capitalize()
|
entry_title = str(entry_data[CONF_KNX_CONNECTION_TYPE]).capitalize()
|
||||||
if entry_data[CONF_KNX_CONNECTION_TYPE] == CONF_KNX_TUNNELING:
|
if entry_data[CONF_KNX_CONNECTION_TYPE] == CONF_KNX_TUNNELING:
|
||||||
entry_title = f"{CONF_KNX_TUNNELING.capitalize()} @ {entry_data[CONF_HOST]}"
|
entry_title = f"Tunneling @ {entry_data[CONF_HOST]}"
|
||||||
if entry_data[CONF_KNX_CONNECTION_TYPE] == CONF_KNX_TUNNELING_TCP:
|
if entry_data[CONF_KNX_CONNECTION_TYPE] == CONF_KNX_TUNNELING_TCP:
|
||||||
entry_title = (
|
entry_title = f"Tunneling @ {entry_data[CONF_HOST]} (TCP)"
|
||||||
f"{CONF_KNX_TUNNELING.capitalize()} (TCP) @ {entry_data[CONF_HOST]}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.hass.config_entries.async_update_entry(
|
self.hass.config_entries.async_update_entry(
|
||||||
self.config_entry,
|
self.config_entry,
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
"""Constants for the KNX integration."""
|
"""Constants for the KNX integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Final, TypedDict
|
from typing import Final, TypedDict
|
||||||
|
|
||||||
@ -68,7 +70,6 @@ CONF_RESET_AFTER: Final = "reset_after"
|
|||||||
CONF_RESPOND_TO_READ: Final = "respond_to_read"
|
CONF_RESPOND_TO_READ: Final = "respond_to_read"
|
||||||
CONF_STATE_ADDRESS: Final = "state_address"
|
CONF_STATE_ADDRESS: Final = "state_address"
|
||||||
CONF_SYNC_STATE: Final = "sync_state"
|
CONF_SYNC_STATE: Final = "sync_state"
|
||||||
CONF_KNX_INITIAL_CONNECTION_TYPES: Final = [CONF_KNX_TUNNELING, CONF_KNX_ROUTING]
|
|
||||||
|
|
||||||
# yaml config merged with config entry data
|
# yaml config merged with config entry data
|
||||||
DATA_KNX_CONFIG: Final = "knx_config"
|
DATA_KNX_CONFIG: Final = "knx_config"
|
||||||
@ -84,7 +85,7 @@ class KNXConfigEntryData(TypedDict, total=False):
|
|||||||
|
|
||||||
connection_type: str
|
connection_type: str
|
||||||
individual_address: str
|
individual_address: str
|
||||||
local_ip: str
|
local_ip: str | None
|
||||||
multicast_group: str
|
multicast_group: str
|
||||||
multicast_port: int
|
multicast_port: int
|
||||||
route_back: bool
|
route_back: bool
|
||||||
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
import ipaddress
|
||||||
from typing import Any, ClassVar, Final
|
from typing import Any, ClassVar, Final
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -70,12 +71,29 @@ def ga_validator(value: Any) -> str | int:
|
|||||||
ga_list_validator = vol.All(cv.ensure_list, [ga_validator])
|
ga_list_validator = vol.All(cv.ensure_list, [ga_validator])
|
||||||
|
|
||||||
ia_validator = vol.Any(
|
ia_validator = vol.Any(
|
||||||
cv.matches_regex(IndividualAddress.ADDRESS_RE.pattern),
|
vol.All(str, str.strip, cv.matches_regex(IndividualAddress.ADDRESS_RE.pattern)),
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||||
msg="value does not match pattern for KNX individual address '<area>.<line>.<device>' (eg.'1.1.100')",
|
msg="value does not match pattern for KNX individual address '<area>.<line>.<device>' (eg.'1.1.100')",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ip_v4_validator(value: Any, multicast: bool | None = None) -> str:
|
||||||
|
"""
|
||||||
|
Validate that value is parsable as IPv4 address.
|
||||||
|
|
||||||
|
Optionally check if address is in a reserved multicast block or is explicitly not.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
address = ipaddress.IPv4Address(value)
|
||||||
|
except ipaddress.AddressValueError as ex:
|
||||||
|
raise vol.Invalid(f"value '{value}' is not a valid IPv4 address: {ex}") from ex
|
||||||
|
if multicast is not None and address.is_multicast != multicast:
|
||||||
|
raise vol.Invalid(
|
||||||
|
f"value '{value}' is not a valid IPv4 {'multicast' if multicast else 'unicast'} address"
|
||||||
|
)
|
||||||
|
return str(address)
|
||||||
|
|
||||||
|
|
||||||
def number_limit_sub_validator(entity_config: OrderedDict) -> OrderedDict:
|
def number_limit_sub_validator(entity_config: OrderedDict) -> OrderedDict:
|
||||||
"""Validate a number entity configurations dependent on configured value type."""
|
"""Validate a number entity configurations dependent on configured value type."""
|
||||||
value_type = entity_config[CONF_TYPE]
|
value_type = entity_config[CONF_TYPE]
|
||||||
|
@ -62,8 +62,8 @@
|
|||||||
"description": "Please configure the routing options.",
|
"description": "Please configure the routing options.",
|
||||||
"data": {
|
"data": {
|
||||||
"individual_address": "Individual address",
|
"individual_address": "Individual address",
|
||||||
"multicast_group": "Multicast group used for routing",
|
"multicast_group": "Multicast group",
|
||||||
"multicast_port": "Multicast port used for routing",
|
"multicast_port": "Multicast port",
|
||||||
"local_ip": "Local IP of Home Assistant"
|
"local_ip": "Local IP of Home Assistant"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
@ -78,8 +78,10 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"invalid_signature": "The password to decrypt the knxkeys file is wrong.",
|
"invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'",
|
||||||
"file_not_found": "The specified knxkeys file was not found in the path config/.storage/knx/"
|
"invalid_ip_address": "Invalid IPv4 address.",
|
||||||
|
"invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.",
|
||||||
|
"file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
@ -88,8 +90,8 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"connection_type": "KNX Connection Type",
|
"connection_type": "KNX Connection Type",
|
||||||
"individual_address": "Default individual address",
|
"individual_address": "Default individual address",
|
||||||
"multicast_group": "Multicast group",
|
"multicast_group": "[%key:component::knx::config::step::routing::data::multicast_group%]",
|
||||||
"multicast_port": "Multicast port",
|
"multicast_port": "[%key:component::knx::config::step::routing::data::multicast_port%]",
|
||||||
"local_ip": "Local IP of Home Assistant",
|
"local_ip": "Local IP of Home Assistant",
|
||||||
"state_updater": "State updater",
|
"state_updater": "State updater",
|
||||||
"rate_limit": "Rate limit"
|
"rate_limit": "Rate limit"
|
||||||
@ -110,8 +112,8 @@
|
|||||||
"host": "[%key:common::config_flow::data::host%]"
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"port": "Port of the KNX/IP tunneling device.",
|
"port": "[%key:component::knx::config::step::manual_tunnel::data_description::port%]",
|
||||||
"host": "IP address of the KNX/IP tunneling device."
|
"host": "[%key:component::knx::config::step::manual_tunnel::data_description::host%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,10 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "Failed to connect",
|
"cannot_connect": "Failed to connect",
|
||||||
"file_not_found": "The specified knxkeys file was not found in the path config/.storage/knx/",
|
"file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/",
|
||||||
"invalid_signature": "The password to decrypt the knxkeys file is wrong."
|
"invalid_individual_address": "Value does not match pattern for KNX individual address.\n'<area>.<line>.<device>'",
|
||||||
|
"invalid_ip_address": "Invalid IPv4 address.",
|
||||||
|
"invalid_signature": "The password to decrypt the `.knxkeys` file is wrong."
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"manual_tunnel": {
|
"manual_tunnel": {
|
||||||
@ -28,8 +30,8 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"individual_address": "Individual address",
|
"individual_address": "Individual address",
|
||||||
"local_ip": "Local IP of Home Assistant",
|
"local_ip": "Local IP of Home Assistant",
|
||||||
"multicast_group": "Multicast group used for routing",
|
"multicast_group": "Multicast group",
|
||||||
"multicast_port": "Multicast port used for routing"
|
"multicast_port": "Multicast port"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`",
|
"individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`",
|
||||||
|
@ -150,6 +150,26 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None:
|
|||||||
assert result2["step_id"] == "routing"
|
assert result2["step_id"] == "routing"
|
||||||
assert not result2["errors"]
|
assert not result2["errors"]
|
||||||
|
|
||||||
|
# invalid user input
|
||||||
|
result_invalid_input = await hass.config_entries.flow.async_configure(
|
||||||
|
result2["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_KNX_MCAST_GRP: "10.1.2.3", # no valid multicast group
|
||||||
|
CONF_KNX_MCAST_PORT: 3675,
|
||||||
|
CONF_KNX_INDIVIDUAL_ADDRESS: "not_a_valid_address",
|
||||||
|
CONF_KNX_LOCAL_IP: "no_local_ip",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert result_invalid_input["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result_invalid_input["step_id"] == "routing"
|
||||||
|
assert result_invalid_input["errors"] == {
|
||||||
|
CONF_KNX_MCAST_GRP: "invalid_ip_address",
|
||||||
|
CONF_KNX_INDIVIDUAL_ADDRESS: "invalid_individual_address",
|
||||||
|
CONF_KNX_LOCAL_IP: "invalid_ip_address",
|
||||||
|
}
|
||||||
|
|
||||||
|
# valid user input
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.knx.async_setup_entry",
|
"homeassistant.components.knx.async_setup_entry",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
@ -297,6 +317,36 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None:
|
|||||||
assert result2["step_id"] == "manual_tunnel"
|
assert result2["step_id"] == "manual_tunnel"
|
||||||
assert not result2["errors"]
|
assert not result2["errors"]
|
||||||
|
|
||||||
|
# invalid host ip address
|
||||||
|
result_invalid_host = await hass.config_entries.flow.async_configure(
|
||||||
|
result2["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP,
|
||||||
|
CONF_HOST: DEFAULT_MCAST_GRP, # multicast addresses are invalid
|
||||||
|
CONF_PORT: 3675,
|
||||||
|
CONF_KNX_LOCAL_IP: "192.168.1.112",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert result_invalid_host["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result_invalid_host["step_id"] == "manual_tunnel"
|
||||||
|
assert result_invalid_host["errors"] == {CONF_HOST: "invalid_ip_address"}
|
||||||
|
# invalid local ip address
|
||||||
|
result_invalid_local = await hass.config_entries.flow.async_configure(
|
||||||
|
result2["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP,
|
||||||
|
CONF_HOST: "192.168.0.2",
|
||||||
|
CONF_PORT: 3675,
|
||||||
|
CONF_KNX_LOCAL_IP: "asdf",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert result_invalid_local["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result_invalid_local["step_id"] == "manual_tunnel"
|
||||||
|
assert result_invalid_local["errors"] == {CONF_KNX_LOCAL_IP: "invalid_ip_address"}
|
||||||
|
|
||||||
|
# valid user input
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.knx.async_setup_entry",
|
"homeassistant.components.knx.async_setup_entry",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
@ -584,7 +634,7 @@ async def test_configure_secure_knxkeys_file_not_found(hass: HomeAssistant):
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert secure_knxkeys["type"] == RESULT_TYPE_FORM
|
assert secure_knxkeys["type"] == RESULT_TYPE_FORM
|
||||||
assert secure_knxkeys["errors"]
|
assert secure_knxkeys["errors"]
|
||||||
assert secure_knxkeys["errors"]["base"] == "file_not_found"
|
assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_FILENAME] == "file_not_found"
|
||||||
|
|
||||||
|
|
||||||
async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant):
|
async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant):
|
||||||
@ -613,7 +663,7 @@ async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant):
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert secure_knxkeys["type"] == RESULT_TYPE_FORM
|
assert secure_knxkeys["type"] == RESULT_TYPE_FORM
|
||||||
assert secure_knxkeys["errors"]
|
assert secure_knxkeys["errors"]
|
||||||
assert secure_knxkeys["errors"]["base"] == "invalid_signature"
|
assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_PASSWORD] == "invalid_signature"
|
||||||
|
|
||||||
|
|
||||||
async def test_options_flow(
|
async def test_options_flow(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user