diff --git a/homeassistant/components/ouman_eh_800/config_flow.py b/homeassistant/components/ouman_eh_800/config_flow.py index 39a348cbb83b..d8cda1ad40a5 100644 --- a/homeassistant/components/ouman_eh_800/config_flow.py +++ b/homeassistant/components/ouman_eh_800/config_flow.py @@ -38,6 +38,34 @@ class OumanEh800ConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + async def _async_validate_input(self, user_input: dict[str, Any]) -> dict[str, str]: + """Normalize the URL, check for duplicates and test the connection. + + Mutates user_input to hold the normalized URL. Returns form errors, + empty if validation succeeded. + """ + try: + user_input[CONF_URL] = _normalize_url(user_input[CONF_URL]) + except ValueError: + return {CONF_URL: "invalid_url"} + self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]}) + client = OumanEh800Client( + session=async_get_clientsession(self.hass), + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + address=user_input[CONF_URL], + ) + try: + await client.login() + except OumanClientCommunicationError: + return {"base": "cannot_connect"} + except OumanClientAuthenticationError: + return {"base": "invalid_auth"} + except Exception: + _LOGGER.exception("Unexpected exception") + return {"base": "unknown"} + return {} + @override async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -45,31 +73,8 @@ class OumanEh800ConfigFlow(ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: - try: - user_input[CONF_URL] = _normalize_url(user_input[CONF_URL]) - except ValueError: - errors[CONF_URL] = "invalid_url" - else: - self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]}) - client = OumanEh800Client( - session=async_get_clientsession(self.hass), - username=user_input[CONF_USERNAME], - password=user_input[CONF_PASSWORD], - address=user_input[CONF_URL], - ) - try: - await client.login() - except OumanClientCommunicationError: - errors["base"] = "cannot_connect" - except OumanClientAuthenticationError: - errors["base"] = "invalid_auth" - except Exception: - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - return self.async_create_entry( - title="Ouman EH-800", data=user_input - ) + if not (errors := await self._async_validate_input(user_input)): + return self.async_create_entry(title="Ouman EH-800", data=user_input) return self.async_show_form( step_id="user", @@ -78,3 +83,23 @@ class OumanEh800ConfigFlow(ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the integration.""" + reconfigure_entry = self._get_reconfigure_entry() + errors: dict[str, str] = {} + if user_input is not None: + if not (errors := await self._async_validate_input(user_input)): + return self.async_update_reload_and_abort( + reconfigure_entry, data_updates=user_input + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=self.add_suggested_values_to_schema( + STEP_USER_DATA_SCHEMA, user_input or reconfigure_entry.data + ), + errors=errors, + ) diff --git a/homeassistant/components/ouman_eh_800/quality_scale.yaml b/homeassistant/components/ouman_eh_800/quality_scale.yaml index 0675818b938b..8d8b321f2723 100644 --- a/homeassistant/components/ouman_eh_800/quality_scale.yaml +++ b/homeassistant/components/ouman_eh_800/quality_scale.yaml @@ -70,7 +70,7 @@ rules: entity-translations: done exception-translations: todo icon-translations: done - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: todo stale-devices: status: exempt diff --git a/homeassistant/components/ouman_eh_800/strings.json b/homeassistant/components/ouman_eh_800/strings.json index a6865d3eab83..e9f92e4cc842 100644 --- a/homeassistant/components/ouman_eh_800/strings.json +++ b/homeassistant/components/ouman_eh_800/strings.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -10,6 +11,18 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { + "reconfigure": { + "data": { + "password": "[%key:common::config_flow::data::password%]", + "url": "[%key:common::config_flow::data::url%]", + "username": "[%key:common::config_flow::data::username%]" + }, + "data_description": { + "password": "[%key:component::ouman_eh_800::config::step::user::data_description::password%]", + "url": "[%key:component::ouman_eh_800::config::step::user::data_description::url%]", + "username": "[%key:component::ouman_eh_800::config::step::user::data_description::username%]" + } + }, "user": { "data": { "password": "[%key:common::config_flow::data::password%]", diff --git a/tests/components/ouman_eh_800/test_config_flow.py b/tests/components/ouman_eh_800/test_config_flow.py index f62e9a2f6e84..30083ca6c671 100644 --- a/tests/components/ouman_eh_800/test_config_flow.py +++ b/tests/components/ouman_eh_800/test_config_flow.py @@ -154,3 +154,103 @@ async def test_user_flow_already_configured( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("mock_ouman_client") +@pytest.mark.parametrize( + "new_input", + [ + pytest.param( + { + CONF_URL: "http://192.168.1.200", + CONF_USERNAME: "new-user", + CONF_PASSWORD: "new-pass", + }, + id="new_url_and_credentials", + ), + pytest.param( + {**USER_INPUT, CONF_PASSWORD: "new-pass"}, + id="same_url_new_password", + ), + ], +) +async def test_reconfigure_flow_success( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + new_input: dict[str, str], +) -> None: + """Test a successful reconfiguration updates the entry data.""" + 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" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], new_input + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert mock_config_entry.data == new_input + + +@pytest.mark.parametrize( + ("error", "expected_error"), + [ + (OumanClientCommunicationError("Connection failed"), "cannot_connect"), + (OumanClientAuthenticationError("Invalid credentials"), "invalid_auth"), + (RuntimeError("Unexpected"), "unknown"), + ], +) +async def test_reconfigure_flow_errors_recover( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_ouman_client: AsyncMock, + error: Exception, + expected_error: str, +) -> None: + """Test that reconfigure errors are surfaced and the flow can recover.""" + mock_config_entry.add_to_hass(hass) + mock_ouman_client.login.side_effect = error + + result = await mock_config_entry.start_reconfigure_flow(hass) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], USER_INPUT + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": expected_error} + + mock_ouman_client.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], USER_INPUT + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + +@pytest.mark.usefixtures("mock_ouman_client") +async def test_reconfigure_flow_already_configured( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test aborting when reconfiguring to the URL of another entry.""" + mock_config_entry.add_to_hass(hass) + other_entry = MockConfigEntry( + domain=DOMAIN, + title="Ouman EH-800", + data={**USER_INPUT, CONF_URL: "http://192.168.1.200"}, + ) + other_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reconfigure_flow(hass) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {**USER_INPUT, CONF_URL: "http://192.168.1.200"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured"