diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py index aa861f0a8423..734f42e03b00 100644 --- a/homeassistant/components/lametric/config_flow.py +++ b/homeassistant/components/lametric/config_flow.py @@ -7,6 +7,7 @@ from typing import Any, override from demetriek import ( CloudDevice, + LaMetricAuthenticationError, LaMetricCloud, LaMetricConnectionError, LaMetricDevice, @@ -118,6 +119,57 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): """Handle initiation of re-authentication with LaMetric.""" return await self.async_step_choice_enter_manual_or_fetch_cloud() + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the host and API key of a device.""" + errors: dict[str, str] = {} + reconfigure_entry = self._get_reconfigure_entry() + + if user_input is not None: + lametric = LaMetricDevice( + host=user_input[CONF_HOST], + api_key=user_input[CONF_API_KEY], + session=async_get_clientsession(self.hass), + ) + try: + device = await lametric.device() + except LaMetricAuthenticationError: + errors["base"] = "invalid_auth" + except LaMetricConnectionError as ex: + LOGGER.error("Error connecting to LaMetric: %s", ex) + errors["base"] = "cannot_connect" + except Exception: # noqa: BLE001 + LOGGER.exception("Unexpected error occurred") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(device.serial_number) + self._abort_if_unique_id_mismatch() + return self.async_update_reload_and_abort( + reconfigure_entry, + data_updates={ + CONF_HOST: user_input[CONF_HOST], + CONF_API_KEY: user_input[CONF_API_KEY], + }, + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=self.add_suggested_values_to_schema( + probatio.Schema( + { + probatio.Required(CONF_HOST): TextSelector(), + probatio.Required(probatio.Secret(CONF_API_KEY)): TextSelector( + TextSelectorConfig(type=TextSelectorType.PASSWORD) + ), + } + ), + {CONF_HOST: reconfigure_entry.data[CONF_HOST]}, + ), + description_placeholders={"devices_url": DEVICES_URL}, + errors=errors, + ) + async def async_step_choice_enter_manual_or_fetch_cloud( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: diff --git a/homeassistant/components/lametric/quality_scale.yaml b/homeassistant/components/lametric/quality_scale.yaml index e8a5d5fb8887..f78356048bbf 100644 --- a/homeassistant/components/lametric/quality_scale.yaml +++ b/homeassistant/components/lametric/quality_scale.yaml @@ -68,7 +68,7 @@ rules: entity-translations: done exception-translations: done icon-translations: done - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: status: exempt comment: | diff --git a/homeassistant/components/lametric/strings.json b/homeassistant/components/lametric/strings.json index f181d9981812..ff1a7f92695b 100644 --- a/homeassistant/components/lametric/strings.json +++ b/homeassistant/components/lametric/strings.json @@ -7,10 +7,12 @@ "missing_configuration": "The LaMetric integration is not configured. Please follow the documentation.", "no_devices": "The authorized user has no LaMetric devices", "reauth_device_not_found": "The device you are trying to re-authenticate is not found in this LaMetric account", + "unique_id_mismatch": "The device at this address is not the LaMetric device you are reconfiguring.", "unknown": "[%key:common::config_flow::error::unknown%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { @@ -47,6 +49,16 @@ "implementation": "[%key:common::config_flow::description::implementation%]" }, "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "reconfigure": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "api_key": "[%key:component::lametric::config::step::manual_entry::data_description::api_key%]", + "host": "[%key:component::lametric::config::step::manual_entry::data_description::host%]" + } } } }, diff --git a/tests/components/lametric/test_config_flow.py b/tests/components/lametric/test_config_flow.py index e52d669ebd1f..ae78a312218b 100644 --- a/tests/components/lametric/test_config_flow.py +++ b/tests/components/lametric/test_config_flow.py @@ -4,6 +4,7 @@ from http import HTTPStatus from unittest.mock import MagicMock from demetriek import ( + LaMetricAuthenticationError, LaMetricConnectionError, LaMetricConnectionTimeoutError, LaMetricError, @@ -927,3 +928,97 @@ async def test_reauth_manual_sky( notification: Notification = mock_lametric.notify.mock_calls[0][2]["notification"] assert notification.model.sound is None + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_reconfigure( + hass: HomeAssistant, + mock_lametric: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test changing the host and API key of a configured device.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_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={CONF_HOST: "127.0.0.42", CONF_API_KEY: "new-api-key"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.42", + CONF_API_KEY: "new-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + + # Reconfiguring only checks the device, it does not send a notification. + assert len(mock_lametric.device.mock_calls) == 1 + assert len(mock_lametric.notify.mock_calls) == 0 + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_reconfigure_other_device( + hass: HomeAssistant, + mock_lametric: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfiguring towards another device is refused.""" + mock_config_entry.add_to_hass(hass) + mock_lametric.device.return_value.serial_number = "SA000000000000000000" + + result = await mock_config_entry.start_reconfigure_flow(hass) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "127.0.0.42", CONF_API_KEY: "new-api-key"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "unique_id_mismatch" + assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" + + +@pytest.mark.usefixtures("mock_setup_entry") +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (LaMetricAuthenticationError, "invalid_auth"), + (LaMetricConnectionError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_reconfigure_errors( + hass: HomeAssistant, + mock_lametric: MagicMock, + mock_config_entry: MockConfigEntry, + side_effect: type[Exception], + error: str, +) -> None: + """Test errors while reconfiguring, and recovering from them.""" + mock_config_entry.add_to_hass(hass) + device = mock_lametric.device.return_value + mock_lametric.device.side_effect = side_effect + + result = await mock_config_entry.start_reconfigure_flow(hass) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "127.0.0.42", CONF_API_KEY: "new-api-key"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_lametric.device.side_effect = None + mock_lametric.device.return_value = device + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "127.0.0.42", CONF_API_KEY: "new-api-key"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful"