From 07c2df3b2744a68f9ec3ad2a0be689bb631261d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 2 Jun 2026 13:42:49 +0000 Subject: [PATCH 1/2] Vistapool: add reconfiguration flow Lets the user proactively update the stored Vistapool password from Settings -> Devices & services without removing and re-adding the integration. Mirrors the reauthentication flow's UX (password-only form, username displayed in the description, account-mismatch check against the cloud user_id) but is user-initiated rather than HA-triggered. Flips the Gold-tier reconfiguration-flow rule from todo to done. --- .../components/vistapool/config_flow.py | 37 ++++++++++ .../components/vistapool/quality_scale.yaml | 2 +- .../components/vistapool/strings.json | 14 +++- .../components/vistapool/test_config_flow.py | 72 +++++++++++++++++++ 4 files changed, 123 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vistapool/config_flow.py b/homeassistant/components/vistapool/config_flow.py index 15f6346c53c224..760a115bc5c1fb 100644 --- a/homeassistant/components/vistapool/config_flow.py +++ b/homeassistant/components/vistapool/config_flow.py @@ -22,6 +22,8 @@ } ) +RECONFIGURE_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): cv.string}) + class VistapoolConfigFlow(ConfigFlow, domain=DOMAIN): """Vistapool config flow (one entry per Hayward account).""" @@ -74,3 +76,38 @@ async def async_step_user( return self.async_show_form( step_id="user", data_schema=AUTH_SCHEMA, errors=errors ) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Let the user proactively update the stored Vistapool password.""" + errors: dict[str, str] = {} + entry = self._get_reconfigure_entry() + username = entry.data[CONF_USERNAME] + + if user_input is not None: + password = user_input[CONF_PASSWORD] + session = async_get_clientsession(self.hass) + auth = AquariteAuth(session, username, password) + try: + await auth.authenticate() + except AuthenticationError: + errors["base"] = "invalid_auth" + except AquariteError: + errors["base"] = "cannot_connect" + except Exception: + _LOGGER.exception("Unexpected error during reconfiguration") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(auth.user_id) + self._abort_if_unique_id_mismatch(reason="account_mismatch") + return self.async_update_reload_and_abort( + entry, data_updates={CONF_PASSWORD: password} + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=RECONFIGURE_SCHEMA, + description_placeholders={"username": username}, + errors=errors, + ) diff --git a/homeassistant/components/vistapool/quality_scale.yaml b/homeassistant/components/vistapool/quality_scale.yaml index f325ac8b1422b3..6703dbabc52208 100644 --- a/homeassistant/components/vistapool/quality_scale.yaml +++ b/homeassistant/components/vistapool/quality_scale.yaml @@ -53,7 +53,7 @@ rules: entity-translations: done exception-translations: done icon-translations: done - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: status: exempt comment: No known repair scenarios diff --git a/homeassistant/components/vistapool/strings.json b/homeassistant/components/vistapool/strings.json index 229b0e3216511f..9526f426af1ccc 100644 --- a/homeassistant/components/vistapool/strings.json +++ b/homeassistant/components/vistapool/strings.json @@ -1,8 +1,10 @@ { "config": { "abort": { + "account_mismatch": "The credentials entered are for a different Vistapool account than the one being reconfigured.", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -12,6 +14,16 @@ }, "flow_title": "Vistapool pool controller", "step": { + "reconfigure": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "password": "The new password for your Vistapool account." + }, + "description": "Update the stored password for {username}.", + "title": "Reconfigure Vistapool" + }, "user": { "data": { "password": "Password", diff --git a/tests/components/vistapool/test_config_flow.py b/tests/components/vistapool/test_config_flow.py index 921207e707467e..b91038f673ecbd 100644 --- a/tests/components/vistapool/test_config_flow.py +++ b/tests/components/vistapool/test_config_flow.py @@ -303,3 +303,75 @@ async def test_dhcp_discovery_aborts_when_in_progress( assert second["type"] is FlowResultType.ABORT assert second["reason"] == "already_in_progress" + + +_NEW_PASSWORD = "new-password" + + +async def test_reconfigure_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, + mock_vistapool_client: AsyncMock, +) -> None: + """Test the reconfigure flow updates the stored password and reloads the entry.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PASSWORD: _NEW_PASSWORD} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.data[CONF_PASSWORD] == _NEW_PASSWORD + + +async def test_reconfigure_invalid_auth( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_vistapool_client: AsyncMock, + mock_vistapool_auth: MagicMock, +) -> None: + """Test the reconfigure flow surfaces invalid_auth and recovers on retry.""" + mock_config_entry.add_to_hass(hass) + mock_vistapool_auth.authenticate.side_effect = AuthenticationError + + result = await mock_config_entry.start_reconfigure_flow(hass) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PASSWORD: _NEW_PASSWORD} + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + mock_vistapool_auth.authenticate.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PASSWORD: _NEW_PASSWORD} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.data[CONF_PASSWORD] == _NEW_PASSWORD + + +async def test_reconfigure_account_mismatch( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_vistapool_client: AsyncMock, + mock_vistapool_auth: MagicMock, +) -> None: + """Test the reconfigure flow aborts when credentials belong to a different account.""" + mock_config_entry.add_to_hass(hass) + mock_vistapool_auth.user_id = "a-different-firebase-uid" + + result = await mock_config_entry.start_reconfigure_flow(hass) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PASSWORD: _NEW_PASSWORD} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "account_mismatch" From 5bbadf01d92cd841e2c3bb4535b23c5470e769a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 2 Jun 2026 13:49:53 +0000 Subject: [PATCH 2/2] Vistapool: assert entry reload count in reconfigure tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit async_update_reload_and_abort triggers async_setup_entry via the config entry reload. Asserting on mock_setup_entry.call_count pins that the reload actually happens (1) on the happy path and the invalid-auth recovery, and that it does NOT happen (0) on the account-mismatch abort — so the abort cleanly refuses to swap the entry to a different cloud account rather than silently re-setting it up with the new credentials. --- tests/components/vistapool/test_config_flow.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/components/vistapool/test_config_flow.py b/tests/components/vistapool/test_config_flow.py index b91038f673ecbd..2057fd4490fe63 100644 --- a/tests/components/vistapool/test_config_flow.py +++ b/tests/components/vistapool/test_config_flow.py @@ -328,11 +328,13 @@ async def test_reconfigure_flow( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" assert mock_config_entry.data[CONF_PASSWORD] == _NEW_PASSWORD + assert mock_setup_entry.call_count == 1 async def test_reconfigure_invalid_auth( hass: HomeAssistant, mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, mock_vistapool_client: AsyncMock, mock_vistapool_auth: MagicMock, ) -> None: @@ -356,11 +358,13 @@ async def test_reconfigure_invalid_auth( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" assert mock_config_entry.data[CONF_PASSWORD] == _NEW_PASSWORD + assert mock_setup_entry.call_count == 1 async def test_reconfigure_account_mismatch( hass: HomeAssistant, mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, mock_vistapool_client: AsyncMock, mock_vistapool_auth: MagicMock, ) -> None: @@ -375,3 +379,4 @@ async def test_reconfigure_account_mismatch( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "account_mismatch" + assert mock_setup_entry.call_count == 0