From 5340a33367217c6a6b5ddd55b0f945010116e340 Mon Sep 17 00:00:00 2001 From: burmistrzak <61958704+burmistrzak@users.noreply.github.com> Date: Sun, 30 Aug 2026 10:54:39 +0200 Subject: [PATCH] Support Shellies with `enhanced_security` and HTTP (#180438) --- .../components/shelly/config_flow.py | 18 +- tests/components/shelly/test_config_flow.py | 154 ++++++++++++++++-- 2 files changed, 138 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 5ebdddf23519..9ebc4499fa2c 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -240,14 +240,6 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): return {} return {CONF_VERIFY_SSL: verify_ssl} - @staticmethod - def _check_enhanced_security(info: dict[str, Any], port: int) -> int: - """Return HTTPS port if device reports enhanced_security is enabled.""" - if info.get("enhanced_security"): - return DEFAULT_HTTPS_PORT - - return port - @staticmethod def _get_name_from_mac_and_ble_model( mac: str, parsed_data: dict[str, int | str] @@ -449,7 +441,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured({CONF_HOST: host}) self.host = host - self.port = self._check_enhanced_security(self.info, port) + self.port = port self.verify_ssl = verify_ssl if get_info_auth(self.info): @@ -1044,8 +1036,6 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): # Device appeared on network but can't connect - allow retry return None - self.port = self._check_enhanced_security(self.info, self.port) - if get_info_auth(self.info): # Device requires authentication - show credentials step return await self.async_step_credentials() @@ -1190,7 +1180,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): await self._async_handle_zeroconf_mac_discovery(mac, host, port) self.host = host - self.port = self._check_enhanced_security(self.info, port) + self.port = port self.verify_ssl = verify_ssl self.context.update( { @@ -1276,8 +1266,6 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): if get_device_entry_gen(reauth_entry) != 1: user_input[CONF_USERNAME] = "admin" - port = self._check_enhanced_security(info, port) - try: await validate_input( self.hass, host, port, info, user_input, verify_ssl @@ -1332,8 +1320,6 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(info[CONF_MAC]) self._abort_if_unique_id_mismatch(reason="another_device") - port = self._check_enhanced_security(info, port) - data_updates: dict[str, Any] = { CONF_HOST: host, CONF_PORT: port, diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index aa52014de000..d9d7c65e75f1 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -101,6 +101,15 @@ DISCOVERY_INFO_WRONG_NAME = ZeroconfServiceInfo( properties={ATTR_PROPERTIES_ID: "shelly2pm-AABBCCDDEEFF"}, type="mock_type", ) +DISCOVERY_INFO_HTTPS = ZeroconfServiceInfo( + ip_address=ip_address("1.1.1.1"), + ip_addresses=[ip_address("1.1.1.1")], + hostname="mock_hostname", + name="shelly1pm-12345", + port=DEFAULT_HTTPS_PORT, + properties={ATTR_PROPERTIES_ID: "shelly1pm-12345"}, + type="mock_type", +) # BLE manufacturer data with RPC-over-BLE enabled (flag bit 2 set) BLE_MANUFACTURER_DATA_RPC = { @@ -569,7 +578,7 @@ async def test_form_enhanced_security( mock_setup_entry: AsyncMock, mock_setup: AsyncMock, ) -> None: - """Test manual setup on port 80 with enhanced_security upgrades to 443.""" + """Test manual setup on port 80 with enhanced_security keeps port 80.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -593,11 +602,10 @@ async def test_form_enhanced_security( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "1.1.1.1", - CONF_PORT: DEFAULT_HTTPS_PORT, + CONF_PORT: DEFAULT_HTTP_PORT, CONF_MODEL: model, CONF_SLEEP_PERIOD: 0, CONF_GEN: gen, - CONF_VERIFY_SSL: False, } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -641,6 +649,46 @@ async def test_form_enhanced_security_older_firmware( assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_enhanced_security_with_https_port( + hass: HomeAssistant, + mock_rpc_device: Mock, + mock_setup_entry: AsyncMock, + mock_setup: AsyncMock, +) -> None: + """Test manual setup on port 443 with enhanced_security keeps port as 443.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + + with patch( + "homeassistant.components.shelly.config_flow.get_info", + return_value={ + "mac": "test-mac", + "model": MODEL_PLUS_2PM, + "auth": False, + "gen": 2, + "enhanced_security": True, + }, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_PORT: DEFAULT_HTTPS_PORT}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_HTTPS_PORT, + CONF_MODEL: MODEL_PLUS_2PM, + CONF_SLEEP_PERIOD: 0, + CONF_GEN: 2, + CONF_VERIFY_SSL: False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_user_flow_overrides_existing_discovery( hass: HomeAssistant, mock_rpc_device: Mock, @@ -2372,7 +2420,7 @@ async def test_zeroconf_enhanced_security( mock_setup_entry: AsyncMock, mock_setup: AsyncMock, ) -> None: - """Test zeroconf discovery with enhanced_security upgrades port to 443.""" + """Test zeroconf discovery with enhanced_security does not upgrade port from 80.""" with patch( "homeassistant.components.shelly.config_flow.get_info", return_value=get_info ): @@ -2390,6 +2438,58 @@ async def test_zeroconf_enhanced_security( ) assert context["title_placeholders"]["name"] == "shelly1pm-12345" assert context["confirm_only"] is True + assert context["configuration_url"] == "http://1.1.1.1" + assert result["step_id"] == "confirm_discovery" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Test name" + assert result["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_HTTP_PORT, + CONF_MODEL: model, + CONF_SLEEP_PERIOD: 0, + CONF_GEN: gen, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf_enhanced_security_with_https_port( + hass: HomeAssistant, + mock_rpc_device: Mock, + mock_setup_entry: AsyncMock, + mock_setup: AsyncMock, +) -> None: + """Test zeroconf discovery with enhanced_security and HTTPS port keeps port as 443.""" + with patch( + "homeassistant.components.shelly.config_flow.get_info", + return_value={ + "mac": "test-mac", + "model": MODEL_PLUS_2PM, + "auth": False, + "gen": 2, + "enhanced_security": True, + }, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO_HTTPS, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + context = next( + flow["context"] + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + assert context["title_placeholders"]["name"] == "shelly1pm-12345" + assert context["confirm_only"] is True assert context["configuration_url"] == "https://1.1.1.1" assert result["step_id"] == "confirm_discovery" @@ -2404,9 +2504,9 @@ async def test_zeroconf_enhanced_security( CONF_HOST: "1.1.1.1", CONF_PORT: DEFAULT_HTTPS_PORT, CONF_VERIFY_SSL: False, - CONF_MODEL: model, + CONF_MODEL: MODEL_PLUS_2PM, CONF_SLEEP_PERIOD: 0, - CONF_GEN: gen, + CONF_GEN: 2, } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -2796,7 +2896,7 @@ async def test_reauth_enhanced_security( hass: HomeAssistant, mock_rpc_device: Mock, ) -> None: - """Test reauth flow with enhanced_security upgrades port to 443.""" + """Test reauth flow with enhanced_security does not upgrade port from 80.""" entry = MockConfigEntry( domain=DOMAIN, unique_id="test-mac", @@ -2827,11 +2927,10 @@ async def test_reauth_enhanced_security( assert result["reason"] == "reauth_successful" assert entry.data == { CONF_HOST: "0.0.0.0", - CONF_PORT: DEFAULT_HTTPS_PORT, + CONF_PORT: DEFAULT_HTTP_PORT, CONF_GEN: 2, CONF_USERNAME: "admin", CONF_PASSWORD: "test password", - CONF_VERIFY_SSL: False, } @@ -3465,15 +3564,39 @@ async def test_reconfigure_with_exception( assert entry.data == {CONF_HOST: "10.10.10.10", CONF_PORT: 99, CONF_GEN: 2} +@pytest.mark.parametrize( + ("port", "entry_data"), + [ + ( + DEFAULT_HTTP_PORT, + { + CONF_HOST: "10.10.10.10", + CONF_PORT: 80, + CONF_GEN: 2, + }, + ), + ( + DEFAULT_HTTPS_PORT, + { + CONF_HOST: "10.10.10.10", + CONF_PORT: 443, + CONF_GEN: 2, + CONF_VERIFY_SSL: False, + }, + ), + ], +) async def test_reconfigure_enhanced_security( hass: HomeAssistant, mock_rpc_device: Mock, + port: int, + entry_data: dict[str, int | str], ) -> None: - """Test reconfigure flow with enhanced_security upgrades port to 443.""" + """Test reconfigure flow with enhanced_security does not upgrade port from 80 or 443.""" entry = MockConfigEntry( domain=DOMAIN, unique_id="test-mac", - data={CONF_HOST: "0.0.0.0", CONF_GEN: 2}, + data={CONF_HOST: "0.0.0.0", CONF_PORT: port, CONF_GEN: 2}, ) entry.add_to_hass(hass) @@ -3494,17 +3617,12 @@ async def test_reconfigure_enhanced_security( ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_HOST: "10.10.10.10", CONF_PORT: DEFAULT_HTTP_PORT}, + user_input={CONF_HOST: "10.10.10.10", CONF_PORT: port}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" - assert entry.data == { - CONF_HOST: "10.10.10.10", - CONF_PORT: DEFAULT_HTTPS_PORT, - CONF_GEN: 2, - CONF_VERIFY_SSL: False, - } + assert entry.data == entry_data async def test_zeroconf_rejects_ipv6(hass: HomeAssistant) -> None: