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
This commit is contained in:
Pete Sage 2024-08-28 10:19:48 -04:00 committed by GitHub
parent 57a73d1b1b
commit 5824d06fd7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 129 additions and 3 deletions

View file

@ -64,6 +64,9 @@
},
"update_alarm": {
"service": "mdi:alarm"
},
"get_queue": {
"service": "mdi:queue-first-in-last-out"
}
}
}

View file

@ -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."""

View file

@ -63,6 +63,11 @@ remove_from_queue:
max: 10000
mode: box
get_queue:
target:
entity:
domain: media_player
update_alarm:
target:
device:

View file

@ -172,6 +172,10 @@
"description": "Enable or disable including grouped rooms."
}
}
},
"get_queue": {
"name": "Get queue",
"description": "Returns the contents of the queue."
}
},
"exceptions": {

View file

@ -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."""

View file

@ -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:*"
}
]
}
]

View file

@ -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',
}),
]),
})
# ---

View file

@ -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