diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index aee9bf4c47e..a2f9671c94b 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -102,6 +102,18 @@ async def _validate_auth( return True +def _get_current_hosts(entry: HeosConfigEntry) -> set[str]: + """Get a set of current hosts from the entry.""" + hosts = set(entry.data[CONF_HOST]) + if hasattr(entry, "runtime_data"): + hosts.update( + player.ip_address + for player in entry.runtime_data.heos.players.values() + if player.ip_address is not None + ) + return hosts + + class HeosFlowHandler(ConfigFlow, domain=DOMAIN): """Define a flow for HEOS.""" @@ -125,10 +137,15 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN): if TYPE_CHECKING: assert discovery_info.ssdp_location - await self.async_set_unique_id(DOMAIN) - # Connect to discovered host and get system information + entry: HeosConfigEntry | None = await self.async_set_unique_id(DOMAIN) hostname = urlparse(discovery_info.ssdp_location).hostname assert hostname is not None + + # Abort early when discovered host is part of the current system + if entry and hostname in _get_current_hosts(entry): + return self.async_abort(reason="single_instance_allowed") + + # Connect to discovered host and get system information heos = Heos(HeosOptions(hostname, events=False, heart_beat=False)) try: await heos.connect() @@ -146,8 +163,23 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN): # Select the preferred host, if available if system_info.preferred_hosts: hostname = system_info.preferred_hosts[0].ip_address - self._discovered_host = hostname - return await self.async_step_confirm_discovery() + + # Move to confirmation when not configured + if entry is None: + self._discovered_host = hostname + return await self.async_step_confirm_discovery() + + # Only update if the configured host isn't part of the discovered hosts to ensure new players that come online don't trigger a reload + if entry.data[CONF_HOST] not in [host.ip_address for host in system_info.hosts]: + _LOGGER.debug( + "Updated host %s to discovered host %s", entry.data[CONF_HOST], hostname + ) + return self.async_update_reload_and_abort( + entry, + data_updates={CONF_HOST: hostname}, + reason="reconfigure_successful", + ) + return self.async_abort(reason="single_instance_allowed") async def async_step_confirm_discovery( self, user_input: dict[str, Any] | None = None @@ -167,6 +199,7 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Obtain host and validate connection.""" await self.async_set_unique_id(DOMAIN) + self._abort_if_unique_id_configured(error="single_instance_allowed") # Try connecting to host if provided errors: dict[str, str] = {} host = None diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index 72472760951..d19b8cfd5ad 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -9,7 +9,6 @@ "loggers": ["pyheos"], "quality_scale": "silver", "requirements": ["pyheos==1.0.2"], - "single_config_entry": true, "ssdp": [ { "st": "urn:schemas-denon-com:device:ACT-Denon:1" diff --git a/homeassistant/components/heos/quality_scale.yaml b/homeassistant/components/heos/quality_scale.yaml index 6ade4e6ffb9..5f5062b6a82 100644 --- a/homeassistant/components/heos/quality_scale.yaml +++ b/homeassistant/components/heos/quality_scale.yaml @@ -38,9 +38,7 @@ rules: # Gold devices: done diagnostics: done - discovery-update-info: - status: todo - comment: Explore if this is possible. + discovery-update-info: done discovery: done docs-data-update: done docs-examples: done diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index a78fc456100..396c3743663 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -7,7 +7,9 @@ from pyheos import ( CommandFailedError, ConnectionState, HeosError, + HeosHost, HeosSystem, + NetworkType, ) import pytest @@ -118,17 +120,44 @@ async def test_discovery( async def test_discovery_flow_aborts_already_setup( - hass: HomeAssistant, discovery_data: SsdpServiceInfo, config_entry: MockConfigEntry + hass: HomeAssistant, + discovery_data_bedroom: SsdpServiceInfo, + config_entry: MockConfigEntry, + controller: MockHeos, ) -> None: - """Test discovery flow aborts when entry already setup.""" + """Test discovery flow aborts when entry already setup and hosts didn't change.""" config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.data[CONF_HOST] == "127.0.0.1" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data + DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data_bedroom ) - assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" + assert controller.get_system_info.call_count == 0 + assert config_entry.data[CONF_HOST] == "127.0.0.1" + + +async def test_discovery_aborts_same_system( + hass: HomeAssistant, + discovery_data_bedroom: SsdpServiceInfo, + controller: MockHeos, + config_entry: MockConfigEntry, + system: HeosSystem, +) -> None: + """Test discovery does not update when current host is part of discovered's system.""" + config_entry.add_to_hass(hass) + assert config_entry.data[CONF_HOST] == "127.0.0.1" + + controller.get_system_info.return_value = system + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data_bedroom + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + assert controller.get_system_info.call_count == 1 + assert config_entry.data[CONF_HOST] == "127.0.0.1" async def test_discovery_fails_to_connect_aborts( @@ -145,6 +174,26 @@ async def test_discovery_fails_to_connect_aborts( assert controller.disconnect.call_count == 1 +async def test_discovery_updates( + hass: HomeAssistant, + discovery_data_bedroom: SsdpServiceInfo, + controller: MockHeos, + config_entry: MockConfigEntry, +) -> None: + """Test discovery updates existing entry.""" + config_entry.add_to_hass(hass) + assert config_entry.data[CONF_HOST] == "127.0.0.1" + + host = HeosHost("Player", "Model", None, None, "127.0.0.2", NetworkType.WIRED, True) + controller.get_system_info.return_value = HeosSystem(None, host, [host]) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_data_bedroom + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert config_entry.data[CONF_HOST] == "127.0.0.2" + + async def test_reconfigure_validates_and_updates_config( hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos ) -> None: