diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 033c6e046c8..b4be0966ada 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -96,6 +96,7 @@ class SonosData: self.discovery_known: set[str] = set() self.boot_counts: dict[str, int] = {} self.mdns_names: dict[str, str] = {} + self.entity_id_mappings: dict[str, SonosSpeaker] = {} async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 65f73eaf3f0..74d310d40ec 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -14,6 +14,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( + DATA_SONOS, DOMAIN, SONOS_FAVORITES_UPDATED, SONOS_POLL_UPDATE, @@ -35,6 +36,7 @@ class SonosEntity(Entity): async def async_added_to_hass(self) -> None: """Handle common setup when added to hass.""" + self.hass.data[DATA_SONOS].entity_id_mappings[self.entity_id] = self.speaker self.async_on_remove( async_dispatcher_connect( self.hass, @@ -57,6 +59,10 @@ class SonosEntity(Entity): ) ) + async def async_will_remove_from_hass(self) -> None: + """Clean up when entity is removed.""" + del self.hass.data[DATA_SONOS].entity_id_mappings[self.entity_id] + async def async_poll(self, now: datetime.datetime) -> None: """Poll the entity if subscriptions fail.""" if not self.speaker.subscriptions_failed: diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index f4dbf8f87d0..f4ce46dd5f5 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -28,6 +28,7 @@ from homeassistant.components.media_player.const import ( REPEAT_MODE_ONE, SUPPORT_BROWSE_MEDIA, SUPPORT_CLEAR_PLAYLIST, + SUPPORT_GROUPING, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -47,6 +48,7 @@ from homeassistant.components.plex.services import play_on_sonos from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TIME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform, service from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -73,6 +75,7 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_SONOS = ( SUPPORT_BROWSE_MEDIA | SUPPORT_CLEAR_PLAYLIST + | SUPPORT_GROUPING | SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PLAY @@ -666,3 +669,18 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): f"Media not found: {media_content_type} / {media_content_id}" ) return response + + def join_players(self, group_members): + """Join `group_members` as a player group with the current player.""" + speakers = [] + for entity_id in group_members: + if speaker := self.hass.data[DATA_SONOS].entity_id_mappings.get(entity_id): + speakers.append(speaker) + else: + raise HomeAssistantError(f"Not a known Sonos entity_id: {entity_id}") + + self.speaker.join(speakers) + + def unjoin_player(self): + """Remove this player from any group.""" + self.speaker.unjoin()