diff --git a/homeassistant/components/hotspring/config_flow.py b/homeassistant/components/hotspring/config_flow.py index 22198fe9a35f4..d900ea1f10c7c 100644 --- a/homeassistant/components/hotspring/config_flow.py +++ b/homeassistant/components/hotspring/config_flow.py @@ -1,11 +1,16 @@ """Config flow for Hot Spring.""" -from typing import override +from collections.abc import Mapping +from typing import Any, override from hotspring import HotSpring, HotSpringConnectionError, HotSpringError, Spa import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + SOURCE_RECONFIGURE, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -20,7 +25,7 @@ ) -async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> Spa: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Spa: """Validate the user input allows us to connect.""" api = HotSpring(data[CONF_HOST], session=async_get_clientsession(hass)) spa = await api.update() @@ -36,7 +41,7 @@ class HotSpringConfigFlow(ConfigFlow, domain=DOMAIN): @override async def async_step_user( - self, user_input: dict[str, str] | None = None + self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a flow initiated by the user.""" errors: dict[str, str] = {} @@ -47,6 +52,12 @@ async def async_step_user( errors["base"] = "cannot_connect" else: await self.async_set_unique_id(spa.info.mac_address) + if self.source == SOURCE_RECONFIGURE: + self._abort_if_unique_id_mismatch() + return self.async_update_reload_and_abort( + self._get_reconfigure_entry(), + data_updates={CONF_HOST: user_input[CONF_HOST]}, + ) self._abort_if_unique_id_configured( updates={CONF_HOST: user_input[CONF_HOST]} ) @@ -57,8 +68,21 @@ async def async_step_user( }, ) + suggested_values: Mapping[str, Any] | None = user_input + if suggested_values is None and self.source == SOURCE_RECONFIGURE: + suggested_values = self._get_reconfigure_entry().data + return self.async_show_form( step_id="user", - data_schema=STEP_USER_DATA_SCHEMA, + data_schema=self.add_suggested_values_to_schema( + STEP_USER_DATA_SCHEMA, + suggested_values, + ), errors=errors, ) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the Hot Spring spa.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/hotspring/quality_scale.yaml b/homeassistant/components/hotspring/quality_scale.yaml index 52ca9f01288dd..a783b783b7408 100644 --- a/homeassistant/components/hotspring/quality_scale.yaml +++ b/homeassistant/components/hotspring/quality_scale.yaml @@ -70,7 +70,7 @@ rules: icon-translations: status: exempt comment: Entity relies on standard platform default icons. - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: status: exempt comment: Integration does not raise repair issues. diff --git a/homeassistant/components/hotspring/strings.json b/homeassistant/components/hotspring/strings.json index 02c626577c9ba..32ae2b0cd1fa6 100644 --- a/homeassistant/components/hotspring/strings.json +++ b/homeassistant/components/hotspring/strings.json @@ -2,7 +2,9 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", + "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%]" diff --git a/tests/components/hotspring/test_config_flow.py b/tests/components/hotspring/test_config_flow.py index e758190ebccea..e8cad058d888d 100644 --- a/tests/components/hotspring/test_config_flow.py +++ b/tests/components/hotspring/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock -from hotspring import HotSpringConnectionError, HotSpringError, Spa, SpaBrand, SpaInfo +from hotspring import HotSpringConnectionError, HotSpringError, Spa import pytest from homeassistant.components.hotspring.const import DOMAIN @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, get_schema_suggested_value @pytest.mark.usefixtures("mock_setup_entry", "mock_hotspring") @@ -97,20 +97,7 @@ async def test_form_no_mac_address( hass: HomeAssistant, mock_hotspring: MagicMock, device_fixture: Spa ) -> None: """Test we show user form on missing MAC address and recover.""" - valid_info = device_fixture.info - device_fixture.info = SpaInfo( - hostname="ConnectedSpa_DDEEFF", - root_topic="unknownTopic123", - sna_ready=True, - brand=SpaBrand.HOTSPRING, - brand_name="Hot Spring", - collection="Highlife", - model_name="Relay", - brand_id="1", - collection_id="1", - model_id="1", - volume=335, - ) + device_fixture.info.root_topic = "unknownTopic123" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -123,7 +110,7 @@ async def test_form_no_mac_address( assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} - device_fixture.info = valid_info + device_fixture.info.root_topic = "mySpaAABBCCDDEEFF" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.100"} ) @@ -132,3 +119,98 @@ async def test_form_no_mac_address( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_HOST: "192.168.1.100"} assert result["result"].unique_id == "AA:BB:CC:DD:EE:FF" + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_full_reconfigure_flow_success( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_hotspring: MagicMock, +) -> None: + """Test the full reconfigure flow from start to finish.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reconfigure_flow(hass) + + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + assert result["data_schema"] is not None + assert ( + get_schema_suggested_value(result["data_schema"].schema, CONF_HOST) + == "192.168.1.100" + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "192.168.1.200"} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + assert mock_config_entry.data[CONF_HOST] == "192.168.1.200" + + +async def test_full_reconfigure_flow_unique_id_mismatch( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_hotspring: MagicMock, + device_fixture: Spa, +) -> None: + """Test reconfiguration failure when the unique ID changes.""" + mock_config_entry.add_to_hass(hass) + device_fixture.info.root_topic = "mySpa112233445566" + + result = await mock_config_entry.start_reconfigure_flow(hass) + + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "192.168.1.200"} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "unique_id_mismatch" + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_full_reconfigure_flow_connection_error_and_success( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_hotspring: MagicMock, +) -> None: + """Test reconfigure flow with connection error and recovery.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reconfigure_flow(hass) + + assert result["step_id"] == "user" + assert result["type"] is FlowResultType.FORM + assert result["data_schema"] is not None + assert ( + get_schema_suggested_value(result["data_schema"].schema, CONF_HOST) + == "192.168.1.100" + ) + + mock_hotspring.update.side_effect = HotSpringConnectionError + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "192.168.1.200"} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + assert result["data_schema"] is not None + assert ( + get_schema_suggested_value(result["data_schema"].schema, CONF_HOST) + == "192.168.1.200" + ) + + mock_hotspring.update.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "192.168.1.200"} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.data[CONF_HOST] == "192.168.1.200"