diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index e8a4dbf7b63..f861247d1a9 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -15,7 +15,20 @@ from .const import DOMAIN def format_title(host: str) -> str: """Format the title for config entries.""" - return f"Controller ({host})" + return f"HEOS System (via {host})" + + +async def _validate_host(host: str, errors: dict[str, str]) -> bool: + """Validate host is reachable, return True, otherwise populate errors and return False.""" + heos = Heos(host) + try: + await heos.connect() + except HeosError: + errors[CONF_HOST] = "cannot_connect" + return False + finally: + await heos.disconnect() + return True class HeosFlowHandler(ConfigFlow, domain=DOMAIN): @@ -47,23 +60,17 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN): self.hass.data.setdefault(DOMAIN, {}) await self.async_set_unique_id(DOMAIN) # Try connecting to host if provided - errors = {} + errors: dict[str, str] = {} host = None if user_input is not None: host = user_input[CONF_HOST] # Map host from friendly name if in discovered hosts host = self.hass.data[DOMAIN].get(host, host) - heos = Heos(host) - try: - await heos.connect() - self.hass.data.pop(DOMAIN) + if await _validate_host(host, errors): + self.hass.data.pop(DOMAIN) # Remove discovery data return self.async_create_entry( title=format_title(host), data={CONF_HOST: host} ) - except HeosError: - errors[CONF_HOST] = "cannot_connect" - finally: - await heos.disconnect() # Return form host_type = ( @@ -74,3 +81,22 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({vol.Required(CONF_HOST, default=host): host_type}), errors=errors, ) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Allow reconfiguration of entry.""" + entry = self._get_reconfigure_entry() + host = entry.data[CONF_HOST] # Get current host value + errors: dict[str, str] = {} + if user_input is not None: + host = user_input[CONF_HOST] + if await _validate_host(host, errors): + return self.async_update_reload_and_abort( + entry, data_updates={CONF_HOST: host} + ) + return self.async_show_form( + step_id="reconfigure", + data_schema=vol.Schema({vol.Required(CONF_HOST, default=host): str}), + errors=errors, + ) diff --git a/homeassistant/components/heos/quality_scale.yaml b/homeassistant/components/heos/quality_scale.yaml index 861ca750780..39c25486e52 100644 --- a/homeassistant/components/heos/quality_scale.yaml +++ b/homeassistant/components/heos/quality_scale.yaml @@ -88,7 +88,7 @@ rules: entity-translations: done exception-translations: todo icon-translations: done - reconfiguration-flow: todo + reconfiguration-flow: done repair-issues: todo stale-devices: todo # Platinum diff --git a/homeassistant/components/heos/strings.json b/homeassistant/components/heos/strings.json index 20a8a2e978b..fe4fc63b449 100644 --- a/homeassistant/components/heos/strings.json +++ b/homeassistant/components/heos/strings.json @@ -2,13 +2,23 @@ "config": { "step": { "user": { - "title": "Connect to Heos", - "description": "Please enter the host name or IP address of a Heos device (preferably one connected via wire to the network).", + "title": "Connect to HEOS", + "description": "Please enter the host name or IP address of a HEOS-capable product to access your HEOS System.", "data": { "host": "[%key:common::config_flow::data::host%]" }, "data_description": { - "host": "The hostname or IP address of your HEOS device." + "host": "Host name or IP address of a HEOS-capable product (preferrably one connected via wire to the network)." + } + }, + "reconfigure": { + "title": "Reconfigure HEOS", + "description": "Change the host name or IP address of the HEOS-capable product used to access your HEOS System.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "[%key:component::heos::config::step::user::data_description::host%]" } } }, @@ -17,13 +27,14 @@ }, "abort": { "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, "services": { "sign_in": { "name": "Sign in", - "description": "Signs the controller in to a HEOS account.", + "description": "Signs in to a HEOS account.", "fields": { "username": { "name": "[%key:common::config_flow::data::username%]", @@ -37,7 +48,7 @@ }, "sign_out": { "name": "Sign out", - "description": "Signs the controller out of the HEOS account." + "description": "Signs out of the HEOS account." } } } diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py index 95a388d87a8..9ea3341304a 100644 --- a/tests/components/heos/conftest.py +++ b/tests/components/heos/conftest.py @@ -27,7 +27,10 @@ from tests.common import MockConfigEntry def config_entry_fixture(): """Create a mock HEOS config entry.""" return MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, title="Controller (127.0.0.1)" + domain=DOMAIN, + data={CONF_HOST: "127.0.0.1"}, + title="HEOS System (via 127.0.0.1)", + unique_id=DOMAIN, ) diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index 464b62df157..38382a81794 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -54,7 +54,7 @@ async def test_create_entry_when_host_valid(hass: HomeAssistant, controller) -> ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN - assert result["title"] == "Controller (127.0.0.1)" + assert result["title"] == "HEOS System (via 127.0.0.1)" assert result["data"] == data assert controller.connect.call_count == 2 # Also called in async_setup_entry assert controller.disconnect.call_count == 1 @@ -73,7 +73,7 @@ async def test_create_entry_when_friendly_name_valid( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN - assert result["title"] == "Controller (127.0.0.1)" + assert result["title"] == "HEOS System (via 127.0.0.1)" assert result["data"] == {CONF_HOST: "127.0.0.1"} assert controller.connect.call_count == 2 # Also called in async_setup_entry assert controller.disconnect.call_count == 1 @@ -120,3 +120,73 @@ async def test_discovery_flow_aborts_already_setup( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" + + +async def test_reconfigure_validates_and_updates_config( + hass: HomeAssistant, config_entry, controller +) -> None: + """Test reconfigure validates host and successfully updates.""" + config_entry.add_to_hass(hass) + result = await config_entry.start_reconfigure_flow(hass) + assert config_entry.data[CONF_HOST] == "127.0.0.1" + + # Test reconfigure initially shows form with current host value. + host = next( + key.default() for key in result["data_schema"].schema if key == CONF_HOST + ) + assert host == "127.0.0.1" + assert result["errors"] == {} + assert result["step_id"] == "reconfigure" + assert result["type"] is FlowResultType.FORM + + # Test reconfigure successfully updates. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "127.0.0.2"}, + ) + assert controller.connect.call_count == 2 # Also called when entry reloaded + assert controller.disconnect.call_count == 1 + assert config_entry.data == {CONF_HOST: "127.0.0.2"} + assert config_entry.unique_id == DOMAIN + assert result["reason"] == "reconfigure_successful" + assert result["type"] is FlowResultType.ABORT + + +async def test_reconfigure_cannot_connect_recovers( + hass: HomeAssistant, config_entry, controller +) -> None: + """Test reconfigure cannot connect and recovers.""" + controller.connect.side_effect = HeosError() + config_entry.add_to_hass(hass) + result = await config_entry.start_reconfigure_flow(hass) + assert config_entry.data[CONF_HOST] == "127.0.0.1" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "127.0.0.2"}, + ) + + assert controller.connect.call_count == 1 + assert controller.disconnect.call_count == 1 + host = next( + key.default() for key in result["data_schema"].schema if key == CONF_HOST + ) + assert host == "127.0.0.2" + assert result["errors"][CONF_HOST] == "cannot_connect" + assert result["step_id"] == "reconfigure" + assert result["type"] is FlowResultType.FORM + + # Test reconfigure recovers and successfully updates. + controller.connect.side_effect = None + controller.connect.reset_mock() + controller.disconnect.reset_mock() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "127.0.0.2"}, + ) + assert controller.connect.call_count == 2 # Also called when entry reloaded + assert controller.disconnect.call_count == 1 + assert config_entry.data == {CONF_HOST: "127.0.0.2"} + assert config_entry.unique_id == DOMAIN + assert result["reason"] == "reconfigure_successful" + assert result["type"] is FlowResultType.ABORT