From 11af0a2d04048c9cfdb2445192f29715a73e2a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 12 Feb 2026 22:27:30 +0100 Subject: [PATCH] Add reauthentication flow to Homevolt (#162868) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- .../components/homevolt/config_flow.py | 39 +++++++++ .../components/homevolt/quality_scale.yaml | 2 +- .../components/homevolt/strings.json | 14 +++- tests/components/homevolt/test_config_flow.py | 82 +++++++++++++++++++ 4 files changed, 135 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homevolt/config_flow.py b/homeassistant/components/homevolt/config_flow.py index 8e64e955e06..442c2f54be6 100644 --- a/homeassistant/components/homevolt/config_flow.py +++ b/homeassistant/components/homevolt/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -85,6 +86,44 @@ class HomevoltConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle reauth on authentication failure.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reauth confirmation with new credentials.""" + reauth_entry = self._get_reauth_entry() + host = reauth_entry.data[CONF_HOST] + errors: dict[str, str] = {} + + if user_input is not None: + password = user_input[CONF_PASSWORD] + websession = async_get_clientsession(self.hass) + client = Homevolt(host, password, websession=websession) + errors = await self.check_status(client) + + if not errors: + device_id = client.unique_id + await self.async_set_unique_id(device_id) + self._abort_if_unique_id_mismatch(reason="wrong_account") + + return self.async_update_reload_and_abort( + reauth_entry, + unique_id=device_id, + data_updates={CONF_HOST: host, CONF_PASSWORD: password}, + ) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=STEP_CREDENTIALS_DATA_SCHEMA, + errors=errors, + description_placeholders={"host": host}, + ) + async def async_step_credentials( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/homevolt/quality_scale.yaml b/homeassistant/components/homevolt/quality_scale.yaml index abac19b7cfe..9c7801309b4 100644 --- a/homeassistant/components/homevolt/quality_scale.yaml +++ b/homeassistant/components/homevolt/quality_scale.yaml @@ -38,7 +38,7 @@ rules: integration-owner: done log-when-unavailable: todo parallel-updates: done - reauthentication-flow: todo + reauthentication-flow: done test-coverage: todo # Gold diff --git a/homeassistant/components/homevolt/strings.json b/homeassistant/components/homevolt/strings.json index d9f6b3c672c..f58f2588c2d 100644 --- a/homeassistant/components/homevolt/strings.json +++ b/homeassistant/components/homevolt/strings.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "wrong_account": "The device you authenticated with is different from the one configured. Re-authenticate with the same Homevolt battery." }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -18,6 +20,16 @@ }, "description": "This device requires a password to connect. Please enter the password for {host}." }, + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "password": "[%key:component::homevolt::config::step::credentials::data_description::password%]" + }, + "description": "Authentication failed for the Homevolt battery at {host}. Please re-enter the password.", + "title": "[%key:common::config_flow::title::reauth%]" + }, "user": { "data": { "host": "[%key:common::config_flow::data::host%]" diff --git a/tests/components/homevolt/test_config_flow.py b/tests/components/homevolt/test_config_flow.py index 3f7d893c8f9..b931c852bfd 100644 --- a/tests/components/homevolt/test_config_flow.py +++ b/tests/components/homevolt/test_config_flow.py @@ -223,3 +223,85 @@ async def test_credentials_step_invalid_password( } assert result["result"].unique_id == "40580137858664" assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_success( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_homevolt_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test successful reauthentication flow.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["description_placeholders"] == { + "host": "127.0.0.1", + "name": "Homevolt", + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert mock_config_entry.unique_id == "40580137858664" + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "new-password", + } + + +@pytest.mark.parametrize( + ("exception", "expected_error"), + [ + (HomevoltAuthenticationError, "invalid_auth"), + (HomevoltConnectionError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_reauth_flow_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_homevolt_client: MagicMock, + mock_config_entry: MockConfigEntry, + exception: Exception, + expected_error: str, +) -> None: + """Test reauthentication flow with errors and recovery.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + mock_homevolt_client.update_info.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "wrong-password"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": expected_error} + + mock_homevolt_client.update_info.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "correct-password"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "correct-password", + }