diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index 6f22d8ab417..ac8cd00d9db 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -7,11 +7,13 @@ from typing import Any, Callable from pysonos.exceptions import SoCoException, SoCoUPnPException +from homeassistant.exceptions import HomeAssistantError + _LOGGER = logging.getLogger(__name__) def soco_error(errorcodes: list[str] | None = None) -> Callable: - """Filter out specified UPnP errors from logs and avoid exceptions.""" + """Filter out specified UPnP errors and raise exceptions for service calls.""" def decorator(funct: Callable) -> Callable: """Decorate functions.""" @@ -21,11 +23,15 @@ def soco_error(errorcodes: list[str] | None = None) -> Callable: """Wrap for all soco UPnP exception.""" try: return funct(*args, **kwargs) - except SoCoUPnPException as err: - if not errorcodes or err.error_code not in errorcodes: - _LOGGER.error("Error on %s with %s", funct.__name__, err) - except SoCoException as err: - _LOGGER.error("Error on %s with %s", funct.__name__, err) + except (OSError, SoCoException, SoCoUPnPException) as err: + error_code = getattr(err, "error_code", None) + function = funct.__name__ + if errorcodes and error_code in errorcodes: + _LOGGER.debug( + "Error code %s ignored in call to %s", error_code, function + ) + return + raise HomeAssistantError(f"Error calling {function}: {err}") from err return wrapper diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 8b1664d4f6c..a4cc6e175ec 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -13,8 +13,6 @@ from pysonos.core import ( PLAY_MODE_BY_MEANING, PLAY_MODES, ) -from pysonos.exceptions import SoCoUPnPException -from pysonos.plugins.sharelink import ShareLinkPlugin import voluptuous as vol from homeassistant.components.media_player import MediaPlayerEntity @@ -518,6 +516,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): If media_id is a Plex payload, attempt Plex->Sonos playback. + If media_id is a Sonos or Tidal share link, attempt playback + using the respective service. + If media_type is "playlist", media_id should be a Sonos Playlist name. Otherwise, media_id should be a URI. @@ -527,28 +528,21 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if media_id and media_id.startswith(PLEX_URI_SCHEME): media_id = media_id[len(PLEX_URI_SCHEME) :] play_on_sonos(self.hass, media_type, media_id, self.name) # type: ignore[no-untyped-call] - elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): - share_link = ShareLinkPlugin(soco) + return + + share_link = self.speaker.share_link + if share_link.is_share_link(media_id): if kwargs.get(ATTR_MEDIA_ENQUEUE): - try: - if share_link.is_share_link(media_id): - share_link.add_share_link_to_queue(media_id) - else: - soco.add_uri_to_queue(media_id) - except SoCoUPnPException: - _LOGGER.error( - 'Error parsing media uri "%s", ' - "please check it's a valid media resource " - "supported by Sonos", - media_id, - ) + share_link.add_share_link_to_queue(media_id) else: - if share_link.is_share_link(media_id): - soco.clear_queue() - share_link.add_share_link_to_queue(media_id) - soco.play_from_queue(0) - else: - soco.play_uri(media_id) + soco.clear_queue() + share_link.add_share_link_to_queue(media_id) + soco.play_from_queue(0) + elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): + if kwargs.get(ATTR_MEDIA_ENQUEUE): + soco.add_uri_to_queue(media_id) + else: + soco.play_uri(media_id) elif media_type == MEDIA_TYPE_PLAYLIST: if media_id.startswith("S:"): item = get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call] @@ -557,11 +551,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): try: playlists = soco.get_sonos_playlists() playlist = next(p for p in playlists if p.title == media_id) + except StopIteration: + _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id) + else: soco.clear_queue() soco.add_to_queue(playlist) soco.play_from_queue(0) - except StopIteration: - _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id) elif media_type in PLAYABLE_MEDIA_TYPES: item = get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call] diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 88e6ac33ba7..b010fa623be 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -16,6 +16,7 @@ from pysonos.data_structures import DidlAudioBroadcast, DidlPlaylistContainer from pysonos.events_base import Event as SonosEvent, SubscriptionBase from pysonos.exceptions import SoCoException from pysonos.music_library import MusicLibrary +from pysonos.plugins.sharelink import ShareLinkPlugin from pysonos.snapshot import Snapshot from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN @@ -147,6 +148,7 @@ class SonosSpeaker: self.soco = soco self.household_id: str = soco.household_id self.media = SonosMedia(soco) + self._share_link_plugin: ShareLinkPlugin | None = None # Synchronization helpers self._is_ready: bool = False @@ -292,6 +294,13 @@ class SonosSpeaker: """Return true if player is a coordinator.""" return self.coordinator is None + @property + def share_link(self) -> ShareLinkPlugin: + """Cache the ShareLinkPlugin instance for this speaker.""" + if not self._share_link_plugin: + self._share_link_plugin = ShareLinkPlugin(self.soco) + return self._share_link_plugin + @property def subscription_address(self) -> str | None: """Return the current subscription callback address if any.""" @@ -476,6 +485,8 @@ class SonosSpeaker: self, now: datetime.datetime | None = None, will_reconnect: bool = False ) -> None: """Make this player unavailable when it was not seen recently.""" + self._share_link_plugin = None + if self._seen_timer: self._seen_timer() self._seen_timer = None