From 5824d06fd772249ca789695c1196a86bd4c48c80 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Wed, 28 Aug 2024 10:19:48 -0400 Subject: [PATCH] Add get_queue action for Sonos (#124707) * initial commit * use constants * use constants * update typing * add queue fixture * remove blank line * update docstring * update icons * use list comprehension --- homeassistant/components/sonos/icons.json | 3 ++ .../components/sonos/media_player.py | 30 +++++++++++++++++-- homeassistant/components/sonos/services.yaml | 5 ++++ homeassistant/components/sonos/strings.json | 4 +++ tests/components/sonos/conftest.py | 19 +++++++++++- .../sonos/fixtures/sonos_queue.json | 30 +++++++++++++++++++ .../sonos/snapshots/test_media_player.ambr | 18 +++++++++++ tests/components/sonos/test_media_player.py | 23 ++++++++++++++ 8 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 tests/components/sonos/fixtures/sonos_queue.json diff --git a/homeassistant/components/sonos/icons.json b/homeassistant/components/sonos/icons.json index e7403b45453..45027d8eabd 100644 --- a/homeassistant/components/sonos/icons.json +++ b/homeassistant/components/sonos/icons.json @@ -64,6 +64,9 @@ }, "update_alarm": { "service": "mdi:alarm" + }, + "get_queue": { + "service": "mdi:queue-first-in-last-out" } } } diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index e68d3dfa97a..75527bdcb72 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -14,7 +14,7 @@ from soco.core import ( PLAY_MODE_BY_MEANING, PLAY_MODES, ) -from soco.data_structures import DidlFavorite +from soco.data_structures import DidlFavorite, DidlMusicTrack from soco.ms_data_structures import MusicServiceItem from sonos_websocket.exception import SonosWebsocketError import voluptuous as vol @@ -22,8 +22,12 @@ import voluptuous as vol from homeassistant.components import media_source, spotify from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, + ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ANNOUNCE, + ATTR_MEDIA_ARTIST, + ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_ENQUEUE, + ATTR_MEDIA_TITLE, BrowseMedia, MediaPlayerDeviceClass, MediaPlayerEnqueue, @@ -38,7 +42,7 @@ from homeassistant.components.plex import PLEX_URI_SCHEME from homeassistant.components.plex.services import process_plex_payload from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TIME -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import config_validation as cv, entity_platform, service from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -88,6 +92,7 @@ SERVICE_CLEAR_TIMER = "clear_sleep_timer" SERVICE_UPDATE_ALARM = "update_alarm" SERVICE_PLAY_QUEUE = "play_queue" SERVICE_REMOVE_FROM_QUEUE = "remove_from_queue" +SERVICE_GET_QUEUE = "get_queue" ATTR_SLEEP_TIME = "sleep_time" ATTR_ALARM_ID = "alarm_id" @@ -190,6 +195,13 @@ async def async_setup_entry( "remove_from_queue", ) + platform.async_register_entity_service( + SERVICE_GET_QUEUE, + None, + "get_queue", + supports_response=SupportsResponse.ONLY, + ) + class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Representation of a Sonos entity.""" @@ -741,6 +753,20 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Remove item from the queue.""" self.coordinator.soco.remove_from_queue(queue_position) + @soco_error() + def get_queue(self) -> list[dict]: + """Get the queue.""" + queue: list[DidlMusicTrack] = self.coordinator.soco.get_queue(max_items=0) + return [ + { + ATTR_MEDIA_TITLE: track.title, + ATTR_MEDIA_ALBUM_NAME: track.album, + ATTR_MEDIA_ARTIST: track.creator, + ATTR_MEDIA_CONTENT_ID: track.get_uri(), + } + for track in queue + ] + @property def extra_state_attributes(self) -> dict[str, Any]: """Return entity specific state attributes.""" diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index f6df83ef6ed..6d6e7ef83f9 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -63,6 +63,11 @@ remove_from_queue: max: 10000 mode: box +get_queue: + target: + entity: + domain: media_player + update_alarm: target: device: diff --git a/homeassistant/components/sonos/strings.json b/homeassistant/components/sonos/strings.json index 7a73378d69b..264420ef758 100644 --- a/homeassistant/components/sonos/strings.json +++ b/homeassistant/components/sonos/strings.json @@ -172,6 +172,10 @@ "description": "Enable or disable including grouped rooms." } } + }, + "get_queue": { + "name": "Get queue", + "description": "Returns the contents of the queue." } }, "exceptions": { diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 6abb010557e..04b35e2c021 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -10,7 +10,12 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from soco import SoCo from soco.alarms import Alarms -from soco.data_structures import DidlFavorite, DidlPlaylistContainer, SearchResult +from soco.data_structures import ( + DidlFavorite, + DidlMusicTrack, + DidlPlaylistContainer, + SearchResult, +) from soco.events_base import Event as SonosEvent from homeassistant.components import ssdp, zeroconf @@ -185,6 +190,7 @@ class SoCoMockFactory: battery_info, alarm_clock, sonos_playlists: SearchResult, + sonos_queue: list[DidlMusicTrack], ) -> None: """Initialize the mock factory.""" self.mock_list: dict[str, MockSoCo] = {} @@ -194,6 +200,7 @@ class SoCoMockFactory: self.battery_info = battery_info self.alarm_clock = alarm_clock self.sonos_playlists = sonos_playlists + self.sonos_queue = sonos_queue def cache_mock( self, mock_soco: MockSoCo, ip_address: str, name: str = "Zone A" @@ -207,6 +214,7 @@ class SoCoMockFactory: mock_soco.get_current_track_info.return_value = self.current_track_info mock_soco.music_source_from_uri = SoCo.music_source_from_uri mock_soco.get_sonos_playlists.return_value = self.sonos_playlists + mock_soco.get_queue.return_value = self.sonos_queue my_speaker_info = self.speaker_info.copy() my_speaker_info["zone_name"] = name my_speaker_info["uid"] = mock_soco.uid @@ -277,6 +285,7 @@ def soco_factory( alarm_clock, sonos_playlists: SearchResult, sonos_websocket, + sonos_queue: list[DidlMusicTrack], ): """Create factory for instantiating SoCo mocks.""" factory = SoCoMockFactory( @@ -286,6 +295,7 @@ def soco_factory( battery_info, alarm_clock, sonos_playlists, + sonos_queue=sonos_queue, ) with ( patch("homeassistant.components.sonos.SoCo", new=factory.get_mock), @@ -370,6 +380,13 @@ def sonos_playlists_fixture() -> SearchResult: return SearchResult(playlists_list, "sonos_playlists", 1, 1, 0) +@pytest.fixture(name="sonos_queue") +def sonos_queue() -> list[DidlMusicTrack]: + """Create sonos queue fixture.""" + queue = load_json_value_fixture("sonos_queue.json", "sonos") + return [DidlMusicTrack.from_dict(track) for track in queue] + + class MockMusicServiceItem: """Mocks a Soco MusicServiceItem.""" diff --git a/tests/components/sonos/fixtures/sonos_queue.json b/tests/components/sonos/fixtures/sonos_queue.json new file mode 100644 index 00000000000..50689a00e1d --- /dev/null +++ b/tests/components/sonos/fixtures/sonos_queue.json @@ -0,0 +1,30 @@ +[ + { + "title": "Something", + "album": "Abbey Road", + "creator": "The Beatles", + "item_id": "Q:0/1", + "parent_id": "Q:0", + "original_track_number": 3, + "resources": [ + { + "uri": "x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3", + "protocol_info": "file:*:audio/mpegurl:*" + } + ] + }, + { + "title": "Come Together", + "album": "Abbey Road", + "creator": "The Beatles", + "item_id": "Q:0/2", + "parent_id": "Q:0", + "original_track_number": 1, + "resources": [ + { + "uri": "x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/01%20Come%20Together.mp3", + "protocol_info": "file:*:audio/mpegurl:*" + } + ] + } +] diff --git a/tests/components/sonos/snapshots/test_media_player.ambr b/tests/components/sonos/snapshots/test_media_player.ambr index 9c43bceb43b..f382d341de6 100644 --- a/tests/components/sonos/snapshots/test_media_player.ambr +++ b/tests/components/sonos/snapshots/test_media_player.ambr @@ -56,3 +56,21 @@ 'state': 'idle', }) # --- +# name: test_media_get_queue + dict({ + 'media_player.zone_a': list([ + dict({ + 'media_album_name': 'Abbey Road', + 'media_artist': 'The Beatles', + 'media_content_id': 'x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3', + 'media_title': 'Something', + }), + dict({ + 'media_album_name': 'Abbey Road', + 'media_artist': 'The Beatles', + 'media_content_id': 'x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/01%20Come%20Together.mp3', + 'media_title': 'Come Together', + }), + ]), + }) +# --- diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index fa77293fbde..ac877f47904 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -32,6 +32,7 @@ from homeassistant.components.sonos.const import ( ) from homeassistant.components.sonos.media_player import ( LONG_SERVICE_TIMEOUT, + SERVICE_GET_QUEUE, SERVICE_RESTORE, SERVICE_SNAPSHOT, VOLUME_INCREMENT, @@ -1121,3 +1122,25 @@ async def test_play_media_announce( blocking=True, ) assert sonos_websocket.play_clip.call_count == 1 + + +async def test_media_get_queue( + hass: HomeAssistant, + soco: MockSoCo, + async_autosetup_sonos, + soco_factory, + snapshot: SnapshotAssertion, +) -> None: + """Test getting the media queue.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + result = await hass.services.async_call( + SONOS_DOMAIN, + SERVICE_GET_QUEUE, + { + ATTR_ENTITY_ID: "media_player.zone_a", + }, + blocking=True, + return_response=True, + ) + soco_mock.get_queue.assert_called_with(max_items=0) + assert result == snapshot