diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 30fdb28b02f..f7f5e2722ff 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -258,6 +258,15 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if self.coordinator.uid == uid: self.async_write_ha_state() + @property + def available(self) -> bool: + """Return if the media_player is available.""" + return ( + self.speaker.available + and self.speaker.sonos_group_entities + and self.media.playback_status + ) + @property def coordinator(self) -> SonosSpeaker: """Return the current coordinator SonosSpeaker.""" diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 4435a65db3d..4e4661b389b 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -189,7 +189,12 @@ class SonosSpeaker: def setup(self, entry: ConfigEntry) -> None: """Run initial setup of the speaker.""" - self.set_basic_info() + self.media.play_mode = self.soco.play_mode + self.update_volume() + self.update_groups() + if self.is_coordinator: + self.media.poll_media() + future = asyncio.run_coroutine_threadsafe( self.async_setup_dispatchers(entry), self.hass.loop ) @@ -247,11 +252,6 @@ class SonosSpeaker: """Write states for associated SonosEntity instances.""" async_dispatcher_send(self.hass, f"{SONOS_STATE_UPDATED}-{self.soco.uid}") - def set_basic_info(self) -> None: - """Set basic information when speaker is reconnected.""" - self.media.play_mode = self.soco.play_mode - self.update_volume() - # # Properties # @@ -456,6 +456,34 @@ class SonosSpeaker: @callback def async_dispatch_media_update(self, event: SonosEvent) -> None: """Update information about currently playing media from an event.""" + # The new coordinator can be provided in a media update event but + # before the ZoneGroupState updates. If this happens the playback + # state will be incorrect and should be ignored. Switching to the + # new coordinator will use its media. The regrouping process will + # be completed during the next ZoneGroupState update. + av_transport_uri = event.variables.get("av_transport_uri", "") + current_track_uri = event.variables.get("current_track_uri", "") + if av_transport_uri == current_track_uri and av_transport_uri.startswith( + "x-rincon:" + ): + new_coordinator_uid = av_transport_uri.split(":")[-1] + if new_coordinator_speaker := self.hass.data[DATA_SONOS].discovered.get( + new_coordinator_uid + ): + _LOGGER.debug( + "Media update coordinator (%s) received for %s", + new_coordinator_speaker.zone_name, + self.zone_name, + ) + self.coordinator = new_coordinator_speaker + else: + _LOGGER.debug( + "Media update coordinator (%s) for %s not yet available", + new_coordinator_uid, + self.zone_name, + ) + return + if crossfade := event.variables.get("current_crossfade_mode"): self.cross_fade = bool(int(crossfade)) @@ -774,6 +802,7 @@ class SonosSpeaker: self.zone_name, uid, ) + return if self.sonos_group_entities == sonos_group_entities: # Useful in polling mode for speakers with stereo pairs or surrounds diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index d18a5eecc8a..8c804f466d4 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -120,6 +120,7 @@ def soco_fixture( mock_soco.get_battery_info.return_value = battery_info mock_soco.all_zones = {mock_soco} mock_soco.visible_zones = {mock_soco} + mock_soco.group.coordinator = mock_soco yield mock_soco