From a752232de8959edc57339d8bb65b8bb81d914158 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 27 Dec 2022 21:00:19 +0100 Subject: [PATCH] KNX ConfigFlow: Validate contents of knxkeys file (#84411) --- homeassistant/components/knx/config_flow.py | 35 +++++++-- homeassistant/components/knx/strings.json | 30 +++++++- .../components/knx/translations/en.json | 66 +++++++++++------ tests/components/knx/test_config_flow.py | 73 ++++++++++++++++--- 4 files changed, 160 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index c1e8c12dd01..25e6684953f 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -440,18 +440,37 @@ class KNXCommonFlow(ABC, FlowHandler): ) -> FlowResult: """Configure secure knxkeys used to authenticate.""" errors = {} + description_placeholders = {} if user_input is not None: + connection_type = self.new_entry_data[CONF_KNX_CONNECTION_TYPE] storage_key = CONST_KNX_STORAGE_KEY + user_input[CONF_KNX_KNXKEY_FILENAME] try: - await load_keyring( + keyring = await load_keyring( path=self.hass.config.path(STORAGE_DIR, storage_key), password=user_input[CONF_KNX_KNXKEY_PASSWORD], ) except FileNotFoundError: - errors[CONF_KNX_KNXKEY_FILENAME] = "file_not_found" + errors[CONF_KNX_KNXKEY_FILENAME] = "keyfile_not_found" except InvalidSecureConfiguration: - errors[CONF_KNX_KNXKEY_PASSWORD] = "invalid_signature" + errors[CONF_KNX_KNXKEY_PASSWORD] = "keyfile_invalid_signature" + else: + if ( + 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( + host=host_ia + ) + if not tunnel_endpoints: + errors["base"] = "keyfile_no_tunnel_for_host" + description_placeholders = {CONF_HOST: str(host_ia)} + + if connection_type == CONF_KNX_ROUTING_SECURE: + if not (keyring.backbone is not None and keyring.backbone.key): + errors["base"] = "keyfile_no_backbone_key" if not errors: self.new_entry_data |= KNXConfigEntryData( @@ -463,10 +482,7 @@ class KNXCommonFlow(ABC, FlowHandler): user_id=None, user_password=None, ) - if ( - self.new_entry_data[CONF_KNX_CONNECTION_TYPE] - == CONF_KNX_ROUTING_SECURE - ): + if connection_type == CONF_KNX_ROUTING_SECURE: title = ( "Secure Routing as" f" {self.new_entry_data[CONF_KNX_INDIVIDUAL_ADDRESS]}" @@ -488,7 +504,10 @@ class KNXCommonFlow(ABC, FlowHandler): } return self.async_show_form( - step_id="secure_knxkeys", data_schema=vol.Schema(fields), errors=errors + step_id="secure_knxkeys", + data_schema=vol.Schema(fields), + errors=errors, + description_placeholders=description_placeholders, ) async def async_step_routing(self, user_input: dict | None = None) -> FlowResult: diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 632af9961dc..80faf8c5ff1 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -2,18 +2,21 @@ "config": { "step": { "connection_type": { + "title": "KNX connection", "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.", "data": { "connection_type": "KNX Connection Type" } }, "tunnel": { + "title": "Tunnel", "description": "Please select a gateway from the list.", "data": { "gateway": "KNX Tunnel Connection" } }, "manual_tunnel": { + "title": "Tunnel settings", "description": "Please enter the connection information of your tunneling device.", "data": { "tunneling_type": "KNX Tunneling Type", @@ -30,6 +33,7 @@ } }, "secure_key_source": { + "title": "KNX IP-Secure", "description": "Select how you want to configure KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys", @@ -38,6 +42,7 @@ } }, "secure_knxkeys": { + "title": "Keyfile", "description": "Please enter the information for your `.knxkeys` file.", "data": { "knxkeys_filename": "The filename of your `.knxkeys` file (including extension)", @@ -49,6 +54,7 @@ } }, "secure_tunnel_manual": { + "title": "Secure tunnelling", "description": "Please enter your IP secure information.", "data": { "user_id": "User ID", @@ -62,6 +68,7 @@ } }, "secure_routing_manual": { + "title": "Secure routing", "description": "Please enter your IP secure information.", "data": { "backbone_key": "Backbone key", @@ -73,6 +80,7 @@ } }, "routing": { + "title": "Routing", "description": "Please configure the routing options.", "data": { "individual_address": "Individual address", @@ -96,8 +104,10 @@ "invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.", "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.", - "file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", + "keyfile_invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.", + "keyfile_no_backbone_key": "The `.knxkeys` file does not contain a backbone key for secure routing.", + "keyfile_no_tunnel_for_host": "The `.knxkeys` file does not contain credentials for host `{host}`.", + "keyfile_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", "no_router_discovered": "No KNXnet/IP router was discovered on the network.", "no_tunnel_discovered": "Could not find a KNX tunneling server on your network.", "unsupported_tunnel_type": "Selected tunnelling type not supported by gateway." @@ -106,12 +116,14 @@ "options": { "step": { "options_init": { + "title": "KNX Settings", "menu_options": { "connection_type": "Configure KNX interface", "communication_settings": "Communication settings" } }, "communication_settings": { + "title": "Communication settings", "data": { "state_updater": "State updater", "rate_limit": "Rate limit" @@ -122,18 +134,21 @@ } }, "connection_type": { + "title": "[%key:component::knx::config::step::connection_type::title%]", "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.", "data": { "connection_type": "KNX Connection Type" } }, "tunnel": { + "title": "[%key:component::knx::config::step::tunnel::title%]", "description": "[%key:component::knx::config::step::tunnel::description%]", "data": { "gateway": "[%key:component::knx::config::step::tunnel::data::gateway%]" } }, "manual_tunnel": { + "title": "[%key:component::knx::config::step::manual_tunnel::title%]", "description": "[%key:component::knx::config::step::manual_tunnel::description%]", "data": { "tunneling_type": "[%key:component::knx::config::step::manual_tunnel::data::tunneling_type%]", @@ -150,6 +165,7 @@ } }, "secure_key_source": { + "title": "[%key:component::knx::config::step::secure_key_source::title%]", "description": "[%key:component::knx::config::step::secure_key_source::description%]", "menu_options": { "secure_knxkeys": "[%key:component::knx::config::step::secure_key_source::menu_options::secure_knxkeys%]", @@ -158,6 +174,7 @@ } }, "secure_knxkeys": { + "title": "[%key:component::knx::config::step::secure_knxkeys::title%]", "description": "[%key:component::knx::config::step::secure_knxkeys::description%]", "data": { "knxkeys_filename": "[%key:component::knx::config::step::secure_knxkeys::data::knxkeys_filename%]", @@ -169,6 +186,7 @@ } }, "secure_tunnel_manual": { + "title": "[%key:component::knx::config::step::secure_tunnel_manual::title%]", "description": "[%key:component::knx::config::step::secure_tunnel_manual::description%]", "data": { "user_id": "[%key:component::knx::config::step::secure_tunnel_manual::data::user_id%]", @@ -182,6 +200,7 @@ } }, "secure_routing_manual": { + "title": "[%key:component::knx::config::step::secure_routing_manual::title%]", "description": "[%key:component::knx::config::step::secure_routing_manual::description%]", "data": { "backbone_key": "[%key:component::knx::config::step::secure_routing_manual::data::backbone_key%]", @@ -193,6 +212,7 @@ } }, "routing": { + "title": "[%key:component::knx::config::step::routing::title%]", "description": "[%key:component::knx::config::step::routing::description%]", "data": { "individual_address": "[%key:component::knx::config::step::routing::data::individual_address%]", @@ -212,8 +232,10 @@ "invalid_backbone_key": "[%key:component::knx::config::error::invalid_backbone_key%]", "invalid_individual_address": "[%key:component::knx::config::error::invalid_individual_address%]", "invalid_ip_address": "[%key:component::knx::config::error::invalid_ip_address%]", - "invalid_signature": "[%key:component::knx::config::error::invalid_signature%]", - "file_not_found": "[%key:component::knx::config::error::file_not_found%]", + "keyfile_no_backbone_key": "[%key:component::knx::config::error::keyfile_no_backbone_key%]", + "keyfile_invalid_signature": "[%key:component::knx::config::error::keyfile_invalid_signature%]", + "keyfile_no_tunnel_for_host": "[%key:component::knx::config::error::keyfile_no_tunnel_for_host%]", + "keyfile_not_found": "[%key:component::knx::config::error::keyfile_not_found%]", "no_router_discovered": "[%key:component::knx::config::error::no_router_discovered%]", "no_tunnel_discovered": "[%key:component::knx::config::error::no_tunnel_discovered%]", "unsupported_tunnel_type": "[%key:component::knx::config::error::unsupported_tunnel_type%]" diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index a014eb376e4..51aef72ed4f 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -6,11 +6,13 @@ }, "error": { "cannot_connect": "Failed to connect", - "file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", "invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.", "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.", + "keyfile_invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.", + "keyfile_no_backbone_key": "The `.knxkeys` file does not contain a backbone key for secure routing.", + "keyfile_no_tunnel_for_host": "The `.knxkeys` file does not contain credentials for host `{host}`.", + "keyfile_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", "no_router_discovered": "No KNXnet/IP router was discovered on the network.", "no_tunnel_discovered": "Could not find a KNX tunneling server on your network.", "unsupported_tunnel_type": "Selected tunnelling type not supported by gateway." @@ -20,7 +22,8 @@ "data": { "connection_type": "KNX Connection Type" }, - "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." + "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" }, "manual_tunnel": { "data": { @@ -36,7 +39,8 @@ "port": "Port of the KNX/IP tunneling device.", "route_back": "Enable if your KNXnet/IP tunneling server is behind NAT. Only applies for UDP connections." }, - "description": "Please enter the connection information of your tunneling device." + "description": "Please enter the connection information of your tunneling device.", + "title": "Tunnel settings" }, "routing": { "data": { @@ -50,7 +54,8 @@ "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", "local_ip": "Leave blank to use auto-discovery." }, - "description": "Please configure the routing options." + "description": "Please configure the routing options.", + "title": "Routing" }, "secure_key_source": { "description": "Select how you want to configure KNX/IP Secure.", @@ -58,7 +63,8 @@ "secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys", "secure_routing_manual": "Configure IP secure backbone key manually", "secure_tunnel_manual": "Configure IP secure credentials manually" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +75,8 @@ "knxkeys_filename": "The file is expected to be found in your config directory in `.storage/knx/`.\nIn Home Assistant OS this would be `/config/.storage/knx/`\nExample: `my_project.knxkeys`", "knxkeys_password": "This was set when exporting the file from ETS." }, - "description": "Please enter the information for your `.knxkeys` file." + "description": "Please enter the information for your `.knxkeys` file.", + "title": "Keyfile" }, "secure_routing_manual": { "data": { @@ -80,7 +87,8 @@ "backbone_key": "Can be seen in the 'Security' report of an ETS project. Eg. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Default is 1000." }, - "description": "Please enter your IP secure information." + "description": "Please enter your IP secure information.", + "title": "Secure routing" }, "secure_tunnel_manual": { "data": { @@ -93,24 +101,28 @@ "user_id": "This is often tunnel number +1. So 'Tunnel 2' would have User-ID '3'.", "user_password": "Password for the specific tunnel connection set in the 'Properties' panel of the tunnel in ETS." }, - "description": "Please enter your IP secure information." + "description": "Please enter your IP secure information.", + "title": "Secure tunnelling" }, "tunnel": { "data": { "gateway": "KNX Tunnel Connection" }, - "description": "Please select a gateway from the list." + "description": "Please select a gateway from the list.", + "title": "Tunnel" } } }, "options": { "error": { "cannot_connect": "Failed to connect", - "file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", "invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.", "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.", + "keyfile_invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.", + "keyfile_no_backbone_key": "The `.knxkeys` file does not contain a backbone key for secure routing.", + "keyfile_no_tunnel_for_host": "The `.knxkeys` file does not contain credentials for host `{host}`.", + "keyfile_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", "no_router_discovered": "No KNXnet/IP router was discovered on the network.", "no_tunnel_discovered": "Could not find a KNX tunneling server on your network.", "unsupported_tunnel_type": "Selected tunnelling type not supported by gateway." @@ -124,13 +136,15 @@ "data_description": { "rate_limit": "Maximum outgoing telegrams per second.\n`0` to disable limit. Recommended: 0 or 20 to 40", "state_updater": "Set default for reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve entity states from the KNX Bus. Can be overridden by `sync_state` entity options." - } + }, + "title": "Communication settings" }, "connection_type": { "data": { "connection_type": "KNX Connection Type" }, - "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." + "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" }, "manual_tunnel": { "data": { @@ -146,13 +160,15 @@ "port": "Port of the KNX/IP tunneling device.", "route_back": "Enable if your KNXnet/IP tunneling server is behind NAT. Only applies for UDP connections." }, - "description": "Please enter the connection information of your tunneling device." + "description": "Please enter the connection information of your tunneling device.", + "title": "Tunnel settings" }, "options_init": { "menu_options": { "communication_settings": "Communication settings", "connection_type": "Configure KNX interface" - } + }, + "title": "KNX Settings" }, "routing": { "data": { @@ -166,7 +182,8 @@ "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", "local_ip": "Leave blank to use auto-discovery." }, - "description": "Please configure the routing options." + "description": "Please configure the routing options.", + "title": "Routing" }, "secure_key_source": { "description": "Select how you want to configure KNX/IP Secure.", @@ -174,7 +191,8 @@ "secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys", "secure_routing_manual": "Configure IP secure backbone key manually", "secure_tunnel_manual": "Configure IP secure credentials manually" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +203,8 @@ "knxkeys_filename": "The file is expected to be found in your config directory in `.storage/knx/`.\nIn Home Assistant OS this would be `/config/.storage/knx/`\nExample: `my_project.knxkeys`", "knxkeys_password": "This was set when exporting the file from ETS." }, - "description": "Please enter the information for your `.knxkeys` file." + "description": "Please enter the information for your `.knxkeys` file.", + "title": "Keyfile" }, "secure_routing_manual": { "data": { @@ -196,7 +215,8 @@ "backbone_key": "Can be seen in the 'Security' report of an ETS project. Eg. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Default is 1000." }, - "description": "Please enter your IP secure information." + "description": "Please enter your IP secure information.", + "title": "Secure routing" }, "secure_tunnel_manual": { "data": { @@ -209,13 +229,15 @@ "user_id": "This is often tunnel number +1. So 'Tunnel 2' would have User-ID '3'.", "user_password": "Password for the specific tunnel connection set in the 'Properties' panel of the tunnel in ETS." }, - "description": "Please enter your IP secure information." + "description": "Please enter your IP secure information.", + "title": "Secure tunnelling" }, "tunnel": { "data": { "gateway": "KNX Tunnel Connection" }, - "description": "Please select a gateway from the list." + "description": "Please select a gateway from the list.", + "title": "Tunnel" } } } diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 429b784b91c..1b8c5c17664 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -1,10 +1,11 @@ """Test the KNX config flow.""" -from unittest.mock import patch +from unittest.mock import Mock, patch 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.telegram import IndividualAddress from homeassistant import config_entries from homeassistant.components.knx.config_flow import ( @@ -44,6 +45,8 @@ from homeassistant.data_entry_flow import FlowResult, FlowResultType from tests.common import MockConfigEntry +GATEWAY_INDIVIDUAL_ADDRESS = IndividualAddress("1.0.0") + @pytest.fixture(name="knx_setup") def fixture_knx_setup(): @@ -63,6 +66,7 @@ def _gateway_descriptor( """Get mock gw descriptor.""" descriptor = GatewayDescriptor( name="Test", + individual_address=GATEWAY_INDIVIDUAL_ADDRESS, ip_addr=ip, port=port, local_interface="eth0", @@ -352,9 +356,25 @@ async def test_routing_secure_keyfile( assert result4["step_id"] == "secure_knxkeys" assert not result4["errors"] + # test file without backbone key with patch( - "homeassistant.components.knx.config_flow.load_keyring", return_value=True - ): + "homeassistant.components.knx.config_flow.load_keyring" + ) as mock_load_keyring: + mock_keyring = Mock() + mock_keyring.backbone.key = None + mock_load_keyring.return_value = mock_keyring + secure_knxkeys = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_KNXKEY_FILENAME: "testcase.knxkeys", + CONF_KNX_KNXKEY_PASSWORD: "password", + }, + ) + assert secure_knxkeys["type"] == FlowResultType.FORM + assert secure_knxkeys["errors"] == {"base": "keyfile_no_backbone_key"} + + # test valid file + with patch("homeassistant.components.knx.config_flow.load_keyring"): routing_secure_knxkeys = await hass.config_entries.flow.async_configure( result4["flow_id"], { @@ -976,8 +996,11 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant, knx_setup): assert not result["errors"] with patch( - "homeassistant.components.knx.config_flow.load_keyring", return_value=True - ): + "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 secure_knxkeys = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -1031,7 +1054,7 @@ async def test_configure_secure_knxkeys_file_not_found(hass: HomeAssistant): ) assert secure_knxkeys["type"] == FlowResultType.FORM assert secure_knxkeys["errors"] - assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_FILENAME] == "file_not_found" + assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_FILENAME] == "keyfile_not_found" async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant): @@ -1059,7 +1082,39 @@ async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant): ) assert secure_knxkeys["type"] == FlowResultType.FORM assert secure_knxkeys["errors"] - assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_PASSWORD] == "invalid_signature" + assert ( + secure_knxkeys["errors"][CONF_KNX_KNXKEY_PASSWORD] + == "keyfile_invalid_signature" + ) + + +async def test_configure_secure_knxkeys_no_tunnel_for_host(hass: HomeAssistant): + """Test configure secure knxkeys but file was not found.""" + menu_step = await _get_menu_step(hass) + + result = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "secure_knxkeys"}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "secure_knxkeys" + 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 = [] + mock_load_keyring.return_value = mock_keyring + secure_knxkeys = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_KNX_KNXKEY_FILENAME: "testcase.knxkeys", + CONF_KNX_KNXKEY_PASSWORD: "password", + }, + ) + assert secure_knxkeys["type"] == FlowResultType.FORM + assert secure_knxkeys["errors"] == {"base": "keyfile_no_tunnel_for_host"} async def test_options_flow_connection_type( @@ -1185,9 +1240,7 @@ 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", return_value=True - ): + with patch("homeassistant.components.knx.config_flow.load_keyring"): secure_knxkeys = await hass.config_entries.options.async_configure( result4["flow_id"], {