diff --git a/homeassistant/components/renault/config_flow.py b/homeassistant/components/renault/config_flow.py index a7998af953a..d46f0ff4a80 100644 --- a/homeassistant/components/renault/config_flow.py +++ b/homeassistant/components/renault/config_flow.py @@ -11,7 +11,11 @@ from renault_api.const import AVAILABLE_LOCALES from renault_api.gigya.exceptions import GigyaException import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + SOURCE_RECONFIGURE, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import CONF_KAMEREON_ACCOUNT_ID, CONF_LOCALE, DOMAIN @@ -46,6 +50,7 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): Ask the user for API keys. """ errors: dict[str, str] = {} + suggested_values: Mapping[str, Any] | None = None if user_input: locale = user_input[CONF_LOCALE] self.renault_config.update(user_input) @@ -64,10 +69,15 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): if login_success: return await self.async_step_kamereon() errors["base"] = "invalid_credentials" + suggested_values = user_input + elif self.source == SOURCE_RECONFIGURE: + suggested_values = self._get_reconfigure_entry().data return self.async_show_form( step_id="user", - data_schema=self.add_suggested_values_to_schema(USER_SCHEMA, user_input), + data_schema=self.add_suggested_values_to_schema( + USER_SCHEMA, suggested_values + ), errors=errors, ) @@ -77,6 +87,14 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): """Select Kamereon account.""" if user_input: await self.async_set_unique_id(user_input[CONF_KAMEREON_ACCOUNT_ID]) + if self.source == SOURCE_RECONFIGURE: + self._abort_if_unique_id_mismatch() + self.renault_config.update(user_input) + return self.async_update_reload_and_abort( + self._get_reconfigure_entry(), + data_updates=self.renault_config, + ) + self._abort_if_unique_id_configured() self.renault_config.update(user_input) @@ -129,3 +147,9 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, description_placeholders={CONF_USERNAME: reauth_entry.data[CONF_USERNAME]}, ) + + async def async_step_reconfigure( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle reconfiguration.""" + return await self.async_step_user() diff --git a/homeassistant/components/renault/quality_scale.yaml b/homeassistant/components/renault/quality_scale.yaml index 0244ff6c391..84a7e352cbc 100644 --- a/homeassistant/components/renault/quality_scale.yaml +++ b/homeassistant/components/renault/quality_scale.yaml @@ -54,7 +54,7 @@ rules: entity-translations: done exception-translations: done icon-translations: done - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: done stale-devices: done diff --git a/homeassistant/components/renault/strings.json b/homeassistant/components/renault/strings.json index d4113f2e3e2..dabe2f77bac 100644 --- a/homeassistant/components/renault/strings.json +++ b/homeassistant/components/renault/strings.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "kamereon_no_account": "Unable to find Kamereon account", - "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": "The selected Kamereon account ID does not match the previous account ID" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/tests/components/renault/test_config_flow.py b/tests/components/renault/test_config_flow.py index 9c3c82eaf3a..9a7146c96cd 100644 --- a/tests/components/renault/test_config_flow.py +++ b/tests/components/renault/test_config_flow.py @@ -283,3 +283,114 @@ async def test_reauth(hass: HomeAssistant, config_entry: MockConfigEntry) -> Non assert config_entry.data[CONF_USERNAME] == "email@test.com" assert config_entry.data[CONF_PASSWORD] == "any" + + +async def test_reconfigure( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + config_entry: MockConfigEntry, +) -> None: + """Test reconfigure works.""" + result = await config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert not result["errors"] + + data_schema = result["data_schema"].schema + assert get_schema_suggested_value(data_schema, CONF_LOCALE) == "fr_FR" + assert get_schema_suggested_value(data_schema, CONF_USERNAME) == "email@test.com" + assert get_schema_suggested_value(data_schema, CONF_PASSWORD) == "test" + + renault_account = AsyncMock() + type(renault_account).account_id = PropertyMock(return_value="account_id_1") + renault_account.get_vehicles.return_value = ( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture("renault/vehicle_zoe_40.json") + ) + ) + + # Account list single + with ( + patch("renault_api.renault_session.RenaultSession.login"), + patch( + "renault_api.renault_account.RenaultAccount.account_id", return_value="123" + ), + patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[renault_account], + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email2@test.com", + CONF_PASSWORD: "test2", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + assert config_entry.data[CONF_USERNAME] == "email2@test.com" + assert config_entry.data[CONF_PASSWORD] == "test2" + assert config_entry.data[CONF_KAMEREON_ACCOUNT_ID] == "account_id_1" + assert config_entry.data[CONF_LOCALE] == "fr_FR" + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reconfigure_mismatch( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + config_entry: MockConfigEntry, +) -> None: + """Test reconfigure fails on account ID mismatch.""" + result = await config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert not result["errors"] + + data_schema = result["data_schema"].schema + assert get_schema_suggested_value(data_schema, CONF_LOCALE) == "fr_FR" + assert get_schema_suggested_value(data_schema, CONF_USERNAME) == "email@test.com" + assert get_schema_suggested_value(data_schema, CONF_PASSWORD) == "test" + + renault_account = AsyncMock() + type(renault_account).account_id = PropertyMock(return_value="account_id_other") + renault_account.get_vehicles.return_value = ( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture("renault/vehicle_zoe_40.json") + ) + ) + + # Account list single + with ( + patch("renault_api.renault_session.RenaultSession.login"), + patch( + "renault_api.renault_account.RenaultAccount.account_id", return_value="1234" + ), + patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[renault_account], + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email2@test.com", + CONF_PASSWORD: "test2", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "unique_id_mismatch" + + # Unchanged values + assert config_entry.data[CONF_USERNAME] == "email@test.com" + assert config_entry.data[CONF_PASSWORD] == "test" + assert config_entry.data[CONF_KAMEREON_ACCOUNT_ID] == "account_id_1" + assert config_entry.data[CONF_LOCALE] == "fr_FR" + + assert len(mock_setup_entry.mock_calls) == 0