diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index 25e6684953f..b03a59b2d4e 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -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 = {} diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 80faf8c5ff1..d1ac3793c05 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -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%]", diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index 683c583107c..54cbabc8272 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -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", diff --git a/tests/components/knx/fixtures/fixture.knxkeys b/tests/components/knx/fixtures/fixture.knxkeys new file mode 100644 index 00000000000..76841091f2b --- /dev/null +++ b/tests/components/knx/fixtures/fixture.knxkeys @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 1b8c5c17664..5626bff5bbe 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -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,