From dbf135a2ef40cb10afeb65515ae4ded5681f929d Mon Sep 17 00:00:00 2001 From: Christophe Gagnier Date: Fri, 4 Sep 2026 02:31:18 -0400 Subject: [PATCH] Reject Hot Spring Spa Network Adapter (SNA) in config flow (hotfix) (#181261) --- .../components/hotspring/config_flow.py | 12 ++++- .../components/hotspring/strings.json | 4 +- .../components/hotspring/test_config_flow.py | 45 +++++++++++++------ 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/hotspring/config_flow.py b/homeassistant/components/hotspring/config_flow.py index 1377c7cf5e63..9967c88f65a2 100644 --- a/homeassistant/components/hotspring/config_flow.py +++ b/homeassistant/components/hotspring/config_flow.py @@ -3,7 +3,13 @@ from collections.abc import Mapping from typing import Any, override -from hotspring import HotSpring, HotSpringConnectionError, HotSpringError, Spa +from hotspring import ( + HotSpring, + HotSpringConnectionError, + HotSpringError, + HotSpringSNADetectedError, + Spa, +) import voluptuous as vol from homeassistant.config_entries import ( @@ -52,6 +58,8 @@ class HotSpringConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: try: spa = await validate_input(self.hass, user_input) + except HotSpringSNADetectedError: + errors["base"] = "sna_device" except HotSpringConnectionError, HotSpringError: errors["base"] = "cannot_connect" else: @@ -101,6 +109,8 @@ class HotSpringConfigFlow(ConfigFlow, domain=DOMAIN): self.discovered_spa = await validate_input( self.hass, {CONF_HOST: discovery_info.host} ) + except HotSpringSNADetectedError: + return self.async_abort(reason="sna_device") except HotSpringConnectionError, HotSpringError: return self.async_abort(reason="cannot_connect") diff --git a/homeassistant/components/hotspring/strings.json b/homeassistant/components/hotspring/strings.json index 4ae8352038d3..8d06bab200b5 100644 --- a/homeassistant/components/hotspring/strings.json +++ b/homeassistant/components/hotspring/strings.json @@ -4,10 +4,12 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", + "sna_device": "The discovered device is a Spa Network Adapter (SNA). Only the Home Network Adapter (HNA) can be configured.", "unique_id_mismatch": "The MAC address does not match the configured device. Please ensure you reconfigure against the same device." }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "sna_device": "The entered address belongs to the Spa Network Adapter (SNA). Please enter the IP address or hostname of your Home Network Adapter (HNA) instead." }, "step": { "user": { diff --git a/tests/components/hotspring/test_config_flow.py b/tests/components/hotspring/test_config_flow.py index 677da008b306..18e620ba849d 100644 --- a/tests/components/hotspring/test_config_flow.py +++ b/tests/components/hotspring/test_config_flow.py @@ -4,7 +4,12 @@ import dataclasses from ipaddress import ip_address from unittest.mock import MagicMock -from hotspring import HotSpringConnectionError, HotSpringError, Spa +from hotspring import ( + HotSpringConnectionError, + HotSpringError, + HotSpringSNADetectedError, + Spa, +) import pytest from homeassistant.components.hotspring.const import DOMAIN @@ -69,14 +74,21 @@ async def test_user_device_exists_abort( @pytest.mark.parametrize( - "exception", - [HotSpringConnectionError, HotSpringError], + ("exception", "error_key"), + [ + (HotSpringConnectionError, "cannot_connect"), + (HotSpringError, "cannot_connect"), + (HotSpringSNADetectedError, "sna_device"), + ], ) @pytest.mark.usefixtures("mock_setup_entry") -async def test_form_cannot_connect( - hass: HomeAssistant, mock_hotspring: MagicMock, exception: type[Exception] +async def test_form_errors( + hass: HomeAssistant, + mock_hotspring: MagicMock, + exception: type[Exception], + error_key: str, ) -> None: - """Test we show user form on Hot Spring connection error and recover.""" + """Test we show user form on error and recover.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -92,7 +104,7 @@ async def test_form_cannot_connect( assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - assert result["errors"] == {"base": "cannot_connect"} + assert result["errors"] == {"base": error_key} mock_hotspring.update.side_effect = None result = await hass.config_entries.flow.async_configure( @@ -158,13 +170,20 @@ async def test_full_zeroconf_flow_implementation(hass: HomeAssistant) -> None: @pytest.mark.parametrize( - "exception", - [HotSpringConnectionError, HotSpringError], + ("exception", "reason"), + [ + (HotSpringConnectionError, "cannot_connect"), + (HotSpringError, "cannot_connect"), + (HotSpringSNADetectedError, "sna_device"), + ], ) -async def test_zeroconf_connection_error( - hass: HomeAssistant, mock_hotspring: MagicMock, exception: type[Exception] +async def test_zeroconf_error( + hass: HomeAssistant, + mock_hotspring: MagicMock, + exception: type[Exception], + reason: str, ) -> None: - """Test we abort zeroconf flow on Hot Spring connection error.""" + """Test we abort zeroconf flow on Hot Spring error.""" mock_hotspring.update.side_effect = exception result = await hass.config_entries.flow.async_init( @@ -174,7 +193,7 @@ async def test_zeroconf_connection_error( ) assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "cannot_connect" + assert result["reason"] == reason @pytest.mark.usefixtures("mock_hotspring")