KNX ConfigFlow: add selection of secure tunnel endpoint (#84651)

pull/59289/merge
Matthias Alphart 2022-12-28 11:43:03 +01:00 committed by GitHub
parent 8827d9e88f
commit 0c7eb431e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 145 additions and 24 deletions

View File

@ -11,7 +11,7 @@ from xknx.exceptions.exception import CommunicationError, InvalidSecureConfigura
from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
from xknx.io.gateway_scanner import GatewayDescriptor, GatewayScanner
from xknx.io.self_description import request_description
from xknx.secure import load_keyring
from xknx.secure.keyring import XMLInterface, load_keyring
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.const import CONF_HOST, CONF_PORT
@ -96,6 +96,7 @@ class KNXCommonFlow(ABC, FlowHandler):
self._found_gateways: list[GatewayDescriptor] = []
self._found_tunnels: list[GatewayDescriptor] = []
self._selected_tunnel: GatewayDescriptor | None = None
self._tunnel_endpoints: list[XMLInterface] = []
self._gatewayscanner: GatewayScanner | None = None
self._async_scan_gen: AsyncGenerator[GatewayDescriptor, None] | None = None
@ -459,12 +460,11 @@ class KNXCommonFlow(ABC, FlowHandler):
connection_type == CONF_KNX_TUNNELING_TCP_SECURE
and self._selected_tunnel is not None
):
tunnel_endpoints = []
if host_ia := self._selected_tunnel.individual_address:
tunnel_endpoints = keyring.get_tunnel_interfaces_by_host(
self._tunnel_endpoints = keyring.get_tunnel_interfaces_by_host(
host=host_ia
)
if not tunnel_endpoints:
if not self._tunnel_endpoints:
errors["base"] = "keyfile_no_tunnel_for_host"
description_placeholders = {CONF_HOST: str(host_ia)}
@ -483,13 +483,13 @@ class KNXCommonFlow(ABC, FlowHandler):
user_password=None,
)
if connection_type == CONF_KNX_ROUTING_SECURE:
title = (
"Secure Routing as"
f" {self.new_entry_data[CONF_KNX_INDIVIDUAL_ADDRESS]}"
return self.finish_flow(
title=(
"Secure Routing as"
f" {self.new_entry_data[CONF_KNX_INDIVIDUAL_ADDRESS]}"
)
)
else:
title = f"Secure Tunneling @ {self.new_entry_data[CONF_HOST]}"
return self.finish_flow(title=title)
return await self.async_step_knxkeys_tunnel_select()
if _default_filename := self.initial_data.get(CONF_KNX_KNXKEY_FILENAME):
_default_filename = _default_filename.lstrip(CONST_KNX_STORAGE_KEY)
@ -510,6 +510,48 @@ class KNXCommonFlow(ABC, FlowHandler):
description_placeholders=description_placeholders,
)
async def async_step_knxkeys_tunnel_select(
self, user_input: dict | None = None
) -> FlowResult:
"""Select if a specific tunnel should be used from knxkeys file."""
if user_input is not None:
if user_input[CONF_KNX_SECURE_USER_ID] == CONF_KNX_AUTOMATIC:
selected_user_id = None
else:
selected_user_id = int(user_input[CONF_KNX_SECURE_USER_ID])
self.new_entry_data |= KNXConfigEntryData(user_id=selected_user_id)
return self.finish_flow(
title=f"Secure Tunneling @ {self.new_entry_data[CONF_HOST]}"
)
tunnel_endpoint_options = [
selector.SelectOptionDict(
value=CONF_KNX_AUTOMATIC, label=CONF_KNX_AUTOMATIC.capitalize()
)
]
for endpoint in self._tunnel_endpoints:
tunnel_endpoint_options.append(
selector.SelectOptionDict(
value=str(endpoint.user_id),
label=f"{endpoint.individual_address} (User ID: {endpoint.user_id})",
)
)
return self.async_show_form(
step_id="knxkeys_tunnel_select",
data_schema=vol.Schema(
{
vol.Required(
CONF_KNX_SECURE_USER_ID, default=CONF_KNX_AUTOMATIC
): selector.SelectSelector(
selector.SelectSelectorConfig(
options=tunnel_endpoint_options,
mode=selector.SelectSelectorMode.LIST,
)
),
}
),
)
async def async_step_routing(self, user_input: dict | None = None) -> FlowResult:
"""Routing setup."""
errors: dict = {}

