diff --git a/homeassistant/components/tolo/config_flow.py b/homeassistant/components/tolo/config_flow.py index fed4ff332fc..7b97fb20343 100644 --- a/homeassistant/components/tolo/config_flow.py +++ b/homeassistant/components/tolo/config_flow.py @@ -1,14 +1,19 @@ -"""Config flow for tolo.""" +"""Config flow for TOLO integration.""" from __future__ import annotations import logging +from types import MappingProxyType from typing import Any from tololib import ToloClient, ToloCommunicationError import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + SOURCE_RECONFIGURE, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import CONF_HOST from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo @@ -17,13 +22,19 @@ from .const import DEFAULT_NAME, DOMAIN _LOGGER = logging.getLogger(__name__) +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + } +) -class ToloSaunaConfigFlow(ConfigFlow, domain=DOMAIN): - """ConfigFlow for TOLO Sauna.""" + +class ToloConfigFlow(ConfigFlow, domain=DOMAIN): + """ConfigFlow for the TOLO Integration.""" VERSION = 1 - _discovered_host: str + _dhcp_discovery_info: DhcpServiceInfo | None = None @staticmethod def _check_device_availability(host: str) -> bool: @@ -37,7 +48,7 @@ class ToloSaunaConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Handle a flow initialized by the user.""" + """Handle a config flow initialized by the user.""" errors = {} if user_input is not None: @@ -47,19 +58,36 @@ class ToloSaunaConfigFlow(ConfigFlow, domain=DOMAIN): self._check_device_availability, user_input[CONF_HOST] ) - if not device_available: - errors["base"] = "cannot_connect" - else: - return self.async_create_entry( - title=DEFAULT_NAME, data={CONF_HOST: user_input[CONF_HOST]} - ) + if device_available: + if self.source == SOURCE_RECONFIGURE: + return self.async_update_reload_and_abort( + self._get_reconfigure_entry(), data_updates=user_input + ) + return self.async_create_entry(title=DEFAULT_NAME, data=user_input) + + errors["base"] = "cannot_connect" + + schema_values: dict[str, Any] | MappingProxyType[str, Any] = {} + if user_input is not None: + schema_values = user_input + elif self.source == SOURCE_RECONFIGURE: + schema_values = self._get_reconfigure_entry().data return self.async_show_form( step_id="user", - data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + data_schema=self.add_suggested_values_to_schema( + CONFIG_SCHEMA, + schema_values, + ), errors=errors, ) + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfiguration config flow initialized by the user.""" + return await self.async_step_user(user_input) + async def async_step_dhcp( self, discovery_info: DhcpServiceInfo ) -> ConfigFlowResult: @@ -73,7 +101,7 @@ class ToloSaunaConfigFlow(ConfigFlow, domain=DOMAIN): ) if device_available: - self._discovered_host = discovery_info.ip + self._dhcp_discovery_info = discovery_info return await self.async_step_confirm() return self.async_abort(reason="not_tolo_device") @@ -81,13 +109,15 @@ class ToloSaunaConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle user-confirmation of discovered node.""" + assert self._dhcp_discovery_info is not None + if user_input is not None: - self._async_abort_entries_match({CONF_HOST: self._discovered_host}) + self._async_abort_entries_match({CONF_HOST: self._dhcp_discovery_info.ip}) return self.async_create_entry( - title=DEFAULT_NAME, data={CONF_HOST: self._discovered_host} + title=DEFAULT_NAME, data={CONF_HOST: self._dhcp_discovery_info.ip} ) return self.async_show_form( step_id="confirm", - description_placeholders={CONF_HOST: self._discovered_host}, + description_placeholders={CONF_HOST: self._dhcp_discovery_info.ip}, ) diff --git a/homeassistant/components/tolo/strings.json b/homeassistant/components/tolo/strings.json index 82b6ecee9e7..55c8274c19b 100644 --- a/homeassistant/components/tolo/strings.json +++ b/homeassistant/components/tolo/strings.json @@ -16,7 +16,8 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "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%]" } }, "entity": { diff --git a/tests/components/tolo/test_config_flow.py b/tests/components/tolo/test_config_flow.py index e918edf70a4..b6cb8f91f82 100644 --- a/tests/components/tolo/test_config_flow.py +++ b/tests/components/tolo/test_config_flow.py @@ -12,6 +12,8 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from tests.common import MockConfigEntry + MOCK_DHCP_DATA = DhcpServiceInfo( ip="127.0.0.2", macaddress="001122334455", hostname="mock_hostname" ) @@ -36,6 +38,22 @@ def coordinator_toloclient() -> Mock: yield toloclient +@pytest.fixture(name="config_entry") +async def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Return a MockConfigEntry for testing.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="TOLO Steam Bath", + entry_id="1", + data={ + CONF_HOST: "127.0.0.1", + }, + ) + config_entry.add_to_hass(hass) + + return config_entry + + async def test_user_with_timed_out_host(hass: HomeAssistant, toloclient: Mock) -> None: """Test a user initiated config flow with provided host which times out.""" toloclient().get_status.side_effect = ToloCommunicationError @@ -64,25 +82,25 @@ async def test_user_walkthrough( toloclient().get_status.side_effect = lambda *args, **kwargs: None - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "127.0.0.2"}, ) - assert result2["type"] is FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "cannot_connect"} + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} toloclient().get_status.side_effect = lambda *args, **kwargs: object() - result3 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "127.0.0.1"}, ) - assert result3["type"] is FlowResultType.CREATE_ENTRY - assert result3["title"] == "TOLO Sauna" - assert result3["data"][CONF_HOST] == "127.0.0.1" + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "TOLO Sauna" + assert result["data"][CONF_HOST] == "127.0.0.1" async def test_dhcp( @@ -116,3 +134,77 @@ async def test_dhcp_invalid_device(hass: HomeAssistant, toloclient: Mock) -> Non DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA ) assert result["type"] is FlowResultType.ABORT + + +async def test_reconfigure_walkthrough( + hass: HomeAssistant, + toloclient: Mock, + coordinator_toloclient: Mock, + config_entry: MockConfigEntry, +) -> None: + """Test a reconfigure flow without problems.""" + result = await config_entry.start_reconfigure_flow(hass) + + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "127.0.0.4"} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert config_entry.data[CONF_HOST] == "127.0.0.4" + + +async def test_reconfigure_error_then_fix( + hass: HomeAssistant, + toloclient: Mock, + coordinator_toloclient: Mock, + config_entry: MockConfigEntry, +) -> None: + """Test a reconfigure flow which first fails and then recovers.""" + result = await config_entry.start_reconfigure_flow(hass) + assert result["step_id"] == "user" + + toloclient().get_status.side_effect = ToloCommunicationError + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "127.0.0.5"} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "cannot_connect" + + toloclient().get_status.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "127.0.0.4"} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert config_entry.data[CONF_HOST] == "127.0.0.4" + + +async def test_reconfigure_duplicate_ip( + hass: HomeAssistant, + toloclient: Mock, + coordinator_toloclient: Mock, + config_entry: MockConfigEntry, +) -> None: + """Test a reconfigure flow where the user is trying to have to entries with the same IP.""" + config_entry2 = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.6"}, unique_id="second_entry" + ) + config_entry2.add_to_hass(hass) + + result = await config_entry.start_reconfigure_flow(hass) + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "127.0.0.6"} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert config_entry.data[CONF_HOST] == "127.0.0.1"