mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
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:
parent
57a73d1b1b
commit
5824d06fd7
@ -64,6 +64,9 @@
|
|||||||
},
|
},
|
||||||
"update_alarm": {
|
"update_alarm": {
|
||||||
"service": "mdi:alarm"
|
"service": "mdi:alarm"
|
||||||
|
},
|
||||||
|
"get_queue": {
|
||||||
|
"service": "mdi:queue-first-in-last-out"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ from soco.core import (
|
|||||||
PLAY_MODE_BY_MEANING,
|
PLAY_MODE_BY_MEANING,
|
||||||
PLAY_MODES,
|
PLAY_MODES,
|
||||||
)
|
)
|
||||||
from soco.data_structures import DidlFavorite
|
from soco.data_structures import DidlFavorite, DidlMusicTrack
|
||||||
from soco.ms_data_structures import MusicServiceItem
|
from soco.ms_data_structures import MusicServiceItem
|
||||||
from sonos_websocket.exception import SonosWebsocketError
|
from sonos_websocket.exception import SonosWebsocketError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -22,8 +22,12 @@ import voluptuous as vol
|
|||||||
from homeassistant.components import media_source, spotify
|
from homeassistant.components import media_source, spotify
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
ATTR_INPUT_SOURCE,
|
ATTR_INPUT_SOURCE,
|
||||||
|
ATTR_MEDIA_ALBUM_NAME,
|
||||||
ATTR_MEDIA_ANNOUNCE,
|
ATTR_MEDIA_ANNOUNCE,
|
||||||
|
ATTR_MEDIA_ARTIST,
|
||||||
|
ATTR_MEDIA_CONTENT_ID,
|
||||||
ATTR_MEDIA_ENQUEUE,
|
ATTR_MEDIA_ENQUEUE,
|
||||||
|
ATTR_MEDIA_TITLE,
|
||||||
BrowseMedia,
|
BrowseMedia,
|
||||||
MediaPlayerDeviceClass,
|
MediaPlayerDeviceClass,
|
||||||
MediaPlayerEnqueue,
|
MediaPlayerEnqueue,
|
||||||
@ -38,7 +42,7 @@ from homeassistant.components.plex import PLEX_URI_SCHEME
|
|||||||
from homeassistant.components.plex.services import process_plex_payload
|
from homeassistant.components.plex.services import process_plex_payload
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_TIME
|
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.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
from homeassistant.helpers import config_validation as cv, entity_platform, service
|
from homeassistant.helpers import config_validation as cv, entity_platform, service
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
@ -88,6 +92,7 @@ SERVICE_CLEAR_TIMER = "clear_sleep_timer"
|
|||||||
SERVICE_UPDATE_ALARM = "update_alarm"
|
SERVICE_UPDATE_ALARM = "update_alarm"
|
||||||
SERVICE_PLAY_QUEUE = "play_queue"
|
SERVICE_PLAY_QUEUE = "play_queue"
|
||||||
SERVICE_REMOVE_FROM_QUEUE = "remove_from_queue"
|
SERVICE_REMOVE_FROM_QUEUE = "remove_from_queue"
|
||||||
|
SERVICE_GET_QUEUE = "get_queue"
|
||||||
|
|
||||||
ATTR_SLEEP_TIME = "sleep_time"
|
ATTR_SLEEP_TIME = "sleep_time"
|
||||||
ATTR_ALARM_ID = "alarm_id"
|
ATTR_ALARM_ID = "alarm_id"
|
||||||
@ -190,6 +195,13 @@ async def async_setup_entry(
|
|||||||
"remove_from_queue",
|
"remove_from_queue",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
platform.async_register_entity_service(
|
||||||
|
SERVICE_GET_QUEUE,
|
||||||
|
None,
|
||||||
|
"get_queue",
|
||||||
|
supports_response=SupportsResponse.ONLY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||||
"""Representation of a Sonos entity."""
|
"""Representation of a Sonos entity."""
|
||||||
@ -741,6 +753,20 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
|||||||
"""Remove item from the queue."""
|
"""Remove item from the queue."""
|
||||||
self.coordinator.soco.remove_from_queue(queue_position)
|
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
|
@property
|
||||||
def extra_state_attributes(self) -> dict[str, Any]:
|
def extra_state_attributes(self) -> dict[str, Any]:
|
||||||
"""Return entity specific state attributes."""
|
"""Return entity specific state attributes."""
|
||||||
|
@ -63,6 +63,11 @@ remove_from_queue:
|
|||||||
max: 10000
|
max: 10000
|
||||||
mode: box
|
mode: box
|
||||||
|
|
||||||
|
get_queue:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: media_player
|
||||||
|
|
||||||
update_alarm:
|
update_alarm:
|
||||||
target:
|
target:
|
||||||
device:
|
device:
|
||||||
|
@ -172,6 +172,10 @@
|
|||||||
"description": "Enable or disable including grouped rooms."
|
"description": "Enable or disable including grouped rooms."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"get_queue": {
|
||||||
|
"name": "Get queue",
|
||||||
|
"description": "Returns the contents of the queue."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
|
@ -10,7 +10,12 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
from soco import SoCo
|
from soco import SoCo
|
||||||
from soco.alarms import Alarms
|
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 soco.events_base import Event as SonosEvent
|
||||||
|
|
||||||
from homeassistant.components import ssdp, zeroconf
|
from homeassistant.components import ssdp, zeroconf
|
||||||
@ -185,6 +190,7 @@ class SoCoMockFactory:
|
|||||||
battery_info,
|
battery_info,
|
||||||
alarm_clock,
|
alarm_clock,
|
||||||
sonos_playlists: SearchResult,
|
sonos_playlists: SearchResult,
|
||||||
|
sonos_queue: list[DidlMusicTrack],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the mock factory."""
|
"""Initialize the mock factory."""
|
||||||
self.mock_list: dict[str, MockSoCo] = {}
|
self.mock_list: dict[str, MockSoCo] = {}
|
||||||
@ -194,6 +200,7 @@ class SoCoMockFactory:
|
|||||||
self.battery_info = battery_info
|
self.battery_info = battery_info
|
||||||
self.alarm_clock = alarm_clock
|
self.alarm_clock = alarm_clock
|
||||||
self.sonos_playlists = sonos_playlists
|
self.sonos_playlists = sonos_playlists
|
||||||
|
self.sonos_queue = sonos_queue
|
||||||
|
|
||||||
def cache_mock(
|
def cache_mock(
|
||||||
self, mock_soco: MockSoCo, ip_address: str, name: str = "Zone A"
|
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.get_current_track_info.return_value = self.current_track_info
|
||||||
mock_soco.music_source_from_uri = SoCo.music_source_from_uri
|
mock_soco.music_source_from_uri = SoCo.music_source_from_uri
|
||||||
mock_soco.get_sonos_playlists.return_value = self.sonos_playlists
|
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 = self.speaker_info.copy()
|
||||||
my_speaker_info["zone_name"] = name
|
my_speaker_info["zone_name"] = name
|
||||||
my_speaker_info["uid"] = mock_soco.uid
|
my_speaker_info["uid"] = mock_soco.uid
|
||||||
@ -277,6 +285,7 @@ def soco_factory(
|
|||||||
alarm_clock,
|
alarm_clock,
|
||||||
sonos_playlists: SearchResult,
|
sonos_playlists: SearchResult,
|
||||||
sonos_websocket,
|
sonos_websocket,
|
||||||
|
sonos_queue: list[DidlMusicTrack],
|
||||||
):
|
):
|
||||||
"""Create factory for instantiating SoCo mocks."""
|
"""Create factory for instantiating SoCo mocks."""
|
||||||
factory = SoCoMockFactory(
|
factory = SoCoMockFactory(
|
||||||
@ -286,6 +295,7 @@ def soco_factory(
|
|||||||
battery_info,
|
battery_info,
|
||||||
alarm_clock,
|
alarm_clock,
|
||||||
sonos_playlists,
|
sonos_playlists,
|
||||||
|
sonos_queue=sonos_queue,
|
||||||
)
|
)
|
||||||
with (
|
with (
|
||||||
patch("homeassistant.components.sonos.SoCo", new=factory.get_mock),
|
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)
|
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:
|
class MockMusicServiceItem:
|
||||||
"""Mocks a Soco MusicServiceItem."""
|
"""Mocks a Soco MusicServiceItem."""
|
||||||
|
|
||||||
|
30
tests/components/sonos/fixtures/sonos_queue.json
Normal file
30
tests/components/sonos/fixtures/sonos_queue.json
Normal 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:*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
@ -56,3 +56,21 @@
|
|||||||
'state': 'idle',
|
'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',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
@ -32,6 +32,7 @@ from homeassistant.components.sonos.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.components.sonos.media_player import (
|
from homeassistant.components.sonos.media_player import (
|
||||||
LONG_SERVICE_TIMEOUT,
|
LONG_SERVICE_TIMEOUT,
|
||||||
|
SERVICE_GET_QUEUE,
|
||||||
SERVICE_RESTORE,
|
SERVICE_RESTORE,
|
||||||
SERVICE_SNAPSHOT,
|
SERVICE_SNAPSHOT,
|
||||||
VOLUME_INCREMENT,
|
VOLUME_INCREMENT,
|
||||||
@ -1121,3 +1122,25 @@ async def test_play_media_announce(
|
|||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
assert sonos_websocket.play_clip.call_count == 1
|
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user