diff --git a/homeassistant/components/vallox/config_flow.py b/homeassistant/components/vallox/config_flow.py index 86253838879..3660c641b7c 100644 --- a/homeassistant/components/vallox/config_flow.py +++ b/homeassistant/components/vallox/config_flow.py @@ -18,7 +18,7 @@ from .const import DEFAULT_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) -STEP_USER_DATA_SCHEMA = vol.Schema( +CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): str, } @@ -47,10 +47,10 @@ class ValloxConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is None: return self.async_show_form( step_id="user", - data_schema=STEP_USER_DATA_SCHEMA, + data_schema=CONFIG_SCHEMA, ) - errors = {} + errors: dict[str, str] = {} host = user_input[CONF_HOST] @@ -76,7 +76,55 @@ class ValloxConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", - data_schema=STEP_USER_DATA_SCHEMA, + data_schema=self.add_suggested_values_to_schema( + CONFIG_SCHEMA, {CONF_HOST: host} + ), + errors=errors, + ) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the Vallox device host address.""" + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert entry + + if not user_input: + return self.async_show_form( + step_id="reconfigure", + data_schema=self.add_suggested_values_to_schema( + CONFIG_SCHEMA, {CONF_HOST: entry.data.get(CONF_HOST)} + ), + ) + + updated_host = user_input[CONF_HOST] + + if entry.data.get(CONF_HOST) != updated_host: + self._async_abort_entries_match({CONF_HOST: updated_host}) + + errors: dict[str, str] = {} + + try: + await validate_host(self.hass, updated_host) + except InvalidHost: + errors[CONF_HOST] = "invalid_host" + except ValloxApiException: + errors[CONF_HOST] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors[CONF_HOST] = "unknown" + else: + return self.async_update_reload_and_abort( + entry, + data={**entry.data, CONF_HOST: updated_host}, + reason="reconfigure_successful", + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=self.add_suggested_values_to_schema( + CONFIG_SCHEMA, {CONF_HOST: updated_host} + ), errors=errors, ) diff --git a/homeassistant/components/vallox/strings.json b/homeassistant/components/vallox/strings.json index d23d54c75cb..072b59b78e0 100644 --- a/homeassistant/components/vallox/strings.json +++ b/homeassistant/components/vallox/strings.json @@ -8,10 +8,19 @@ "data_description": { "host": "Hostname or IP address of your Vallox device." } + }, + "reconfigure": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "[%key:component::vallox::config::step::user::data_description::host%]" + } } }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_host": "[%key:common::config_flow::error::invalid_host%]", "unknown": "[%key:common::config_flow::error::unknown%]" diff --git a/tests/components/vallox/conftest.py b/tests/components/vallox/conftest.py index 08c020c1982..9f65734b926 100644 --- a/tests/components/vallox/conftest.py +++ b/tests/components/vallox/conftest.py @@ -5,21 +5,47 @@ from unittest.mock import AsyncMock, patch import pytest from vallox_websocket_api import MetricData +from homeassistant import config_entries from homeassistant.components.vallox.const import DOMAIN +from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +DEFAULT_HOST = "192.168.100.50" +DEFAULT_NAME = "Vallox" + @pytest.fixture -def mock_entry(hass: HomeAssistant) -> MockConfigEntry: +def default_host() -> str: + """Return the default host used in the default mock entry.""" + return DEFAULT_HOST + + +@pytest.fixture +def default_name() -> str: + """Return the default name used in the default mock entry.""" + return DEFAULT_NAME + + +@pytest.fixture +def mock_entry( + hass: HomeAssistant, default_host: str, default_name: str +) -> MockConfigEntry: + """Create mocked Vallox config entry fixture.""" + return create_mock_entry(hass, default_host, default_name) + + +def create_mock_entry(hass: HomeAssistant, host: str, name: str) -> MockConfigEntry: """Create mocked Vallox config entry.""" vallox_mock_entry = MockConfigEntry( domain=DOMAIN, data={ - CONF_HOST: "192.168.100.50", - CONF_NAME: "Vallox", + CONF_HOST: host, + CONF_NAME: name, }, ) vallox_mock_entry.add_to_hass(hass) @@ -27,6 +53,49 @@ def mock_entry(hass: HomeAssistant) -> MockConfigEntry: return vallox_mock_entry +@pytest.fixture +async def setup_vallox_entry( + hass: HomeAssistant, default_host: str, default_name: str +) -> None: + """Define a fixture to set up Vallox.""" + await do_setup_vallox_entry(hass, default_host, default_name) + + +async def do_setup_vallox_entry(hass: HomeAssistant, host: str, name: str) -> None: + """Set up the Vallox component.""" + assert await async_setup_component( + hass, + DOMAIN, + { + CONF_HOST: host, + CONF_NAME: name, + }, + ) + await hass.async_block_till_done() + + +@pytest.fixture +async def init_reconfigure_flow( + hass: HomeAssistant, mock_entry, setup_vallox_entry +) -> tuple[MockConfigEntry, ConfigFlowResult]: + """Initialize a config entry and a reconfigure flow for it.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": mock_entry.entry_id, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + # original entry + assert mock_entry.data["host"] == "192.168.100.50" + + return (mock_entry, result) + + @pytest.fixture def default_metrics(): """Return default Vallox metrics.""" diff --git a/tests/components/vallox/test_config_flow.py b/tests/components/vallox/test_config_flow.py index cfeb7152b17..3cd14dbcaff 100644 --- a/tests/components/vallox/test_config_flow.py +++ b/tests/components/vallox/test_config_flow.py @@ -6,11 +6,10 @@ from vallox_websocket_api import ValloxApiException, ValloxWebsocketException from homeassistant.components.vallox.const import DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from tests.common import MockConfigEntry +from .conftest import create_mock_entry, do_setup_vallox_entry async def test_form_no_input(hass: HomeAssistant) -> None: @@ -137,14 +136,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - mock_entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_HOST: "20.40.10.30", - CONF_NAME: "Vallox 110 MV", - }, - ) - mock_entry.add_to_hass(hass) + create_mock_entry(hass, "20.40.10.30", "Vallox 110 MV") result = await hass.config_entries.flow.async_configure( init["flow_id"], @@ -154,3 +146,115 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_reconfigure_host(hass: HomeAssistant, init_reconfigure_flow) -> None: + """Test that the host can be reconfigured.""" + entry, init_flow_result = init_reconfigure_flow + + reconfigure_result = await hass.config_entries.flow.async_configure( + init_flow_result["flow_id"], + { + "host": "192.168.100.60", + }, + ) + await hass.async_block_till_done() + assert reconfigure_result["type"] is FlowResultType.ABORT + assert reconfigure_result["reason"] == "reconfigure_successful" + + # changed entry + assert entry.data["host"] == "192.168.100.60" + + +async def test_reconfigure_host_to_same_host_as_another_fails( + hass: HomeAssistant, init_reconfigure_flow +) -> None: + """Test that changing host to a host that already exists fails.""" + entry, init_flow_result = init_reconfigure_flow + + # Create second device + create_mock_entry(hass=hass, host="192.168.100.70", name="Vallox 2") + await do_setup_vallox_entry(hass=hass, host="192.168.100.70", name="Vallox 2") + + reconfigure_result = await hass.config_entries.flow.async_configure( + init_flow_result["flow_id"], + { + "host": "192.168.100.70", + }, + ) + await hass.async_block_till_done() + assert reconfigure_result["type"] is FlowResultType.ABORT + assert reconfigure_result["reason"] == "already_configured" + + # entry not changed + assert entry.data["host"] == "192.168.100.50" + + +async def test_reconfigure_host_to_invalid_ip_fails( + hass: HomeAssistant, init_reconfigure_flow +) -> None: + """Test that an invalid IP error is handled by the reconfigure step.""" + entry, init_flow_result = init_reconfigure_flow + + reconfigure_result = await hass.config_entries.flow.async_configure( + init_flow_result["flow_id"], + { + "host": "test.host.com", + }, + ) + await hass.async_block_till_done() + assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["errors"] == {"host": "invalid_host"} + + # entry not changed + assert entry.data["host"] == "192.168.100.50" + + +async def test_reconfigure_host_vallox_api_exception_cannot_connect( + hass: HomeAssistant, init_reconfigure_flow +) -> None: + """Test that cannot connect error is handled by the reconfigure step.""" + entry, init_flow_result = init_reconfigure_flow + + with patch( + "homeassistant.components.vallox.config_flow.Vallox.fetch_metric_data", + side_effect=ValloxApiException, + ): + reconfigure_result = await hass.config_entries.flow.async_configure( + init_flow_result["flow_id"], + { + "host": "192.168.100.80", + }, + ) + await hass.async_block_till_done() + + assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["errors"] == {"host": "cannot_connect"} + + # entry not changed + assert entry.data["host"] == "192.168.100.50" + + +async def test_reconfigure_host_unknown_exception( + hass: HomeAssistant, init_reconfigure_flow +) -> None: + """Test that cannot connect error is handled by the reconfigure step.""" + entry, init_flow_result = init_reconfigure_flow + + with patch( + "homeassistant.components.vallox.config_flow.Vallox.fetch_metric_data", + side_effect=Exception, + ): + reconfigure_result = await hass.config_entries.flow.async_configure( + init_flow_result["flow_id"], + { + "host": "192.168.100.90", + }, + ) + await hass.async_block_till_done() + + assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["errors"] == {"host": "unknown"} + + # entry not changed + assert entry.data["host"] == "192.168.100.50"