diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index 1fb3bb3d5e74..e83b0132a0e6 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -15,6 +15,7 @@ from soco.exceptions import SoCoException, SoCoUPnPException from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import dispatcher_send from .const import SONOS_SPEAKER_ACTIVITY @@ -135,6 +136,7 @@ class UnjoinData: speakers: list[SonosSpeaker] = field(default_factory=list) event: asyncio.Event = field(default_factory=asyncio.Event) + exception: HomeAssistantError | OSError | SoCoException | None = None @dataclass diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index f7d72587c5b0..cb48037524e4 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -15,6 +15,7 @@ from soco.core import ( PLAY_MODES, ) from soco.data_structures import DidlFavorite, DidlMusicTrack +from soco.exceptions import SoCoException from soco.ms_data_structures import MusicServiceItem from sonos_websocket.exception import SonosWebsocketError @@ -853,10 +854,14 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): _LOGGER.debug( "Processing unjoins for %s", [x.zone_name for x in unjoin_data.speakers] ) - await SonosSpeaker.unjoin_multi( - self.hass, self.config_entry, unjoin_data.speakers - ) - unjoin_data.event.set() + try: + await SonosSpeaker.unjoin_multi( + self.hass, self.config_entry, unjoin_data.speakers + ) + except (HomeAssistantError, SoCoException, OSError) as err: + unjoin_data.exception = err + finally: + unjoin_data.event.set() if unjoin_data := sonos_data.unjoin_data.get(household_id): unjoin_data.speakers.append(self.speaker) @@ -868,3 +873,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): _LOGGER.debug("Requesting unjoin for %s", self.speaker.zone_name) await unjoin_data.event.wait() + + # Re-raise any exception that occurred during processing + if unjoin_data.exception: + raise unjoin_data.exception diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index ed2d4add7ba9..e760b8db3e8b 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -1060,7 +1060,10 @@ class SonosSpeaker: async with config_entry.runtime_data.topology_condition: await hass.async_add_executor_job(_unjoin_all, speakers) await SonosSpeaker.wait_for_groups( - hass, config_entry, [[s] for s in speakers] + hass, + config_entry, + [[s] for s in speakers], + action="unjoin", ) @soco_error() @@ -1204,6 +1207,7 @@ class SonosSpeaker: hass: HomeAssistant, config_entry: SonosConfigEntry, groups: list[list[SonosSpeaker]], + action: str = "join", ) -> None: """Wait until all groups are present, or timeout.""" @@ -1228,14 +1232,17 @@ class SonosSpeaker: while not _test_groups(groups): await config_entry.runtime_data.topology_condition.wait() except TimeoutError: - group_description = [ + group_description = "; ".join( f"{group[0].zone_name}: {', '.join(speaker.zone_name for speaker in group)}" for group in groups - ] + ) raise HomeAssistantError( translation_domain=DOMAIN, translation_key="timeout_join", - translation_placeholders={"group_description": str(group_description)}, + translation_placeholders={ + "group_description": group_description, + "action": action, + }, ) from TimeoutError any_speaker = next(iter(config_entry.runtime_data.discovered.values())) any_speaker.soco.zone_group_state.clear_cache() diff --git a/homeassistant/components/sonos/strings.json b/homeassistant/components/sonos/strings.json index 71b6ffe6c63f..902aed2b7b72 100644 --- a/homeassistant/components/sonos/strings.json +++ b/homeassistant/components/sonos/strings.json @@ -125,7 +125,7 @@ "message": "{entity_id} is not a known Sonos speaker." }, "timeout_join": { - "message": "Timeout while waiting for Sonos player to join the group {group_description}" + "message": "Timeout while waiting for Sonos player to {action} the group {group_description}" } }, "issues": { diff --git a/tests/components/sonos/test_services.py b/tests/components/sonos/test_services.py index a94a03b95a03..3bc85d3b1662 100644 --- a/tests/components/sonos/test_services.py +++ b/tests/components/sonos/test_services.py @@ -13,6 +13,7 @@ from homeassistant.components.media_player import ( SERVICE_JOIN, SERVICE_UNJOIN, ) +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -131,7 +132,7 @@ async def test_media_player_join_timeout( expected = ( "Timeout while waiting for Sonos player to join the " - "group ['Living Room: Living Room, Bedroom']" + "group Living Room: Living Room, Bedroom" ) with ( patch( @@ -153,6 +154,37 @@ async def test_media_player_join_timeout( assert soco_living_room.join.call_count == 0 +async def test_media_player_unjoin_timeout( + hass: HomeAssistant, + sonos_setup_two_speakers: list[MockSoCo], +) -> None: + """Test unjoining of speaker with timeout error.""" + + soco_living_room = sonos_setup_two_speakers[0] + soco_bedroom = sonos_setup_two_speakers[1] + + # First group the speakers together + group_speakers(soco_living_room, soco_bedroom) + await hass.async_block_till_done(wait_background_tasks=True) + + expected = ( + "Timeout while waiting for Sonos player to unjoin the group Bedroom: Bedroom" + ) + with ( + patch( + "homeassistant.components.sonos.speaker.asyncio.timeout", instant_timeout + ), + pytest.raises(HomeAssistantError, match=re.escape(expected)), + ): + await hass.services.async_call( + MP_DOMAIN, + SERVICE_UNJOIN, + {ATTR_ENTITY_ID: "media_player.bedroom"}, + blocking=True, + ) + assert soco_bedroom.unjoin.call_count == 1 + + async def test_media_player_unjoin( hass: HomeAssistant, sonos_setup_two_speakers: list[MockSoCo],