KNX ConfigFlow: add selection of secure tunnel endpoint (#84651)
parent
8827d9e88f
commit
0c7eb431e6
|
@ -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 = {}
|
||||
|
|
|
@ -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%]",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue