Improve Sonos Spotify/Tidal support, add service exceptions (#51871)
This commit is contained in:
parent
016ba39dfb
commit
d3724355cf
3 changed files with 42 additions and 30 deletions
|
@ -7,11 +7,13 @@ from typing import Any, Callable
|
||||||
|
|
||||||
from pysonos.exceptions import SoCoException, SoCoUPnPException
|
from pysonos.exceptions import SoCoException, SoCoUPnPException
|
||||||
|
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def soco_error(errorcodes: list[str] | None = None) -> Callable:
|
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:
|
def decorator(funct: Callable) -> Callable:
|
||||||
"""Decorate functions."""
|
"""Decorate functions."""
|
||||||
|
@ -21,11 +23,15 @@ def soco_error(errorcodes: list[str] | None = None) -> Callable:
|
||||||
"""Wrap for all soco UPnP exception."""
|
"""Wrap for all soco UPnP exception."""
|
||||||
try:
|
try:
|
||||||
return funct(*args, **kwargs)
|
return funct(*args, **kwargs)
|
||||||
except SoCoUPnPException as err:
|
except (OSError, SoCoException, SoCoUPnPException) as err:
|
||||||
if not errorcodes or err.error_code not in errorcodes:
|
error_code = getattr(err, "error_code", None)
|
||||||
_LOGGER.error("Error on %s with %s", funct.__name__, err)
|
function = funct.__name__
|
||||||
except SoCoException as err:
|
if errorcodes and error_code in errorcodes:
|
||||||
_LOGGER.error("Error on %s with %s", funct.__name__, err)
|
_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
|
return wrapper
|
||||||
|
|
||||||
|
|
|
@ -13,8 +13,6 @@ from pysonos.core import (
|
||||||
PLAY_MODE_BY_MEANING,
|
PLAY_MODE_BY_MEANING,
|
||||||
PLAY_MODES,
|
PLAY_MODES,
|
||||||
)
|
)
|
||||||
from pysonos.exceptions import SoCoUPnPException
|
|
||||||
from pysonos.plugins.sharelink import ShareLinkPlugin
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.media_player import MediaPlayerEntity
|
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 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
|
If media_type is "playlist", media_id should be a Sonos
|
||||||
Playlist name. Otherwise, media_id should be a URI.
|
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):
|
if media_id and media_id.startswith(PLEX_URI_SCHEME):
|
||||||
media_id = media_id[len(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]
|
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):
|
return
|
||||||
share_link = ShareLinkPlugin(soco)
|
|
||||||
|
share_link = self.speaker.share_link
|
||||||
|
if share_link.is_share_link(media_id):
|
||||||
if kwargs.get(ATTR_MEDIA_ENQUEUE):
|
if kwargs.get(ATTR_MEDIA_ENQUEUE):
|
||||||
try:
|
share_link.add_share_link_to_queue(media_id)
|
||||||
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,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
if share_link.is_share_link(media_id):
|
soco.clear_queue()
|
||||||
soco.clear_queue()
|
share_link.add_share_link_to_queue(media_id)
|
||||||
share_link.add_share_link_to_queue(media_id)
|
soco.play_from_queue(0)
|
||||||
soco.play_from_queue(0)
|
elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK):
|
||||||
else:
|
if kwargs.get(ATTR_MEDIA_ENQUEUE):
|
||||||
soco.play_uri(media_id)
|
soco.add_uri_to_queue(media_id)
|
||||||
|
else:
|
||||||
|
soco.play_uri(media_id)
|
||||||
elif media_type == MEDIA_TYPE_PLAYLIST:
|
elif media_type == MEDIA_TYPE_PLAYLIST:
|
||||||
if media_id.startswith("S:"):
|
if media_id.startswith("S:"):
|
||||||
item = get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call]
|
item = get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call]
|
||||||
|
@ -557,11 +551,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||||
try:
|
try:
|
||||||
playlists = soco.get_sonos_playlists()
|
playlists = soco.get_sonos_playlists()
|
||||||
playlist = next(p for p in playlists if p.title == media_id)
|
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.clear_queue()
|
||||||
soco.add_to_queue(playlist)
|
soco.add_to_queue(playlist)
|
||||||
soco.play_from_queue(0)
|
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:
|
elif media_type in PLAYABLE_MEDIA_TYPES:
|
||||||
item = get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call]
|
item = get_media(self.media.library, media_id, media_type) # type: ignore[no-untyped-call]
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ from pysonos.data_structures import DidlAudioBroadcast, DidlPlaylistContainer
|
||||||
from pysonos.events_base import Event as SonosEvent, SubscriptionBase
|
from pysonos.events_base import Event as SonosEvent, SubscriptionBase
|
||||||
from pysonos.exceptions import SoCoException
|
from pysonos.exceptions import SoCoException
|
||||||
from pysonos.music_library import MusicLibrary
|
from pysonos.music_library import MusicLibrary
|
||||||
|
from pysonos.plugins.sharelink import ShareLinkPlugin
|
||||||
from pysonos.snapshot import Snapshot
|
from pysonos.snapshot import Snapshot
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||||
|
@ -147,6 +148,7 @@ class SonosSpeaker:
|
||||||
self.soco = soco
|
self.soco = soco
|
||||||
self.household_id: str = soco.household_id
|
self.household_id: str = soco.household_id
|
||||||
self.media = SonosMedia(soco)
|
self.media = SonosMedia(soco)
|
||||||
|
self._share_link_plugin: ShareLinkPlugin | None = None
|
||||||
|
|
||||||
# Synchronization helpers
|
# Synchronization helpers
|
||||||
self._is_ready: bool = False
|
self._is_ready: bool = False
|
||||||
|
@ -292,6 +294,13 @@ class SonosSpeaker:
|
||||||
"""Return true if player is a coordinator."""
|
"""Return true if player is a coordinator."""
|
||||||
return self.coordinator is None
|
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
|
@property
|
||||||
def subscription_address(self) -> str | None:
|
def subscription_address(self) -> str | None:
|
||||||
"""Return the current subscription callback address if any."""
|
"""Return the current subscription callback address if any."""
|
||||||
|
@ -476,6 +485,8 @@ class SonosSpeaker:
|
||||||
self, now: datetime.datetime | None = None, will_reconnect: bool = False
|
self, now: datetime.datetime | None = None, will_reconnect: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Make this player unavailable when it was not seen recently."""
|
"""Make this player unavailable when it was not seen recently."""
|
||||||
|
self._share_link_plugin = None
|
||||||
|
|
||||||
if self._seen_timer:
|
if self._seen_timer:
|
||||||
self._seen_timer()
|
self._seen_timer()
|
||||||
self._seen_timer = None
|
self._seen_timer = None
|
||||||
|
|
Loading…
Add table
Reference in a new issue