More bugfixes for the new Sonos provider (#1612)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Mon, 26 Aug 2024 00:03:27 +0000 (02:03 +0200)
committerGitHub <noreply@github.com>
Mon, 26 Aug 2024 00:03:27 +0000 (02:03 +0200)
music_assistant/server/providers/sonos/__init__.py
music_assistant/server/providers/sonos/manifest.json
requirements_all.txt

index 3739267e06422cfe1981217dc0e93655dbae31ff..720e7104d68f6a2113562661731b1b926b17d1ed 100644 (file)
@@ -5,16 +5,16 @@ from __future__ import annotations
 import asyncio
 import logging
 import time
-from typing import TYPE_CHECKING, Final
+from typing import TYPE_CHECKING
 
+import shortuuid
 from aiohttp import web
+from aiosonos.api.models import ContainerType, MusicService, SonosCapability
 from aiosonos.api.models import DiscoveryInfo as SonosDiscoveryInfo
 from aiosonos.api.models import PlayBackState as SonosPlayBackState
-from aiosonos.api.models import SonosCapability
 from aiosonos.client import SonosLocalApiClient
 from aiosonos.const import EventType as SonosEventType
 from aiosonos.const import SonosEvent
-from aiosonos.exceptions import FailedCommand
 from aiosonos.utils import get_discovery_info
 from zeroconf import IPVersion, ServiceStateChange
 
@@ -26,8 +26,6 @@ from music_assistant.common.models.config_entries import (
     create_sample_rates_config_entry,
 )
 from music_assistant.common.models.enums import (
-    ConfigEntryType,
-    EventType,
     PlayerFeature,
     PlayerState,
     PlayerType,
@@ -35,7 +33,6 @@ from music_assistant.common.models.enums import (
     RepeatMode,
 )
 from music_assistant.common.models.errors import PlayerCommandFailed
-from music_assistant.common.models.event import MassEvent
 from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia
 from music_assistant.constants import (
     CONF_CROSSFADE,
@@ -74,8 +71,6 @@ SOURCE_SPOTIFY = "spotify"
 SOURCE_UNKNOWN = "unknown"
 SOURCE_RADIO = "radio"
 
-CONF_SMART_AIRPLAY: Final[str] = "smart_airplay"
-
 
 async def setup(
     mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
@@ -129,29 +124,11 @@ class SonosPlayer:
         self.mass_player: Player | None = None
         self._listen_task: asyncio.Task | None = None
         # Sonos speakers can optionally have airplay (most S2 speakers do)
-        # and this airplay player can also be a player within MA
-        # we can do some smart stuff if we link them together where possible
-        # the player if we can just guess from the sonos player id (mac address)
+        # and this airplay player can also be a player within MA.
+        # We can do some smart stuff if we link them together where possible.
+        # The player we can just guess from the sonos player id (mac address).
         self.airplay_player_id = f"ap{self.player_id[7:-5].lower()}"
-        self.stream_mode: str = "sonos"
-
-    def get_linked_airplay_player(self, active_only: bool = True) -> Player | None:
-        """Return the linked airplay player if available/enabled."""
-        if not self.mass.config.get_raw_player_config_value(
-            self.player_id, CONF_SMART_AIRPLAY, False
-        ):
-            return None
-        if not (airplay := self.mass.players.get(self.airplay_player_id)):
-            return None
-        if not airplay.available:
-            return None
-        if not active_only:
-            return airplay
-        if (airplay.powered and airplay.group_childs) or airplay.state == PlayerState.PLAYING:
-            # only when the airplay player has synced members,
-            # it makes sense to link it to the sonos player
-            return airplay
-        return None
+        self.queue_version: str = shortuuid.random(8)
 
     async def setup(self) -> None:
         """Handle setup of the player."""
@@ -198,12 +175,6 @@ class SonosPlayer:
                 SonosEventType.PLAYER_UPDATED,
             ),
         )
-        # register callback for airplay player state changes
-        self.mass.subscribe(
-            self._on_airplay_player_event,
-            (EventType.PLAYER_UPDATED, EventType.PLAYER_ADDED),
-            self.airplay_player_id,
-        )
 
     async def connect(self) -> None:
         """Connect to the Sonos player."""
@@ -241,6 +212,39 @@ class SonosPlayer:
             await self.client.disconnect()
         self.logger.debug("Disconnected from player API")
 
+    async def cmd_stop(self) -> None:
+        """Send STOP command to given player."""
+        if self.client.player.is_passive:
+            self.logger.debug("Ignore STOP command: Player is synced to another player.")
+            return
+        await self.client.player.group.stop()
+
+    async def cmd_play(self) -> None:
+        """Send PLAY command to given player."""
+        if self.client.player.is_passive:
+            self.logger.debug("Ignore STOP command: Player is synced to another player.")
+            return
+        await self.client.player.group.play()
+
+    async def cmd_pause(self) -> None:
+        """Send PAUSE command to given player."""
+        if self.client.player.is_passive:
+            self.logger.debug("Ignore STOP command: Player is synced to another player.")
+            return
+        await self.client.player.group.pause()
+
+    async def cmd_volume_set(self, volume_level: int) -> None:
+        """Send VOLUME_SET command to given player."""
+        await self.client.player.set_volume(volume_level)
+        # sync volume level with airplay player if linked
+        if airplay := self.mass.players.get(self.airplay_player_id):
+            if airplay.state not in (PlayerState.PLAYING, PlayerState.PAUSED):
+                airplay.volume_level = volume_level
+
+    async def cmd_volume_mute(self, player_id: str, muted: bool) -> None:
+        """Send VOLUME MUTE command to given player."""
+        await self.client.player.set_volume(muted=muted)
+
     def update_attributes(self) -> None:  # noqa: PLR0915
         """Update the player attributes."""
         if not self.mass_player:
@@ -268,15 +272,11 @@ class SonosPlayer:
             # player is group coordinator
             active_group = self.client.player.group
             self.mass_player.group_childs = (
-                self.client.player.group_members
+                set(self.client.player.group_members)
                 if len(self.client.player.group_members) > 1
                 else set()
             )
             self.mass_player.synced_to = None
-            if airplay := self.get_linked_airplay_player(False):
-                self.mass_player.group_childs.update(
-                    x for x in airplay.group_childs if x != self.airplay_player_id
-                )
         else:
             # player is group child (synced to another player)
             group_parent = self.prov.sonos_players.get(self.client.player.group.coordinator_id)
@@ -289,15 +289,6 @@ class SonosPlayer:
             self.mass_player.active_source = active_group.coordinator_id
             self.mass_player.can_sync_with = ()
 
-        if self.stream_mode == "airplay" and (airplay := self.get_linked_airplay_player(True)):
-            # linked airplay player is active, update media from there
-            self.mass_player.state = airplay.state
-            self.mass_player.powered = airplay.powered
-            self.mass_player.active_source = airplay.active_source
-            self.mass_player.elapsed_time = airplay.elapsed_time
-            self.mass_player.elapsed_time_last_updated = airplay.elapsed_time_last_updated
-            return
-
         # map playback state
         self.mass_player.state = PLAYBACK_STATE_MAP[active_group.playback_state]
         if (
@@ -309,37 +300,33 @@ class SonosPlayer:
         self.mass_player.elapsed_time = active_group.position
 
         # figure out the active source based on the container
-        is_playing = active_group.playback_state in (
-            SonosPlayBackState.PLAYBACK_STATE_PLAYING,
-            SonosPlayBackState.PLAYBACK_STATE_BUFFERING,
-            SonosPlayBackState.PLAYBACK_STATE_PAUSED,
-        )
-        if container := active_group.playback_metadata.get("container"):
-            if group_parent and group_parent.mass_player:
-                self.mass_player.active_source = group_parent.mass_player.active_source
-            elif (
-                container.get("type") == "linein"
-                and active_group.playback_state == SonosPlayBackState.PLAYBACK_STATE_PLAYING
+        container_type = active_group.container_type
+        active_service = active_group.active_service
+        container = active_group.playback_metadata.get("container")
+        if container_type == ContainerType.LINEIN:
+            self.mass_player.active_source = SOURCE_LINE_IN
+        elif container_type == ContainerType.AIRPLAY:
+            # check if the MA airplay player is active
+            airplay_player = self.mass.players.get(self.airplay_player_id)
+            if airplay_player and airplay_player.state in (
+                PlayerState.PLAYING,
+                PlayerState.PAUSED,
             ):
-                self.mass_player.active_source = SOURCE_LINE_IN
-            elif is_playing and container.get("type") == "linein.airplay":
-                # check if the MA airplay player is active
-                airplay_player = self.mass.players.get(self.airplay_player_id)
-                if airplay_player and airplay_player.powered:
-                    self.mass_player.active_source = airplay_player.active_source
-                else:
-                    self.mass_player.active_source = SOURCE_AIRPLAY
-            elif is_playing and container.get("type") == "station":
-                self.mass_player.active_source = SOURCE_RADIO
-            elif container.get("id", {}).get("objectId") == f"mass:queue:{self.player_id}":
-                # mass queue is active
-                self.mass_player.active_source = self.player_id
-            elif is_playing and container.get("id", {}).get("serviceId") == "9":
-                self.mass_player.active_source = SOURCE_SPOTIFY
-            elif is_playing:
-                self.mass_player.active_source = container.get("type", SOURCE_UNKNOWN)
+                self.mass_player.active_source = airplay_player.active_source
+            else:
+                self.mass_player.active_source = SOURCE_AIRPLAY
+        elif container_type == ContainerType.STATION:
+            self.mass_player.active_source = SOURCE_RADIO
+        elif active_service == MusicService.SPOTIFY:
+            self.mass_player.active_source = SOURCE_SPOTIFY
+        elif active_service == MusicService.MUSIC_ASSISTANT:
+            if container and (object_id := container.get("id", {}).get("objectId")):
+                self.mass_player.active_source = object_id.split(":")[-1]
             else:
                 self.mass_player.active_source = self.player_id
+        else:
+            # all our (known) options exhausted, fallback to unknown
+            self.mass_player.active_source = active_service
 
         if self.mass_player.active_source == self.player_id and active_group.active_session_id:
             # active source is the mass queue
@@ -390,45 +377,6 @@ class SonosPlayer:
         self.update_attributes()
         self.mass.players.update(self.player_id)
 
-    def _on_airplay_player_event(self, event: MassEvent) -> None:
-        """Handle incoming event from linked airplay player."""
-        if not self.mass.config.get_raw_player_config_value(
-            self.player_id, CONF_SMART_AIRPLAY, False
-        ):
-            return
-        if event.object_id != self.airplay_player_id:
-            return
-        airplay_player: Player = event.data
-        if (
-            self.stream_mode == "sonos"
-            and airplay_player.group_childs
-            and self.mass_player.state == PlayerState.PLAYING
-            and airplay_player.state != PlayerState.PLAYING
-            and self.mass_player.active_source == self.player_id
-            and not self.mass_player.synced_to
-        ):
-            # linked airplay player became active, transfer playback to playback
-            self.logger.info(
-                "Linked Airplay player became active, transferring playback.",
-            )
-            self.stream_mode = "airplay"
-            self.mass.create_task(self.mass.player_queues.resume(self.player_id))
-        elif (
-            self.stream_mode == "airplay"
-            and not airplay_player.group_childs
-            and airplay_player.state == PlayerState.PLAYING
-        ):
-            # linked airplay player became active, transfer playback to sonos
-            self.logger.info(
-                "Linked Airplay player became idle, transferring playback.",
-            )
-            self.stream_mode = "sonos"
-            self.mass.create_task(self.mass.players.cmd_power(self.airplay_player_id, False))
-            self.mass.create_task(self.mass.player_queues.resume(self.player_id))
-        else:
-            self.update_attributes()
-            self.mass.players.update(self.player_id)
-
 
 class SonosPlayerProvider(PlayerProvider):
     """Sonos Player provider."""
@@ -507,7 +455,7 @@ class SonosPlayerProvider(PlayerProvider):
                 if not sonos_player.connected:
                     self.logger.debug("Player back online: %s", mass_player.display_name)
                     sonos_player.client.player_ip = cur_address
-                    await sonos_player.connect(self.mass.http_session)
+                    await sonos_player.connect()
                 self.mass.players.update(player_id)
             return
         # handle new player
@@ -519,7 +467,7 @@ class SonosPlayerProvider(PlayerProvider):
     ) -> tuple[ConfigEntry, ...]:
         """Return Config Entries for the given player."""
         base_entries = await super().get_player_config_entries(player_id)
-        if not (sonos_player := self.sonos_players.get(player_id)):
+        if not (self.sonos_players.get(player_id)):
             # most probably a syncgroup or the player is not yet discovered
             return (*base_entries, CONF_ENTRY_CROSSFADE, CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED)
         return (
@@ -527,88 +475,27 @@ class SonosPlayerProvider(PlayerProvider):
             CONF_ENTRY_CROSSFADE,
             CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED,
             create_sample_rates_config_entry(48000, 24, 48000, 24, True),
-            ConfigEntry(
-                key=CONF_SMART_AIRPLAY,
-                type=ConfigEntryType.BOOLEAN,
-                label="Enable smart Airplay mode (experimental)",
-                description="Almost all newer Sonos speakers have Airplay support. "
-                "If you have the Airplay provider enabled in Music Assistant, "
-                "your Sonos speakers will also be detected as Airplay speakers, meaning "
-                "you can group them with other Airplay speakers."
-                "The downside of that is that you'll end up with a duplicated player. \n\n"
-                "This feature will try to circumvent that by linking the Sonos "
-                "player to the Airplay player: \n\nIf the AirPlay player is powered on, "
-                "and has other players grouped to it or is playing, any playback related  commands "
-                "will be automatically redirected to the Airplay player. \n\n"
-                "Effectively this will mean that the Sonos player holds the queue and for "
-                "playback, Music Assistant will automatically pick either "
-                "the Sonos or the Airplay protocol.",
-                required=False,
-                default_value=False,
-                hidden=SonosCapability.AIRPLAY
-                not in sonos_player.discovery_info["device"]["capabilities"],
-            ),
         )
 
     async def cmd_stop(self, player_id: str) -> None:
         """Send STOP command to given player."""
-        sonos_player = self.sonos_players[player_id]
-        if sonos_player.client.player.is_passive:
-            self.logger.debug(
-                "Ignore STOP command for %s: Player is synced to another player.",
-                player_id,
-            )
-            return
-        if airplay := sonos_player.get_linked_airplay_player(True):
-            # linked airplay player is active, redirect the command
-            self.logger.debug("Redirecting STOP command to linked airplay player.")
-            await self.mass.players.cmd_stop(airplay.player_id)
-            return
-        try:
-            await sonos_player.client.player.group.stop()
-        except FailedCommand:
-            # no session to stop, fallback to pause
-            await self.mass.players.cmd_pause(player_id)
+        if sonos_player := self.sonos_players[player_id]:
+            await sonos_player.cmd_stop()
 
     async def cmd_play(self, player_id: str) -> None:
         """Send PLAY command to given player."""
-        sonos_player = self.sonos_players[player_id]
-        if sonos_player.client.player.is_passive:
-            self.logger.debug(
-                "Ignore PLAY command for %s: Player is synced to another player.",
-                player_id,
-            )
-            return
-        if airplay := sonos_player.get_linked_airplay_player(True):
-            # linked airplay player is active, redirect the command
-            self.logger.debug("Redirecting PLAY command to linked airplay player.")
-            await self.mass.players.cmd_play(airplay.player_id)
-            return
-        await sonos_player.client.player.group.play()
+        if sonos_player := self.sonos_players[player_id]:
+            await sonos_player.cmd_play()
 
     async def cmd_pause(self, player_id: str) -> None:
         """Send PAUSE command to given player."""
-        sonos_player = self.sonos_players[player_id]
-        if sonos_player.client.player.is_passive:
-            self.logger.debug(
-                "Ignore PLAY command for %s: Player is synced to another player.",
-                player_id,
-            )
-            return
-        if airplay := sonos_player.get_linked_airplay_player(True):
-            # linked airplay player is active, redirect the command
-            self.logger.debug("Redirecting PAUSE command to linked airplay player.")
-            await self.mass.players.cmd_pause(airplay.player_id)
-            return
-        await sonos_player.client.player.group.pause()
+        if sonos_player := self.sonos_players[player_id]:
+            await sonos_player.cmd_pause()
 
     async def cmd_volume_set(self, player_id: str, volume_level: int) -> None:
         """Send VOLUME_SET command to given player."""
-        sonos_player = self.sonos_players[player_id]
-        await sonos_player.client.player.set_volume(volume_level)
-        # sync volume level with airplay player if linked
-        if airplay := sonos_player.get_linked_airplay_player(False):
-            airplay.volume_level = volume_level
+        if sonos_player := self.sonos_players[player_id]:
+            await sonos_player.cmd_volume_set(volume_level)
 
     async def cmd_volume_mute(self, player_id: str, muted: bool) -> None:
         """Send VOLUME MUTE command to given player."""
@@ -628,13 +515,9 @@ class SonosPlayerProvider(PlayerProvider):
     async def cmd_sync_many(self, target_player: str, child_player_ids: list[str]) -> None:
         """Create temporary sync group by joining given players to target player."""
         sonos_player = self.sonos_players[target_player]
-        if airplay_child_ids := [x for x in child_player_ids if x.startswith("ap")]:
-            # handle airplay players
-            await self.mass.players.cmd_sync_many(sonos_player.airplay_player_id, airplay_child_ids)
-        if sonos_child_ids := [x for x in child_player_ids if not x.startswith("ap")]:
-            await sonos_player.client.player.group.modify_group_members(
-                player_ids_to_add=sonos_child_ids, player_ids_to_remove=[]
-            )
+        await sonos_player.client.player.group.modify_group_members(
+            player_ids_to_add=child_player_ids, player_ids_to_remove=[]
+        )
 
     async def cmd_unsync(self, player_id: str) -> None:
         """Handle UNSYNC command for given player.
@@ -653,6 +536,7 @@ class SonosPlayerProvider(PlayerProvider):
     ) -> None:
         """Handle PLAY MEDIA on given player."""
         sonos_player = self.sonos_players[player_id]
+        sonos_player.queue_version = shortuuid.random(8)
         mass_player = self.mass.players.get(player_id)
         if sonos_player.client.player.is_passive:
             # this should be already handled by the player manager, but just in case...
@@ -662,32 +546,19 @@ class SonosPlayerProvider(PlayerProvider):
             )
             raise PlayerCommandFailed(msg)
 
-        if airplay := sonos_player.get_linked_airplay_player(True):
-            # linked airplay player is active, redirect the command
-            self.logger.debug("Redirecting PLAY_MEDIA command to linked airplay player.")
-            self.stream_mode = "airplay"
-            mass_player.active_source = airplay.player_id
-            # TODO: sonos has an annoying bug where it looses its sync childs
-            # when airplay playback is started
-            await self.mass.players.play_media(airplay.player_id, media)
-            return
-
         if media.queue_id:
             # create a sonos cloud queue and load it
-            self.stream_mode = "sonos"
             await sonos_player.client.player.group.create_playback_session()
-            mass_queue = self.mass.player_queues.get(media.queue_id)
             cloud_queue_url = f"{self.mass.streams.base_url}/sonos_queue/v2.3/"
             await sonos_player.client.player.group.play_cloud_queue(
                 cloud_queue_url,
                 http_authorization=media.queue_id,
                 item_id=media.queue_item_id,
-                queue_version=mass_queue.queue_version,
+                queue_version=sonos_player.queue_version,
             )
             return
 
         # play a single uri/url
-        self.stream_mode = "sonos"
         await sonos_player.client.player.group.play_stream_url(
             media.uri, {"name": media.title, "type": "track"}
         )
@@ -695,12 +566,6 @@ class SonosPlayerProvider(PlayerProvider):
     async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None:
         """Handle enqueuing of the next queue item on the player."""
         sonos_player = self.sonos_players[player_id]
-        if airplay := sonos_player.get_linked_airplay_player(True):
-            # linked airplay player is active, redirect the command
-            self.logger.debug("Redirecting ENQUEUE_NEXT command to linked airplay player.")
-            await self.mass.players.enqueue_next_media(airplay.player_id, media)
-            return
-
         if session_id := sonos_player.client.player.group.active_session_id:
             await sonos_player.client.api.playback_session.refresh_cloud_queue(session_id)
         # sync play modes from player queue --> sonos
@@ -736,10 +601,12 @@ class SonosPlayerProvider(PlayerProvider):
         self.logger.debug(
             "Playing announcement %s using websocket audioclip on %s",
             announcement.uri,
-            sonos_player.zone_name,
+            sonos_player.mass_player.display_name,
         )
         volume_level = self.mass.players.get_announcement_volume(player_id, volume_level)
-        await sonos_player.client.player.play_audio_clip(announcement.uri, volume_level)
+        await sonos_player.client.player.play_audio_clip(
+            announcement.uri, volume_level, name="Announcement"
+        )
 
     async def _setup_player(self, player_id: str, name: str, info: AsyncServiceInfo) -> None:
         """Handle setup of a new player that is discovered using mdns."""
@@ -788,7 +655,9 @@ class SonosPlayerProvider(PlayerProvider):
         if item_id := request.query.get("itemId"):
             queue_index = self.mass.player_queues.index_by_id(mass_queue.queue_id, item_id)
         else:
-            queue_index = mass_queue.current_index or 0
+            queue_index = mass_queue.current_index
+        if queue_index is None:
+            return web.Response(status=501)
         offset = max(queue_index - previous_window_size, 0)
         queue_items = self.mass.player_queues.items(
             sonos_player_id,
@@ -813,14 +682,15 @@ class SonosPlayerProvider(PlayerProvider):
                     else None,
                     "durationMillis": item.duration * 1000 if item.duration else None,
                     "artist": {
-                        "name": item.media_item.artist_str,
+                        "name": artist_str,
                     }
-                    if item.media_item and hasattr(item.media_item, "artist_str")
+                    if item.media_item
+                    and (artist_str := getattr(item.media_item, "artist_str", None))
                     else None,
                     "album": {
-                        "name": item.media_item.album.name,
+                        "name": album.name,
                     }
-                    if item.media_item and hasattr(item.media_item, "album")
+                    if item.media_item and (album := getattr(item.media_item, "album", None))
                     else None,
                     "quality": {
                         "bitDepth": item.streamdetails.audio_format.bit_depth,
@@ -852,10 +722,10 @@ class SonosPlayerProvider(PlayerProvider):
         self.logger.log(VERBOSE_LOG_LEVEL, "Cloud Queue Version request: %s", request.query)
         sonos_playback_id = request.headers["X-Sonos-Playback-Id"]
         sonos_player_id = sonos_playback_id.split(":")[0]
-        if not (mass_queue := self.mass.player_queues.get_active_queue(sonos_player_id)):
+        if not (sonos_player := self.sonos_players.get(sonos_player_id)):
             return web.Response(status=501)
         context_version = request.query.get("contextVersion") or "1"
-        queue_version = mass_queue.queue_version
+        queue_version = sonos_player.queue_version
         result = {"contextVersion": context_version, "queueVersion": queue_version}
         return web.json_response(result)
 
@@ -870,9 +740,11 @@ class SonosPlayerProvider(PlayerProvider):
         sonos_player_id = sonos_playback_id.split(":")[0]
         if not (mass_queue := self.mass.player_queues.get_active_queue(sonos_player_id)):
             return web.Response(status=501)
+        if not (sonos_player := self.sonos_players.get(sonos_player_id)):
+            return web.Response(status=501)
         result = {
             "contextVersion": "1",
-            "queueVersion": mass_queue.queue_version,
+            "queueVersion": sonos_player.queue_version,
             "container": {
                 "type": "playlist",
                 "name": "Music Assistant",
@@ -917,10 +789,10 @@ class SonosPlayerProvider(PlayerProvider):
         sonos_player_id = sonos_playback_id.split(":")[0]
         if not (mass_player := self.mass.players.get(sonos_player_id)):
             return web.Response(status=501)
-        if not (mass_queue := self.mass.player_queues.get(mass_player.active_source)):
+        if not (sonos_player := self.sonos_players.get(sonos_player_id)):
             return web.Response(status=501)
         for item in json_body["items"]:
-            if item["queueVersion"] != mass_queue.queue_version:
+            if item["queueVersion"] != sonos_player.queue_version:
                 continue
             if "positionMillis" not in item:
                 continue
index 067621429a531ef9f5a0bf819ad6d5f59dba6131..d952d53e3c264d83d02eb672dce814f1eeb7009c 100644 (file)
@@ -4,7 +4,7 @@
   "name": "SONOS",
   "description": "SONOS Player provider for Music Assistant.",
   "codeowners": ["@music-assistant"],
-  "requirements": ["aiosonos==0.1.2"],
+  "requirements": ["aiosonos==0.1.3"],
   "documentation": "https://music-assistant.io/player-support/sonos/",
   "multi_instance": false,
   "builtin": false,
index b2cbb1830b5c75d6ce6d9c7c22a3d7ea0866c6fa..190356eb94a2ce61560339d10a397f8fa2ce8c32 100644 (file)
@@ -7,7 +7,7 @@ aiohttp==3.10.4
 aiojellyfin==0.10.1
 aiorun==2024.8.1
 aioslimproto==3.0.1
-aiosonos==0.1.2
+aiosonos==0.1.3
 aiosqlite==0.20.0
 async-upnp-client==0.40.0
 bidict==0.23.1