From 2fdfcd6badc88fd40da3671d7a35756ba5bed1ee Mon Sep 17 00:00:00 2001 From: mettolen <1007649+mettolen@users.noreply.github.com> Date: Mon, 29 Dec 2025 12:13:35 +0200 Subject: [PATCH] Add reconfigure flow to Airobot integration (#159810) --- .../components/airobot/config_flow.py | 36 ++++++ .../components/airobot/quality_scale.yaml | 2 +- homeassistant/components/airobot/strings.json | 17 ++- tests/components/airobot/test_config_flow.py | 117 ++++++++++++++++++ 4 files changed, 170 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airobot/config_flow.py b/homeassistant/components/airobot/config_flow.py index 3bdd9273e82..1b3485c8b4e 100644 --- a/homeassistant/components/airobot/config_flow.py +++ b/homeassistant/components/airobot/config_flow.py @@ -175,6 +175,42 @@ class AirobotConfigFlow(BaseConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, 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] = {} + reconfigure_entry = self._get_reconfigure_entry() + + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + # Verify the device ID matches the existing config entry + await self.async_set_unique_id(info.device_id) + self._abort_if_unique_id_mismatch(reason="wrong_device") + + return self.async_update_reload_and_abort( + reconfigure_entry, + data_updates=user_input, + title=info.title, + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=self.add_suggested_values_to_schema( + STEP_USER_DATA_SCHEMA, reconfigure_entry.data + ), + errors=errors, + ) + async def async_step_reauth( self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: diff --git a/homeassistant/components/airobot/quality_scale.yaml b/homeassistant/components/airobot/quality_scale.yaml index b7213eb04a5..bf722a63597 100644 --- a/homeassistant/components/airobot/quality_scale.yaml +++ b/homeassistant/components/airobot/quality_scale.yaml @@ -58,7 +58,7 @@ rules: entity-translations: done exception-translations: done icon-translations: done - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: status: exempt comment: This integration doesn't have any cases where raising an issue is needed. diff --git a/homeassistant/components/airobot/strings.json b/homeassistant/components/airobot/strings.json index f6808229416..77b264d1c44 100644 --- a/homeassistant/components/airobot/strings.json +++ b/homeassistant/components/airobot/strings.json @@ -2,7 +2,9 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "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%]", + "wrong_device": "Device ID does not match the existing configuration. Please use the correct device credentials." }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -28,6 +30,19 @@ }, "description": "The authentication for Airobot thermostat at {host} (Device ID: {username}) has expired. Please enter the password to reauthenticate. Find the password in the thermostat settings menu under Connectivity → Mobile app." }, + "reconfigure": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]", + "username": "Device ID" + }, + "data_description": { + "host": "[%key:component::airobot::config::step::user::data_description::host%]", + "password": "[%key:component::airobot::config::step::user::data_description::password%]", + "username": "[%key:component::airobot::config::step::user::data_description::username%]" + }, + "description": "Update your Airobot thermostat connection details. Note: The Device ID must remain the same as the original configuration." + }, "user": { "data": { "host": "[%key:common::config_flow::data::host%]", diff --git a/tests/components/airobot/test_config_flow.py b/tests/components/airobot/test_config_flow.py index 5a1b4978909..3e66556c850 100644 --- a/tests/components/airobot/test_config_flow.py +++ b/tests/components/airobot/test_config_flow.py @@ -307,3 +307,120 @@ async def test_reauth_flow_errors( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_config_entry.data[CONF_PASSWORD] == "new-password" + + +async def test_reconfigure_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_airobot_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfiguration flow.""" + mock_config_entry.add_to_hass(hass) + + # Trigger reconfiguration + result = await mock_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + # Update configuration (e.g., new IP address and password) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "192.168.1.200", + CONF_USERNAME: "T01A1B2C3", + CONF_PASSWORD: "new-password", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.data[CONF_HOST] == "192.168.1.200" + assert mock_config_entry.data[CONF_USERNAME] == "T01A1B2C3" + assert mock_config_entry.data[CONF_PASSWORD] == "new-password" + + +async def test_reconfigure_flow_wrong_device( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_airobot_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfiguration flow with wrong device.""" + mock_config_entry.add_to_hass(hass) + + # Trigger reconfiguration + result = await mock_config_entry.start_reconfigure_flow(hass) + + # Try to reconfigure with a different device ID + mock_airobot_client.get_settings.return_value.device_name = "Different Device" + mock_airobot_client.get_statuses.return_value.device_id = "T01DIFFERENT" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "192.168.1.200", + CONF_USERNAME: "T01DIFFERENT", + CONF_PASSWORD: "test-password", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "wrong_device" + + +@pytest.mark.parametrize( + ("exception", "error_base"), + [ + (AirobotAuthError("Invalid credentials"), "invalid_auth"), + (AirobotConnectionError("Connection failed"), "cannot_connect"), + (Exception("Unknown error"), "unknown"), + ], +) +async def test_reconfigure_flow_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_airobot_client: AsyncMock, + mock_config_entry: MockConfigEntry, + exception: Exception, + error_base: str, +) -> None: + """Test reconfiguration flow with errors.""" + mock_config_entry.add_to_hass(hass) + + # Trigger reconfiguration + result = await mock_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + # First attempt with error + mock_airobot_client.get_statuses.side_effect = exception + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "192.168.1.200", + CONF_USERNAME: "T01A1B2C3", + CONF_PASSWORD: "wrong-password", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error_base} + + # Recover from error + mock_airobot_client.get_statuses.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "192.168.1.200", + CONF_USERNAME: "T01A1B2C3", + CONF_PASSWORD: "new-password", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.data[CONF_HOST] == "192.168.1.200" + assert mock_config_entry.data[CONF_PASSWORD] == "new-password"