diff --git a/homeassistant/components/pooldose/config_flow.py b/homeassistant/components/pooldose/config_flow.py index 73eb12d7562..d15de677960 100644 --- a/homeassistant/components/pooldose/config_flow.py +++ b/homeassistant/components/pooldose/config_flow.py @@ -114,32 +114,72 @@ class PooldoseConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" - if not user_input: - return self.async_show_form( - step_id="user", - data_schema=SCHEMA_DEVICE, + if user_input is not None: + host = user_input[CONF_HOST] + serial_number, api_versions, errors = await self._validate_host(host) + if errors: + return self.async_show_form( + step_id="user", + data_schema=SCHEMA_DEVICE, + errors=errors, + # Handle API version info for error display; pass version info when available + # or None when api_versions is None to avoid displaying version details + description_placeholders={ + "api_version_is": api_versions.get("api_version_is") or "", + "api_version_should": api_versions.get("api_version_should") + or "", + } + if api_versions + else None, + ) + + await self.async_set_unique_id(serial_number, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=f"PoolDose {serial_number}", + data={CONF_HOST: host}, ) - host = user_input[CONF_HOST] - serial_number, api_versions, errors = await self._validate_host(host) - if errors: - return self.async_show_form( - step_id="user", - data_schema=SCHEMA_DEVICE, - errors=errors, - # Handle API version info for error display; pass version info when available - # or None when api_versions is None to avoid displaying version details - description_placeholders={ - "api_version_is": api_versions.get("api_version_is") or "", - "api_version_should": api_versions.get("api_version_should") or "", - } - if api_versions - else None, - ) - - await self.async_set_unique_id(serial_number, raise_on_progress=False) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=f"PoolDose {serial_number}", - data={CONF_HOST: host}, + return self.async_show_form( + step_id="user", + data_schema=SCHEMA_DEVICE, + ) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfigure to change the device host/IP for an existing entry.""" + if user_input is not None: + host = user_input[CONF_HOST] + serial_number, api_versions, errors = await self._validate_host(host) + if errors: + return self.async_show_form( + step_id="reconfigure", + data_schema=SCHEMA_DEVICE, + errors=errors, + # Handle API version info for error display identical to other steps + description_placeholders={ + "api_version_is": api_versions.get("api_version_is") or "", + "api_version_should": api_versions.get("api_version_should") + or "", + } + if api_versions + else None, + ) + + # Ensure new serial number matches the existing entry unique_id (serial number) + if serial_number != self._get_reconfigure_entry().unique_id: + return self.async_abort(reason="wrong_device") + + # Update the existing config entry with the new host and schedule reload + return self.async_update_reload_and_abort( + self._get_reconfigure_entry(), data_updates={CONF_HOST: host} + ) + + return self.async_show_form( + step_id="reconfigure", + # Pre-fill with current host from the entry being reconfigured + data_schema=self.add_suggested_values_to_schema( + SCHEMA_DEVICE, self._get_reconfigure_entry().data + ), ) diff --git a/homeassistant/components/pooldose/quality_scale.yaml b/homeassistant/components/pooldose/quality_scale.yaml index 07514d681af..7c8ce99715f 100644 --- a/homeassistant/components/pooldose/quality_scale.yaml +++ b/homeassistant/components/pooldose/quality_scale.yaml @@ -60,7 +60,7 @@ rules: entity-translations: done exception-translations: done icon-translations: done - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: status: exempt comment: This integration does not provide repair issues, as it is designed for a single PoolDose device with a fixed configuration. diff --git a/homeassistant/components/pooldose/strings.json b/homeassistant/components/pooldose/strings.json index c80353c5004..3d50d28faac 100644 --- a/homeassistant/components/pooldose/strings.json +++ b/homeassistant/components/pooldose/strings.json @@ -4,7 +4,9 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "no_device_info": "Unable to retrieve device information", - "no_serial_number": "No serial number found on the device" + "no_serial_number": "No serial number found on the device", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", + "wrong_device": "The provided device does not match the configured device" }, "error": { "api_not_set": "API version not found in device response. Device firmware may not be compatible with this integration.", @@ -20,6 +22,14 @@ "description": "A PoolDose device was found on your network at {ip} with MAC address {mac}.\n\nDo you want to add {name} to Home Assistant?", "title": "Confirm DHCP discovered PoolDose device" }, + "reconfigure": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "[%key:component::pooldose::config::step::user::data_description::host%]" + } + }, "user": { "data": { "host": "[%key:common::config_flow::data::host%]" diff --git a/tests/components/pooldose/test_config_flow.py b/tests/components/pooldose/test_config_flow.py index 354808c51d3..930d5cbd774 100644 --- a/tests/components/pooldose/test_config_flow.py +++ b/tests/components/pooldose/test_config_flow.py @@ -1,8 +1,10 @@ """Test the PoolDose config flow.""" +from datetime import timedelta from typing import Any from unittest.mock import AsyncMock +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.pooldose.const import DOMAIN @@ -14,7 +16,7 @@ from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from .conftest import RequestStatus -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: @@ -426,3 +428,80 @@ async def test_dhcp_preserves_existing_mac( assert entry.data[CONF_HOST] == "192.168.0.123" # IP was updated assert entry.data[CONF_MAC] == "existing11aabb" # MAC remains unchanged assert entry.data[CONF_MAC] != "different22ccdd" # Not updated to new MAC + + +async def _start_reconfigure_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, host_ip: str +) -> Any: + """Initialize a reconfigure flow for PoolDose and submit new host.""" + mock_config_entry.add_to_hass(hass) + + reconfigure_result = await mock_config_entry.start_reconfigure_flow(hass) + + assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["step_id"] == "reconfigure" + + return await hass.config_entries.flow.async_configure( + reconfigure_result["flow_id"], {CONF_HOST: host_ip} + ) + + +async def test_reconfigure_flow_success( + hass: HomeAssistant, + mock_pooldose_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test successful reconfigure updates host and reloads entry.""" + # Ensure the mocked device returns the same serial number as the + # config entry so the reconfigure flow matches the device + mock_pooldose_client.device_info = {"SERIAL_NUMBER": mock_config_entry.unique_id} + + result = await _start_reconfigure_flow(hass, mock_config_entry, "192.168.0.200") + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + # Config entry should have updated host + assert mock_config_entry.data.get(CONF_HOST) == "192.168.0.200" + + freezer.tick(timedelta(seconds=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Config entry should have updated host + entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert entry is not None + assert entry.data.get(CONF_HOST) == "192.168.0.200" + + +async def test_reconfigure_flow_cannot_connect( + hass: HomeAssistant, + mock_pooldose_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfigure shows cannot_connect when device unreachable.""" + mock_pooldose_client.connect.return_value = RequestStatus.HOST_UNREACHABLE + + result = await _start_reconfigure_flow(hass, mock_config_entry, "192.168.0.200") + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_reconfigure_flow_wrong_device( + hass: HomeAssistant, + mock_pooldose_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfigure aborts when serial number doesn't match existing entry.""" + # Return device info with different serial number + mock_pooldose_client.device_info = {"SERIAL_NUMBER": "OTHER123"} + + result = await _start_reconfigure_flow(hass, mock_config_entry, "192.168.0.200") + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "wrong_device"