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..2057fd4490fe63 100644 --- a/tests/components/vistapool/test_config_flow.py +++ b/tests/components/vistapool/test_config_flow.py @@ -303,3 +303,80 @@ 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 + 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: + """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 + 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: + """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" + assert mock_setup_entry.call_count == 0