Fix via_device race in bluesound (#177932)

This commit is contained in:
Erik Montnemery
2026-08-03 17:34:46 +02:00
committed by GitHub
parent bf69a1a85f
commit 21a7268b22
4 changed files with 84 additions and 3 deletions
+7 -1
View File
@@ -9,6 +9,7 @@ from pyblu import Player
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.const import CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
@@ -118,8 +119,13 @@ class BluesoundButton(CoordinatorEntity[BluesoundCoordinator], ButtonEntity):
manufacturer=sync_status.brand,
model=sync_status.model_name,
model_id=sync_status.model,
via_device=(DOMAIN, format_mac(sync_status.mac)),
)
# The leader (default-port) device is a separate config entry that may
# not be loaded yet; link to it only if it already exists.
if leader_devices := dr.async_get(coordinator.hass).async_get_devices(
identifiers={(DOMAIN, format_mac(sync_status.mac))}
):
self._attr_device_info["via_device_id"] = leader_devices[0].id
@override
async def async_press(self) -> None:
@@ -19,7 +19,11 @@ from homeassistant.components.media_player import (
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
@@ -131,8 +135,13 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
manufacturer=sync_status.brand,
model=sync_status.model_name,
model_id=sync_status.model,
via_device=(DOMAIN, format_mac(sync_status.mac)),
)
# The leader (default-port) device is a separate config entry that may
# not be loaded yet; link to it only if it already exists.
if leader_devices := dr.async_get(coordinator.hass).async_get_devices(
identifiers={(DOMAIN, format_mac(sync_status.mac))}
):
self._attr_device_info["via_device_id"] = leader_devices[0].id
@override
async def async_added_to_hass(self) -> None:
+17
View File
@@ -159,6 +159,23 @@ def config_entry_secondary() -> MockConfigEntry:
)
@pytest.fixture
def config_entry_non_default_port() -> MockConfigEntry:
"""Return a mocked config entry for a non-default-port player.
Shares its mac (via player_mocks) with the leader `config_entry` at the
default port.
"""
return MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "1.1.1.2",
CONF_PORT: 11001,
},
unique_id="ff:ff:01:01:01:01-11001",
)
@pytest.fixture
async def setup_config_entry(
hass: HomeAssistant, config_entry: MockConfigEntry, player_mocks: PlayerMocks
+49
View File
@@ -2,8 +2,10 @@
from pyblu.errors import PlayerUnreachableError
from homeassistant.components.bluesound import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from .conftest import PlayerMocks
@@ -45,3 +47,50 @@ async def test_unload_entry_while_player_is_offline(
assert hass.states.get("media_player.player_name1111").state == "unavailable"
assert config_entry.state is ConfigEntryState.NOT_LOADED
async def test_non_default_port_links_to_leader_via_device_id(
hass: HomeAssistant,
setup_config_entry: None,
config_entry_non_default_port: MockConfigEntry,
player_mocks: PlayerMocks,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test a non-default-port player links to the already-registered leader device."""
config_entry_non_default_port.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry_non_default_port.entry_id)
await hass.async_block_till_done()
assert config_entry_non_default_port.state is ConfigEntryState.LOADED
leader_devices = device_registry.async_get_devices(
identifiers={(DOMAIN, "ff:ff:01:01:01:01")}
)
assert leader_devices
leader_device = leader_devices[0]
child_device = device_registry.async_get_device_by_identifier(
(DOMAIN, "ff:ff:01:01:01:01-11001"), config_entry_non_default_port.entry_id
)
assert child_device is not None
assert child_device.via_device_id == leader_device.id
async def test_non_default_port_without_leader_has_no_via_device_id(
hass: HomeAssistant,
config_entry_non_default_port: MockConfigEntry,
player_mocks: PlayerMocks,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test a non-default-port player sets up fine when the leader device is absent."""
config_entry_non_default_port.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry_non_default_port.entry_id)
await hass.async_block_till_done()
assert config_entry_non_default_port.state is ConfigEntryState.LOADED
child_device = device_registry.async_get_device_by_identifier(
(DOMAIN, "ff:ff:01:01:01:01-11001"), config_entry_non_default_port.entry_id
)
assert child_device is not None
assert child_device.via_device_id is None