diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index ce45b2605ca..5b85457e741 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -6,7 +6,7 @@ import asyncio from collections.abc import Mapping from dataclasses import dataclass import logging -from typing import Any +from typing import TYPE_CHECKING, Any from aiohttp.client_exceptions import ClientConnectorError from nettigo_air_monitor import ( @@ -227,3 +227,48 @@ class NAMFlowHandler(ConfigFlow, domain=DOMAIN): data_schema=AUTH_SCHEMA, errors=errors, ) + + async def async_step_reconfigure( + self, _: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + + if TYPE_CHECKING: + assert entry is not None + + self.host = entry.data[CONF_HOST] + self.entry = entry + + return await self.async_step_reconfigure_confirm() + + async def async_step_reconfigure_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + errors = {} + + if user_input is not None: + try: + config = await async_get_config(self.hass, user_input[CONF_HOST]) + except (ApiError, ClientConnectorError, TimeoutError): + errors["base"] = "cannot_connect" + else: + if format_mac(config.mac_address) != self.entry.unique_id: + return self.async_abort(reason="another_device") + + data = {**self.entry.data, CONF_HOST: user_input[CONF_HOST]} + self.hass.config_entries.async_update_entry(self.entry, data=data) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reconfigure_successful") + + return self.async_show_form( + step_id="reconfigure_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=self.host): str, + } + ), + description_placeholders={"device_name": self.entry.title}, + errors=errors, + ) diff --git a/homeassistant/components/nam/strings.json b/homeassistant/components/nam/strings.json index 83a40d87f76..602faebdcd7 100644 --- a/homeassistant/components/nam/strings.json +++ b/homeassistant/components/nam/strings.json @@ -27,6 +27,15 @@ }, "confirm_discovery": { "description": "Do you want to set up Nettigo Air Monitor at {host}?" + }, + "reconfigure_confirm": { + "description": "Update configuration for {device_name}.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "[%key:component::nam::config::step::user::data_description::host%]" + } } }, "error": { @@ -38,7 +47,8 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "device_unsupported": "The device is unsupported.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again." + "reauth_unsuccessful": "Re-authentication was unsuccessful, please remove the integration and set it up again.", + "another_device": "The IP address/hostname of another Nettigo Air Monitor was used." } }, "entity": { diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index 5dff9855988..b96eddfd18b 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -8,7 +8,13 @@ import pytest from homeassistant.components import zeroconf from homeassistant.components.nam.const import DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import ( + SOURCE_REAUTH, + SOURCE_RECONFIGURE, + SOURCE_USER, + SOURCE_ZEROCONF, +) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -437,3 +443,161 @@ async def test_zeroconf_errors(hass: HomeAssistant, error) -> None: assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason + + +async def test_reconfigure_successful(hass: HomeAssistant) -> None: + """Test starting a reconfigure flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="10.10.2.3", + unique_id="aa:bb:cc:dd:ee:ff", + data={ + CONF_HOST: "10.10.2.3", + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + with ( + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG_AUTH, + ), + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "10.10.10.10"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert entry.data == { + CONF_HOST: "10.10.10.10", + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + } + + +async def test_reconfigure_not_successful(hass: HomeAssistant) -> None: + """Test starting a reconfigure flow but no connection found.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="10.10.2.3", + unique_id="aa:bb:cc:dd:ee:ff", + data={ + CONF_HOST: "10.10.2.3", + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + side_effect=ApiError("API Error"), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "10.10.10.10"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + assert result["errors"] == {"base": "cannot_connect"} + + with ( + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG_AUTH, + ), + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "10.10.10.10"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert entry.data == { + CONF_HOST: "10.10.10.10", + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + } + + +async def test_reconfigure_not_the_same_device(hass: HomeAssistant) -> None: + """Test starting the reconfiguration process, but with a different printer.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="10.10.2.3", + unique_id="11:22:33:44:55:66", + data={ + CONF_HOST: "10.10.2.3", + CONF_USERNAME: "fake_username", + CONF_PASSWORD: "fake_password", + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure_confirm" + + with ( + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG_AUTH, + ), + patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "10.10.10.10"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "another_device"