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:
Matthias Alphart 2022-04-10 15:56:45 +02:00 committed by GitHub
parent 4853ce208f
commit b3d1574a71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 226 additions and 135 deletions

View File

@ -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,

View File

@ -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

View File

@ -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]

View File

@ -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%]"
} }
} }
} }

View File

@ -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`",

View File

@ -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(