From a09f39b521671fd4f71316dae7600b47625c3bd6 Mon Sep 17 00:00:00 2001 From: rrooggiieerr Date: Wed, 16 Sep 2026 06:05:29 +0200 Subject: [PATCH] Add my-PV reauth flow (#181674) Co-authored-by: Robert Resch Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com> --- homeassistant/components/my_pv/config_flow.py | 47 ++++++++ .../components/my_pv/quality_scale.yaml | 2 +- homeassistant/components/my_pv/strings.json | 14 ++- tests/components/my_pv/test_config_flow.py | 104 ++++++++++++++++++ 4 files changed, 164 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/my_pv/config_flow.py b/homeassistant/components/my_pv/config_flow.py index 78811ab00af3..966bfdff2ff1 100644 --- a/homeassistant/components/my_pv/config_flow.py +++ b/homeassistant/components/my_pv/config_flow.py @@ -1,5 +1,6 @@ """Config flow for the my-PV integration.""" +from collections.abc import Mapping import logging from typing import Any, Final, override @@ -213,3 +214,49 @@ class MyPVConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, description_placeholders=self.context["title_placeholders"], ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth upon an authentication error.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> ConfigFlowResult: + """Confirm reauth dialog.""" + errors: dict[str, str] = {} + + reauth_entry = self._get_reauth_entry() + if user_input is not None: + user_input = {**reauth_entry.data, **user_input} + host = user_input[CONF_HOST] + password = user_input[CONF_PASSWORD] + + device = MyPVLocalDevice(host, password) + try: + if not await device.connect(): + errors[CONF_BASE] = "cannot_connect" + except MyPVAuthenticationError: + errors[CONF_PASSWORD] = "invalid_password" + finally: + await device.disconnect() + + if not errors: + await self.async_set_unique_id(device.serial_number) + self._abort_if_unique_id_mismatch() + data = { + CONF_PASSWORD: password, + } + return self.async_update_reload_and_abort( + reauth_entry, data_updates=data + ) + + data_schema = self.add_suggested_values_to_schema(AUTH_SCHEMA, user_input or {}) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=data_schema, + errors=errors, + description_placeholders=self.context["title_placeholders"], + ) diff --git a/homeassistant/components/my_pv/quality_scale.yaml b/homeassistant/components/my_pv/quality_scale.yaml index 4c73587b9000..87a42b5e1a2c 100644 --- a/homeassistant/components/my_pv/quality_scale.yaml +++ b/homeassistant/components/my_pv/quality_scale.yaml @@ -42,7 +42,7 @@ rules: integration-owner: done log-when-unavailable: todo parallel-updates: todo - reauthentication-flow: todo + reauthentication-flow: done test-coverage: todo # Gold tier rules diff --git a/homeassistant/components/my_pv/strings.json b/homeassistant/components/my_pv/strings.json index 26662bfd3060..28a5a13ba383 100644 --- a/homeassistant/components/my_pv/strings.json +++ b/homeassistant/components/my_pv/strings.json @@ -6,7 +6,8 @@ "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%]", + "unique_id_mismatch": "The device serial number does not match the original device." }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -36,6 +37,15 @@ "description": "[%key:component::my_pv::common::discovery_description%]", "title": "{name}" }, + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "password": "[%key:component::my_pv::common::password_description%]" + }, + "title": "{name} password" + }, "user": { "data": { "host": "[%key:common::config_flow::data::host%]" @@ -49,7 +59,7 @@ }, "exceptions": { "auth_error": { - "message": "Authentication failed. Remove and add the integration again with the current device password." + "message": "Authentication failed, please reauthenticate." }, "cannot_connect": { "message": "[%key:common::config_flow::error::cannot_connect%]" diff --git a/tests/components/my_pv/test_config_flow.py b/tests/components/my_pv/test_config_flow.py index 88d88da1ff01..c5531c3474d0 100644 --- a/tests/components/my_pv/test_config_flow.py +++ b/tests/components/my_pv/test_config_flow.py @@ -442,3 +442,107 @@ async def test_step_discovery_auth_wrong_password( CONF_PASSWORD: "test-password", } assert result["result"].unique_id == ELWA2_SERIAL_NUMBER + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_step_reauth( + hass: HomeAssistant, + mock_my_pv_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test for reauth.""" + 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 not result["errors"] + + 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" + + updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert updated_entry.data[CONF_PASSWORD] == "new-password" + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_step_reauth_wrong_password( + hass: HomeAssistant, + mock_my_pv_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test for reauth with an incorrect password.""" + 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 not result["errors"] + + mock_my_pv_client.connect.side_effect = MyPVAuthenticationError() + + 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"]["password"] == "invalid_password" + + mock_my_pv_client.connect.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"] == "reauth_successful" + + updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert updated_entry.data[CONF_PASSWORD] == "new-password" + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_step_reauth_cannot_connect( + hass: HomeAssistant, + mock_my_pv_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test for reauth if we can not connect to device.""" + 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 not result["errors"] + + mock_my_pv_client.connect.return_value = False + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PASSWORD: "new-password"} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"]["base"] == "cannot_connect" + + mock_my_pv_client.connect.return_value = True + + 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" + + updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert updated_entry.data[CONF_PASSWORD] == "new-password"