Add Get Queue HEOS entity service (#141150)

This commit is contained in:
Andrew Sayre 2025-03-25 16:55:44 -05:00 committed by GitHub
parent f3bcb96b41
commit ab709aeb46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 120 additions and 8 deletions

View File

@ -4,6 +4,7 @@ ATTR_PASSWORD = "password"
ATTR_USERNAME = "username"
DOMAIN = "heos"
ENTRY_TITLE = "HEOS System"
SERVICE_GET_QUEUE = "get_queue"
SERVICE_GROUP_VOLUME_SET = "group_volume_set"
SERVICE_GROUP_VOLUME_DOWN = "group_volume_down"
SERVICE_GROUP_VOLUME_UP = "group_volume_up"

View File

@ -1,5 +1,8 @@
{
"services": {
"get_queue": {
"service": "mdi:playlist-music"
},
"group_volume_set": {
"service": "mdi:volume-medium"
},

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine, Sequence
from contextlib import suppress
import dataclasses
from datetime import datetime
from functools import reduce, wraps
import logging
@ -42,7 +43,12 @@ from homeassistant.components.media_player import (
)
from homeassistant.components.media_source import BrowseMediaSource
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import (
HomeAssistant,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import (
config_validation as cv,
@ -56,6 +62,7 @@ from homeassistant.util.dt import utcnow
from .const import (
DOMAIN as HEOS_DOMAIN,
SERVICE_GET_QUEUE,
SERVICE_GROUP_VOLUME_DOWN,
SERVICE_GROUP_VOLUME_SET,
SERVICE_GROUP_VOLUME_UP,
@ -132,6 +139,12 @@ async def async_setup_entry(
"""Add media players for a config entry."""
# Register custom entity services
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
SERVICE_GET_QUEUE,
None,
"async_get_queue",
supports_response=SupportsResponse.ONLY,
)
platform.async_register_entity_service(
SERVICE_GROUP_VOLUME_SET,
{vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float},
@ -155,20 +168,20 @@ async def async_setup_entry(
add_entities_callback(list(coordinator.heos.players.values()))
type _FuncType[**_P] = Callable[_P, Awaitable[Any]]
type _ReturnFuncType[**_P] = Callable[_P, Coroutine[Any, Any, None]]
type _FuncType[**_P, _R] = Callable[_P, Awaitable[_R]]
type _ReturnFuncType[**_P, _R] = Callable[_P, Coroutine[Any, Any, _R]]
def catch_action_error[**_P](
def catch_action_error[**_P, _R](
action: str,
) -> Callable[[_FuncType[_P]], _ReturnFuncType[_P]]:
) -> Callable[[_FuncType[_P, _R]], _ReturnFuncType[_P, _R]]:
"""Return decorator that catches errors and raises HomeAssistantError."""
def decorator(func: _FuncType[_P]) -> _ReturnFuncType[_P]:
def decorator(func: _FuncType[_P, _R]) -> _ReturnFuncType[_P, _R]:
@wraps(func)
async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> None:
async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
try:
await func(*args, **kwargs)
return await func(*args, **kwargs)
except (HeosError, ValueError) as ex:
raise HomeAssistantError(
translation_domain=HEOS_DOMAIN,
@ -268,6 +281,12 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity):
self.async_on_remove(self._player.add_on_player_event(self._player_update))
await super().async_added_to_hass()
@catch_action_error("get queue")
async def async_get_queue(self) -> ServiceResponse:
"""Get the queue for the current player."""
queue = await self._player.get_queue()
return {"queue": [dataclasses.asdict(item) for item in queue]}
@catch_action_error("clear playlist")
async def async_clear_playlist(self) -> None:
"""Clear players playlist."""

View File

@ -1,3 +1,9 @@
get_queue:
target:
entity:
integration: heos
domain: media_player
group_volume_set:
target:
entity:

View File

@ -86,6 +86,10 @@
}
}
},
"get_queue": {
"name": "Get queue",
"description": "Retrieves the queue of the media player."
},
"group_volume_down": {
"name": "Turn down group volume",
"description": "Turns down the group volume."

View File

@ -37,6 +37,7 @@ class MockHeos(Heos):
self.play_preset_station: AsyncMock = AsyncMock()
self.play_url: AsyncMock = AsyncMock()
self.player_clear_queue: AsyncMock = AsyncMock()
self.player_get_queue: AsyncMock = AsyncMock()
self.player_get_quick_selects: AsyncMock = AsyncMock()
self.player_play_next: AsyncMock = AsyncMock()
self.player_play_previous: AsyncMock = AsyncMock()

View File

@ -20,6 +20,7 @@ from pyheos import (
NetworkType,
PlayerUpdateResult,
PlayState,
QueueItem,
RepeatType,
const,
)
@ -359,3 +360,28 @@ def change_data_fixture() -> PlayerUpdateResult:
def change_data_mapped_ids_fixture() -> PlayerUpdateResult:
"""Create player change data for testing."""
return PlayerUpdateResult(updated_player_ids={1: 101})
@pytest.fixture(name="queue")
def queue_fixture() -> list[QueueItem]:
"""Create a queue fixture."""
return [
QueueItem(
queue_id=1,
song="Espresso",
album="Espresso",
artist="Sabrina Carpenter",
image_url="http://resources.wimpmusic.com/images/e4f2d75f/a69e/4b8a/b800/e18546b1ad4c/640x640.jpg",
media_id="356276483",
album_id="356276481",
),
QueueItem(
queue_id=2,
song="A Bar Song (Tipsy)",
album="A Bar Song (Tipsy)",
artist="Shaboozey",
image_url="http://resources.wimpmusic.com/images/d05b8da3/4fae/45ff/ac1b/7ab7caab3523/640x640.jpg",
media_id="354365598",
album_id="354365596",
),
]

View File

@ -159,6 +159,32 @@
'title': 'Music Sources',
})
# ---
# name: test_get_queue
dict({
'media_player.test_player': dict({
'queue': list([
dict({
'album': 'Espresso',
'album_id': '356276481',
'artist': 'Sabrina Carpenter',
'image_url': 'http://resources.wimpmusic.com/images/e4f2d75f/a69e/4b8a/b800/e18546b1ad4c/640x640.jpg',
'media_id': '356276483',
'queue_id': 1,
'song': 'Espresso',
}),
dict({
'album': 'A Bar Song (Tipsy)',
'album_id': '354365596',
'artist': 'Shaboozey',
'image_url': 'http://resources.wimpmusic.com/images/d05b8da3/4fae/45ff/ac1b/7ab7caab3523/640x640.jpg',
'media_id': '354365598',
'queue_id': 2,
'song': 'A Bar Song (Tipsy)',
}),
]),
}),
})
# ---
# name: test_state_attributes
StateSnapshot({
'attributes': ReadOnlyDict({

View File

@ -15,6 +15,7 @@ from pyheos import (
MediaType as HeosMediaType,
PlayerUpdateResult,
PlayState,
QueueItem,
RepeatType,
SignalHeosEvent,
SignalType,
@ -27,6 +28,7 @@ from syrupy.filters import props
from homeassistant.components.heos.const import (
DOMAIN,
SERVICE_GET_QUEUE,
SERVICE_GROUP_VOLUME_DOWN,
SERVICE_GROUP_VOLUME_SET,
SERVICE_GROUP_VOLUME_UP,
@ -1696,3 +1698,27 @@ async def test_media_player_group_fails_wrong_integration(
blocking=True,
)
controller.set_group.assert_not_called()
async def test_get_queue(
hass: HomeAssistant,
config_entry: MockConfigEntry,
controller: MockHeos,
queue: list[QueueItem],
snapshot: SnapshotAssertion,
) -> None:
"""Test the get queue service."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
controller.player_get_queue.return_value = queue
response = await hass.services.async_call(
DOMAIN,
SERVICE_GET_QUEUE,
{
ATTR_ENTITY_ID: "media_player.test_player",
},
blocking=True,
return_response=True,
)
controller.player_get_queue.assert_called_once_with(1, None, None)
assert response == snapshot