Remove mapping of entity_ids to speakers in Sonos (#147506)

* fix

* fix: change entity_id mappings

* fix: translate errors

* fix:merge issues

* fix: translate error messages

* fix: improve test coverage

* fix: remove unneeded strings
This commit is contained in:
Pete Sage 2025-06-25 12:29:23 -04:00 committed by GitHub
parent c05d8aab1c
commit d8258924f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 82 additions and 12 deletions

View File

@ -6,6 +6,7 @@ import time
from typing import Any
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntry
from .const import DOMAIN
@ -132,11 +133,23 @@ async def async_generate_speaker_info(
value = getattr(speaker, attrib)
payload[attrib] = get_contents(value)
entity_registry = er.async_get(hass)
payload["enabled_entities"] = sorted(
entity_id
for entity_id, s in config_entry.runtime_data.entity_id_mappings.items()
if s is speaker
registry_entry.entity_id
for registry_entry in entity_registry.entities.get_entries_for_config_entry_id(
config_entry.entry_id
)
if (
(
entity_speaker
:= config_entry.runtime_data.unique_id_speaker_mappings.get(
registry_entry.unique_id
)
)
and speaker.uid == entity_speaker.uid
)
)
payload["media"] = await async_generate_media_info(hass, speaker)
payload["activity_stats"] = speaker.activity_stats.report()
payload["event_stats"] = speaker.event_stats.report()

View File

@ -34,7 +34,10 @@ class SonosEntity(Entity):
async def async_added_to_hass(self) -> None:
"""Handle common setup when added to hass."""
self.config_entry.runtime_data.entity_id_mappings[self.entity_id] = self.speaker
assert self.unique_id
self.config_entry.runtime_data.unique_id_speaker_mappings[self.unique_id] = (
self.speaker
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
@ -52,7 +55,8 @@ class SonosEntity(Entity):
async def async_will_remove_from_hass(self) -> None:
"""Clean up when entity is removed."""
del self.config_entry.runtime_data.entity_id_mappings[self.entity_id]
assert self.unique_id
del self.config_entry.runtime_data.unique_id_speaker_mappings[self.unique_id]
async def async_fallback_poll(self, now: datetime.datetime) -> None:
"""Poll the entity if subscriptions fail."""

View File

@ -149,7 +149,8 @@ class SonosData:
discovery_known: set[str] = field(default_factory=set)
boot_counts: dict[str, int] = field(default_factory=dict)
mdns_names: dict[str, str] = field(default_factory=dict)
entity_id_mappings: dict[str, SonosSpeaker] = field(default_factory=dict)
# Maps the entity unique id to the associated SonosSpeaker instance.
unique_id_speaker_mappings: dict[str, SonosSpeaker] = field(default_factory=dict)
unjoin_data: dict[str, UnjoinData] = field(default_factory=dict)

View File

@ -43,7 +43,12 @@ from homeassistant.components.plex.services import process_plex_payload
from homeassistant.const import ATTR_TIME
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, entity_platform, service
from homeassistant.helpers import (
config_validation as cv,
entity_platform,
entity_registry as er,
service,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_call_later
@ -880,13 +885,28 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
async def async_join_players(self, group_members: list[str]) -> None:
"""Join `group_members` as a player group with the current player."""
speakers = []
entity_registry = er.async_get(self.hass)
for entity_id in group_members:
if speaker := self.config_entry.runtime_data.entity_id_mappings.get(
entity_id
if not (entity_reg_entry := entity_registry.async_get(entity_id)):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="entity_not_found",
translation_placeholders={"entity_id": entity_id},
)
if not (
speaker
:= self.config_entry.runtime_data.unique_id_speaker_mappings.get(
entity_reg_entry.unique_id
)
):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="speaker_not_found",
translation_placeholders={"entity_id": entity_id},
)
speakers.append(speaker)
else:
raise HomeAssistantError(f"Not a known Sonos entity_id: {entity_id}")
await SonosSpeaker.join_multi(
self.hass, self.config_entry, self.speaker, speakers

View File

@ -195,6 +195,12 @@
"announce_media_error": {
"message": "Announcing clip {media_id} failed {response}"
},
"entity_not_found": {
"message": "Entity {entity_id} not found."
},
"speaker_not_found": {
"message": "{entity_id} is not a known Sonos speaker."
},
"timeout_join": {
"message": "Timeout while waiting for Sonos player to join the group {group_description}"
}

View File

@ -15,6 +15,7 @@ from homeassistant.components.media_player import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from .conftest import MockSoCo, group_speakers, ungroup_speakers
@ -85,6 +86,31 @@ async def test_media_player_join_bad_entity(
assert "media_player.bad_entity" in str(excinfo.value)
async def test_media_player_join_entity_no_speaker(
hass: HomeAssistant,
sonos_setup_two_speakers: list[MockSoCo],
entity_registry: er.EntityRegistry,
) -> None:
"""Test error handling of joining with no associated speaker."""
bad_media_player = entity_registry.async_get_or_create(
"media_player", "demo", "1234"
)
# Ensure an error is raised if the entity does not have a speaker
with pytest.raises(HomeAssistantError) as excinfo:
await hass.services.async_call(
MP_DOMAIN,
SERVICE_JOIN,
{
"entity_id": "media_player.living_room",
"group_members": bad_media_player.entity_id,
},
blocking=True,
)
assert bad_media_player.entity_id in str(excinfo.value)
@asynccontextmanager
async def instant_timeout(*args, **kwargs) -> None:
"""Mock a timeout error."""