Update HEOS host from discovery (#138950)

This commit is contained in:
Andrew Sayre 2025-02-21 06:32:36 -06:00 committed by GitHub
parent 1d43cb3f29
commit b73c6ed768
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 91 additions and 12 deletions

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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: