diff --git a/homeassistant/components/peblar/config_flow.py b/homeassistant/components/peblar/config_flow.py index 809cb13746e..29bf456b7ea 100644 --- a/homeassistant/components/peblar/config_flow.py +++ b/homeassistant/components/peblar/config_flow.py @@ -76,6 +76,57 @@ class PeblarFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of a Peblar device.""" + errors = {} + reconfigure_entry = self._get_reconfigure_entry() + + if user_input is not None: + peblar = Peblar( + host=user_input[CONF_HOST], + session=async_create_clientsession( + self.hass, cookie_jar=CookieJar(unsafe=True) + ), + ) + try: + await peblar.login(password=user_input[CONF_PASSWORD]) + info = await peblar.system_information() + except PeblarAuthenticationError: + errors[CONF_PASSWORD] = "invalid_auth" + except PeblarConnectionError: + errors[CONF_HOST] = "cannot_connect" + except Exception: # noqa: BLE001 + LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(info.product_serial_number) + self._abort_if_unique_id_mismatch(reason="different_device") + return self.async_update_reload_and_abort( + reconfigure_entry, + data_updates=user_input, + ) + + host = reconfigure_entry.data[CONF_HOST] + if user_input is not None: + host = user_input[CONF_HOST] + + return self.async_show_form( + step_id="reconfigure", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=host): TextSelector( + TextSelectorConfig(autocomplete="off") + ), + vol.Required(CONF_PASSWORD): TextSelector( + TextSelectorConfig(type=TextSelectorType.PASSWORD) + ), + } + ), + errors=errors, + ) + async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> ConfigFlowResult: diff --git a/homeassistant/components/peblar/quality_scale.yaml b/homeassistant/components/peblar/quality_scale.yaml index aea83da25ab..9de0031373f 100644 --- a/homeassistant/components/peblar/quality_scale.yaml +++ b/homeassistant/components/peblar/quality_scale.yaml @@ -66,7 +66,7 @@ rules: comment: | The coordinator needs translation when the update failed. icon-translations: done - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: status: exempt comment: | diff --git a/homeassistant/components/peblar/strings.json b/homeassistant/components/peblar/strings.json index 0cce7ed8191..f09a156dd1e 100644 --- a/homeassistant/components/peblar/strings.json +++ b/homeassistant/components/peblar/strings.json @@ -2,8 +2,10 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "different_device": "The information entered is from a different Peblar EV charger.", "no_serial_number": "The discovered Peblar device did not provide a serial number.", - "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%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -20,6 +22,17 @@ }, "description": "Reauthenticate with your Peblar RV charger.\n\nTo do so, you will need to enter your new password you use to log into Peblar's device web interface." }, + "reconfigure": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "host": "[%key:component::peblar::config::step::user::data_description::host%]", + "password": "[%key:component::peblar::config::step::user::data_description::password%]" + }, + "description": "Reconfigure your Peblar EV charger.\n\nThis allows you to change the IP address of your Peblar charger and the password you use to log into the Peblar device' web interface." + }, "user": { "data": { "host": "[%key:common::config_flow::data::host%]", diff --git a/tests/components/peblar/test_config_flow.py b/tests/components/peblar/test_config_flow.py index a4a461b6bba..a97e8d3b564 100644 --- a/tests/components/peblar/test_config_flow.py +++ b/tests/components/peblar/test_config_flow.py @@ -117,6 +117,115 @@ async def test_user_flow_already_configured( assert result["reason"] == "already_configured" +@pytest.mark.usefixtures("mock_peblar") +async def test_reconfigure_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test the full happy path reconfigure flow from start to finish.""" + 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 mock_config_entry.data == { + CONF_HOST: "127.0.0.127", + CONF_PASSWORD: "OMGSPIDERS", + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "OMGPUPPIES", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "OMGPUPPIES", + } + + +@pytest.mark.usefixtures("mock_peblar") +async def test_reconfigure_to_different_device( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfiguring to a different device doesn't work.""" + mock_config_entry.add_to_hass(hass) + + # Change the unique ID of the entry, so we have a mismatch + hass.config_entries.async_update_entry(mock_config_entry, unique_id="mismatch") + + result = await mock_config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "OMGPUPPIES", + }, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "different_device" + + +@pytest.mark.parametrize( + ("side_effect", "expected_error"), + [ + (PeblarConnectionError, {CONF_HOST: "cannot_connect"}), + (PeblarAuthenticationError, {CONF_PASSWORD: "invalid_auth"}), + (Exception, {"base": "unknown"}), + ], +) +async def test_reconfigure_flow_errors( + hass: HomeAssistant, + mock_peblar: MagicMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, + expected_error: dict[str, str], +) -> None: + """Test we show user form on a connection error.""" + mock_config_entry.add_to_hass(hass) + mock_peblar.login.side_effect = side_effect + + result = await mock_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "OMGPUPPIES", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == expected_error + + mock_peblar.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "127.0.0.2", + CONF_PASSWORD: "OMGPUPPIES", + }, + ) + assert result["type"] is FlowResultType.ABORT + + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.2", + CONF_PASSWORD: "OMGPUPPIES", + } + + @pytest.mark.usefixtures("mock_peblar") async def test_zeroconf_flow(hass: HomeAssistant) -> None: """Test the zeroconf happy flow from start to finish."""