View File

@ -53,6 +53,13 @@
"knxkeys_password": "This was set when exporting the file from ETS."
}
},
"knxkeys_tunnel_select": {
"title": "Tunnel endpoint",
"description": "Select the tunnel used for connection.",
"data": {
"user_id": "`Automatic` will use the first free tunnel endpoint."
}
},
"secure_tunnel_manual": {
"title": "Secure tunnelling",
"description": "Please enter your IP secure information.",
@ -185,6 +192,13 @@
"knxkeys_password": "[%key:component::knx::config::step::secure_knxkeys::data_description::knxkeys_password%]"
}
},
"knxkeys_tunnel_select": {
"title": "[%key:component::knx::config::step::knxkeys_tunnel_select::title%]",
"description": "[%key:component::knx::config::step::knxkeys_tunnel_select::description%]",
"data": {
"user_id": "[%key:component::knx::config::step::knxkeys_tunnel_select::data::user_id%]"
}
},
"secure_tunnel_manual": {
"title": "[%key:component::knx::config::step::secure_tunnel_manual::title%]",
"description": "[%key:component::knx::config::step::secure_tunnel_manual::description%]",

View File

@ -27,6 +27,13 @@
"description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing.",
"title": "KNX connection"
},
"knxkeys_tunnel_select": {
"data": {
"user_id": "`Automatic` will use the first free tunnel endpoint."
},
"description": "Select the tunnel used for connection.",
"title": "Tunnel endpoint"
},
"manual_tunnel": {
"data": {
"host": "Host",
@ -150,6 +157,13 @@
"description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing.",
"title": "KNX connection"
},
"knxkeys_tunnel_select": {
"data": {
"user_id": "`Automatic` will use the first free tunnel endpoint."
},
"description": "Select the tunnel used for connection.",
"title": "Tunnel endpoint"
},
"manual_tunnel": {
"data": {
"host": "Host",

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<Keyring Project="Fixture" CreatedBy="ETS 5.7.7 (Build 1428)" Created="2022-12-25T21:51:38" Signature="D8Ba/jmH4UHQuJLw53OdpQ==" xmlns="http://knx.org/xml/keyring/1">
<Backbone MulticastAddress="224.0.23.12" Latency="2000" Key="xcV3R7WALk0eQ6HSI+ncfQ==" />
<Interface IndividualAddress="1.0.1" Type="Tunneling" Host="1.0.0" UserID="2" Password="N0HFn4s9NHsAK6xp63mD3Wr0y8iBpPVHtX5FZeVFWw4=" Authentication="qnfMZYp2fkUdc2YUBueD6xmYJ0z7ieBFiUK50LMnbm8=">
<Group Address="258" Senders="1.1.6 2.0.15" />
</Interface>
<Interface IndividualAddress="1.0.2" Type="Tunneling" Host="1.0.0" UserID="3" Password="N7Jk0/i/TAR2+4ox8JGdV//OVtGSbxyRKQB6CndWDVU=" Authentication="yhzBk/+Lxxqof6zGiOvfzqNuLVYZYmmBu4SN4Ipypkw=" />
<Interface IndividualAddress="1.0.3" Type="Tunneling" Host="1.0.0" UserID="4" Password="5KlUWTpmK3djAR2Fl7YuUfAJOorImmyxnQ43AOPCmlI=" Authentication="/5K2ZLNOC53LCnnMGgyX6SEkfB8k0/H3Pt7MpHtJEzY=" />
<Interface IndividualAddress="1.0.4" Type="Tunneling" Host="1.0.0" UserID="5" Password="5BzzkJEL8vE1CM4AldjpjRW6b+zuRSIG8uxCUvxTroY=" Authentication="GBcR0tVvq3qnZ92WNA9O1/N3cTmWf2GruQCJU+fkkyQ=" />
<Interface IndividualAddress="1.0.5" Type="Tunneling" Host="1.0.0" UserID="6" Password="sYTj/8WoEnLHQq5r79Cf/KnPqvqqxn6m0n//WOl+EgY=" Authentication="wkxfoYopUEEB8WkBa2Bb4qzvlXDD3I57Y5SXBJV0cdU=" />
<Interface IndividualAddress="1.0.6" Type="Tunneling" Host="1.0.0" UserID="7" Password="4lkU7sLlgq6d9qKFg3YEwOPcJQhoRGM+t9CCVcrPHOs=" Authentication="k4ALDpPFm2xOXSt8SJBG8vhRWCeNs/FpKk9B7WifzQk=" />
<Interface IndividualAddress="1.0.7" Type="Tunneling" Host="1.0.0" UserID="8" Password="4p6RMLAH+sI8mZJPxi+zX3GGnKlR54NMVj2jcKcRfK0=" Authentication="ZB+iC7vgmM1ycl3oSofh/zTyVLJrobyGVwgG2Kyt5Ms=" />
<Interface IndividualAddress="1.0.8" Type="Tunneling" Host="1.0.0" UserID="9" Password="IicVcQJOxCsdsRAu9RrPz9gwEp+Jkk2LQEOCov86lzo=" Authentication="HtrGUq2RDjf5rMuALQ/ZpqdthOpSKfj7hLXCac/fHd8=" />
<GroupAddresses>
<Group Address="256" Key="KiYcgBVMtwrtCRvz3u/vMg==" />
<Group Address="258" Key="eUybuo35i89BcR68CFomhQ==" />
</GroupAddresses>
<Devices>
<Device IndividualAddress="1.0.0" ToolKey="mjMZLrvZx1J0Z+QACuV7yA==" ManagementPassword="AsKTjFqd9RmP+OWarzz0PnD1Xd7P+ITC6lMEHEoijYo=" Authentication="vExfS/5D+tSOhd3hyC1lU7UsV1Nxh05ylWnPbYYvI8o=" />
<Device IndividualAddress="1.1.1" ToolKey="ZX3n7DioND4tg4jFn1BJzw==" ManagementPassword="FBrVvTIYuWMXW3+iRFA2XGh7sgMEjV/lRzpuOsubiWQ=" Authentication="h0TtsiDS3Fvz0KikbDukvV/DEF49sr8yk86Og0/AZX4=" />
<Device IndividualAddress="1.1.6" ToolKey="B/vHKFcP1CfbkrcWOuF3Ug==" />
<Device IndividualAddress="2.0.0" ToolKey="ucwHonh1NHzZBvjHRcgmzQ==" ManagementPassword="yRj53fmVApvZv//enaGzkEUAoNT7AeSOwFRVgZ8LjJA=" Authentication="wYyzgnSnP+VifVg5mcugfoGFfO1W7iCIy/xR5AXYHp4=" />
<Device IndividualAddress="2.0.6" ToolKey="RSgmgmTfbJoKAyx8DF2bpA==" ManagementPassword="mv2G10VM7iMf5NLJLJSjw6VFRyNXA+WwmfhsrJhDP9g=" Authentication="uMMm8eoNIkRoGB/W76pdBNR1iic9Mlp1px4Asm5u0T0=" />
<Device IndividualAddress="2.0.15" ToolKey="9xe7vNIyJD25ulbXMSRSLw==" />
</Devices>
</Keyring>

View File

@ -5,6 +5,7 @@ import pytest
from xknx.exceptions.exception import CommunicationError, InvalidSecureConfiguration
from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
from xknx.io.gateway_scanner import GatewayDescriptor
from xknx.secure.keyring import _load_keyring
from xknx.telegram import IndividualAddress
from homeassistant import config_entries
@ -43,8 +44,9 @@ from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult, FlowResultType
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, get_fixture_path
FIXTURE_KNXKEYS_PASSWORD = "test"
GATEWAY_INDIVIDUAL_ADDRESS = IndividualAddress("1.0.0")
@ -996,25 +998,34 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant, knx_setup):
assert not result["errors"]
with patch(
"homeassistant.components.knx.config_flow.load_keyring"
) as mock_load_keyring:
mock_keyring = Mock()
mock_keyring.get_tunnel_interfaces_by_host.return_value = ["stub"]
mock_load_keyring.return_value = mock_keyring
"xknx.secure.keyring._load_keyring",
return_value=_load_keyring(
str(get_fixture_path("fixture.knxkeys", DOMAIN).absolute()),
FIXTURE_KNXKEYS_PASSWORD,
),
):
secure_knxkeys = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_KNX_KNXKEY_FILENAME: "testcase.knxkeys",
CONF_KNX_KNXKEY_PASSWORD: "password",
CONF_KNX_KNXKEY_PASSWORD: "test",
},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert secure_knxkeys["step_id"] == "knxkeys_tunnel_select"
assert not result["errors"]
secure_knxkeys = await hass.config_entries.flow.async_configure(
secure_knxkeys["flow_id"],
{CONF_KNX_SECURE_USER_ID: CONF_KNX_AUTOMATIC},
)
await hass.async_block_till_done()
assert secure_knxkeys["type"] == FlowResultType.CREATE_ENTRY
assert secure_knxkeys["data"] == {
**DEFAULT_ENTRY_DATA,
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE,
CONF_KNX_KNXKEY_FILENAME: "knx/testcase.knxkeys",
CONF_KNX_KNXKEY_PASSWORD: "password",
CONF_KNX_KNXKEY_PASSWORD: "test",
CONF_KNX_ROUTING_BACKBONE_KEY: None,
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: None,
CONF_KNX_SECURE_DEVICE_AUTHENTICATION: None,
@ -1240,23 +1251,37 @@ async def test_options_flow_secure_manual_to_keyfile(
assert result4["step_id"] == "secure_knxkeys"
assert not result4["errors"]
with patch("homeassistant.components.knx.config_flow.load_keyring"):
with patch(
"xknx.secure.keyring._load_keyring",
return_value=_load_keyring(
str(get_fixture_path("fixture.knxkeys", DOMAIN).absolute()),
FIXTURE_KNXKEYS_PASSWORD,
),
):
secure_knxkeys = await hass.config_entries.options.async_configure(
result4["flow_id"],
{
CONF_KNX_KNXKEY_FILENAME: "testcase.knxkeys",
CONF_KNX_KNXKEY_PASSWORD: "password",
CONF_KNX_KNXKEY_PASSWORD: "test",
},
)
await hass.async_block_till_done()
assert result["type"] == FlowResultType.FORM
assert secure_knxkeys["step_id"] == "knxkeys_tunnel_select"
assert not result["errors"]
secure_knxkeys = await hass.config_entries.options.async_configure(
secure_knxkeys["flow_id"],
{CONF_KNX_SECURE_USER_ID: "2"},
)
await hass.async_block_till_done()
assert secure_knxkeys["type"] == FlowResultType.CREATE_ENTRY
assert mock_config_entry.data == {
**DEFAULT_ENTRY_DATA,
CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE,
CONF_KNX_KNXKEY_FILENAME: "knx/testcase.knxkeys",
CONF_KNX_KNXKEY_PASSWORD: "password",
CONF_KNX_KNXKEY_PASSWORD: "test",
CONF_KNX_SECURE_DEVICE_AUTHENTICATION: None,
CONF_KNX_SECURE_USER_ID: None,
CONF_KNX_SECURE_USER_ID: 2,
CONF_KNX_SECURE_USER_PASSWORD: None,
CONF_KNX_ROUTING_BACKBONE_KEY: None,
CONF_KNX_ROUTING_SYNC_LATENCY_TOLERANCE: None,