From f9aa307cb2b0be5d01940402d7947c5ae953027b Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Fri, 16 Jan 2026 10:16:34 +0100 Subject: [PATCH] SMA add reconfigure flow (#160743) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- homeassistant/components/sma/config_flow.py | 45 ++++++++ homeassistant/components/sma/strings.json | 15 ++- tests/components/sma/__init__.py | 8 ++ tests/components/sma/test_config_flow.py | 112 +++++++++++++++++++- 4 files changed, 177 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/config_flow.py b/homeassistant/components/sma/config_flow.py index 7d76fbe0ec9..9e9656eb4b9 100644 --- a/homeassistant/components/sma/config_flow.py +++ b/homeassistant/components/sma/config_flow.py @@ -144,6 +144,51 @@ class SmaConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the integration.""" + errors: dict[str, str] = {} + reconf_entry = self._get_reconfigure_entry() + if user_input is not None: + errors, device_info = await self._handle_user_input( + user_input={ + **reconf_entry.data, + **user_input, + } + ) + + if not errors: + await self.async_set_unique_id( + str(device_info["serial"]), raise_on_progress=False + ) + self._abort_if_unique_id_mismatch() + return self.async_update_reload_and_abort( + reconf_entry, + data_updates={ + CONF_HOST: user_input[CONF_HOST], + CONF_SSL: user_input[CONF_SSL], + CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], + CONF_GROUP: user_input[CONF_GROUP], + }, + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=self.add_suggested_values_to_schema( + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL): cv.boolean, + vol.Optional(CONF_GROUP): vol.In(GROUPS), + } + ), + suggested_values=user_input or dict(reconf_entry.data), + ), + errors=errors, + ) + async def async_step_reauth( self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: diff --git a/homeassistant/components/sma/strings.json b/homeassistant/components/sma/strings.json index 07e4047de54..2e065507545 100644 --- a/homeassistant/components/sma/strings.json +++ b/homeassistant/components/sma/strings.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", + "unique_id_mismatch": "You selected a different SMA device than the one this config entry was configured with, this is not allowed." }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -29,6 +31,17 @@ "description": "The SMA integration needs to re-authenticate your connection details", "title": "[%key:common::config_flow::title::reauth%]" }, + "reconfigure": { + "data": { + "group": "[%key:component::sma::config::step::user::data::group%]", + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]", + "ssl": "[%key:common::config_flow::data::ssl%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + }, + "description": "Use the following form to reconfigure your SMA device.", + "title": "Reconfigure SMA Solar Integration" + }, "user": { "data": { "group": "Group", diff --git a/tests/components/sma/__init__.py b/tests/components/sma/__init__.py index eebaf43ccd8..99ae823dd97 100644 --- a/tests/components/sma/__init__.py +++ b/tests/components/sma/__init__.py @@ -35,6 +35,14 @@ MOCK_USER_REAUTH = { CONF_PASSWORD: "new_password", } +MOCK_USER_RECONFIGURE = { + CONF_HOST: "1.1.1.2", + CONF_SSL: True, + CONF_VERIFY_SSL: False, + CONF_GROUP: "user", +} + + MOCK_DHCP_DISCOVERY_INPUT = { CONF_SSL: True, CONF_VERIFY_SSL: False, diff --git a/tests/components/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py index 63130c5cf35..f927f9979da 100644 --- a/tests/components/sma/test_config_flow.py +++ b/tests/components/sma/test_config_flow.py @@ -3,11 +3,12 @@ from unittest.mock import AsyncMock, MagicMock, patch from pysma import SmaAuthenticationException, SmaConnectionException, SmaReadException +from pysma.helpers import DeviceInfo import pytest -from homeassistant.components.sma.const import DOMAIN +from homeassistant.components.sma.const import CONF_GROUP, DOMAIN from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER -from homeassistant.const import CONF_MAC +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_SSL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.device_registry import format_mac @@ -19,6 +20,7 @@ from . import ( MOCK_DHCP_DISCOVERY_INPUT, MOCK_USER_INPUT, MOCK_USER_REAUTH, + MOCK_USER_RECONFIGURE, ) from tests.conftest import MockConfigEntry @@ -311,3 +313,109 @@ async def test_reauth_flow_exceptions( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" + + +async def test_full_flow_reconfigure( + hass: HomeAssistant, + mock_setup_entry: MockConfigEntry, + mock_sma_client: AsyncMock, +) -> None: + """Test the full flow of the config flow.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT, unique_id="123456789") + entry.add_to_hass(hass) + result = await 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"], + user_input=MOCK_USER_RECONFIGURE, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert entry.data[CONF_HOST] == "1.1.1.2" + assert entry.data[CONF_SSL] is True + assert entry.data[CONF_VERIFY_SSL] is False + assert entry.data[CONF_GROUP] == "user" + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (SmaConnectionException, "cannot_connect"), + (SmaAuthenticationException, "invalid_auth"), + (SmaReadException, "cannot_retrieve_device_info"), + (Exception, "unknown"), + ], +) +async def test_full_flow_reconfigure_exceptions( + hass: HomeAssistant, + mock_setup_entry: MockConfigEntry, + mock_sma_client: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test we handle cannot connect error and recover from it.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT, unique_id="123456789") + entry.add_to_hass(hass) + result = await entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + mock_sma_client.new_session.side_effect = exception + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_RECONFIGURE, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_sma_client.new_session.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_USER_RECONFIGURE, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert entry.data[CONF_HOST] == "1.1.1.2" + assert entry.data[CONF_SSL] is True + assert entry.data[CONF_VERIFY_SSL] is False + assert entry.data[CONF_GROUP] == "user" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reconfigure_mismatch_id( + hass: HomeAssistant, + mock_setup_entry: MockConfigEntry, + mock_sma_client: AsyncMock, +) -> None: + """Test when a mismatch happens during reconfigure.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT, unique_id="123456789") + entry.add_to_hass(hass) + result = await entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + # New device, on purpose to demonstrate we can't switch + different_device = DeviceInfo( + manufacturer="SMA", + name="Different SMA Device", + type="Sunny Boy 5.0", + serial=987654321, + sw_version="2.0.0", + ) + mock_sma_client.device_info = AsyncMock(return_value=different_device) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_USER_RECONFIGURE, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "unique_id_mismatch"