mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Add Get Queue HEOS entity service (#141150)
This commit is contained in:
parent
f3bcb96b41
commit
ab709aeb46
@ -4,6 +4,7 @@ ATTR_PASSWORD = "password"
|
|||||||
ATTR_USERNAME = "username"
|
ATTR_USERNAME = "username"
|
||||||
DOMAIN = "heos"
|
DOMAIN = "heos"
|
||||||
ENTRY_TITLE = "HEOS System"
|
ENTRY_TITLE = "HEOS System"
|
||||||
|
SERVICE_GET_QUEUE = "get_queue"
|
||||||
SERVICE_GROUP_VOLUME_SET = "group_volume_set"
|
SERVICE_GROUP_VOLUME_SET = "group_volume_set"
|
||||||
SERVICE_GROUP_VOLUME_DOWN = "group_volume_down"
|
SERVICE_GROUP_VOLUME_DOWN = "group_volume_down"
|
||||||
SERVICE_GROUP_VOLUME_UP = "group_volume_up"
|
SERVICE_GROUP_VOLUME_UP = "group_volume_up"
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
|
"get_queue": {
|
||||||
|
"service": "mdi:playlist-music"
|
||||||
|
},
|
||||||
"group_volume_set": {
|
"group_volume_set": {
|
||||||
"service": "mdi:volume-medium"
|
"service": "mdi:volume-medium"
|
||||||
},
|
},
|
||||||
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Awaitable, Callable, Coroutine, Sequence
|
from collections.abc import Awaitable, Callable, Coroutine, Sequence
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
import dataclasses
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import reduce, wraps
|
from functools import reduce, wraps
|
||||||
import logging
|
import logging
|
||||||
@ -42,7 +43,12 @@ from homeassistant.components.media_player import (
|
|||||||
)
|
)
|
||||||
from homeassistant.components.media_source import BrowseMediaSource
|
from homeassistant.components.media_source import BrowseMediaSource
|
||||||
from homeassistant.const import Platform
|
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.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
config_validation as cv,
|
config_validation as cv,
|
||||||
@ -56,6 +62,7 @@ from homeassistant.util.dt import utcnow
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN as HEOS_DOMAIN,
|
DOMAIN as HEOS_DOMAIN,
|
||||||
|
SERVICE_GET_QUEUE,
|
||||||
SERVICE_GROUP_VOLUME_DOWN,
|
SERVICE_GROUP_VOLUME_DOWN,
|
||||||
SERVICE_GROUP_VOLUME_SET,
|
SERVICE_GROUP_VOLUME_SET,
|
||||||
SERVICE_GROUP_VOLUME_UP,
|
SERVICE_GROUP_VOLUME_UP,
|
||||||
@ -132,6 +139,12 @@ async def async_setup_entry(
|
|||||||
"""Add media players for a config entry."""
|
"""Add media players for a config entry."""
|
||||||
# Register custom entity services
|
# Register custom entity services
|
||||||
platform = entity_platform.async_get_current_platform()
|
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(
|
platform.async_register_entity_service(
|
||||||
SERVICE_GROUP_VOLUME_SET,
|
SERVICE_GROUP_VOLUME_SET,
|
||||||
{vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float},
|
{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()))
|
add_entities_callback(list(coordinator.heos.players.values()))
|
||||||
|
|
||||||
|
|
||||||
type _FuncType[**_P] = Callable[_P, Awaitable[Any]]
|
type _FuncType[**_P, _R] = Callable[_P, Awaitable[_R]]
|
||||||
type _ReturnFuncType[**_P] = Callable[_P, Coroutine[Any, Any, None]]
|
type _ReturnFuncType[**_P, _R] = Callable[_P, Coroutine[Any, Any, _R]]
|
||||||
|
|
||||||
|
|
||||||
def catch_action_error[**_P](
|
def catch_action_error[**_P, _R](
|
||||||
action: str,
|
action: str,
|
||||||
) -> Callable[[_FuncType[_P]], _ReturnFuncType[_P]]:
|
) -> Callable[[_FuncType[_P, _R]], _ReturnFuncType[_P, _R]]:
|
||||||
"""Return decorator that catches errors and raises HomeAssistantError."""
|
"""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)
|
@wraps(func)
|
||||||
async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> None:
|
async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
|
||||||
try:
|
try:
|
||||||
await func(*args, **kwargs)
|
return await func(*args, **kwargs)
|
||||||
except (HeosError, ValueError) as ex:
|
except (HeosError, ValueError) as ex:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=HEOS_DOMAIN,
|
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))
|
self.async_on_remove(self._player.add_on_player_event(self._player_update))
|
||||||
await super().async_added_to_hass()
|
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")
|
@catch_action_error("clear playlist")
|
||||||
async def async_clear_playlist(self) -> None:
|
async def async_clear_playlist(self) -> None:
|
||||||
"""Clear players playlist."""
|
"""Clear players playlist."""
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
get_queue:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
integration: heos
|
||||||
|
domain: media_player
|
||||||
|
|
||||||
group_volume_set:
|
group_volume_set:
|
||||||
target:
|
target:
|
||||||
entity:
|
entity:
|
||||||
|
@ -86,6 +86,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"get_queue": {
|
||||||
|
"name": "Get queue",
|
||||||
|
"description": "Retrieves the queue of the media player."
|
||||||
|
},
|
||||||
"group_volume_down": {
|
"group_volume_down": {
|
||||||
"name": "Turn down group volume",
|
"name": "Turn down group volume",
|
||||||
"description": "Turns down the group volume."
|
"description": "Turns down the group volume."
|
||||||
|
@ -37,6 +37,7 @@ class MockHeos(Heos):
|
|||||||
self.play_preset_station: AsyncMock = AsyncMock()
|
self.play_preset_station: AsyncMock = AsyncMock()
|
||||||
self.play_url: AsyncMock = AsyncMock()
|
self.play_url: AsyncMock = AsyncMock()
|
||||||
self.player_clear_queue: AsyncMock = AsyncMock()
|
self.player_clear_queue: AsyncMock = AsyncMock()
|
||||||
|
self.player_get_queue: AsyncMock = AsyncMock()
|
||||||
self.player_get_quick_selects: AsyncMock = AsyncMock()
|
self.player_get_quick_selects: AsyncMock = AsyncMock()
|
||||||
self.player_play_next: AsyncMock = AsyncMock()
|
self.player_play_next: AsyncMock = AsyncMock()
|
||||||
self.player_play_previous: AsyncMock = AsyncMock()
|
self.player_play_previous: AsyncMock = AsyncMock()
|
||||||
|
@ -20,6 +20,7 @@ from pyheos import (
|
|||||||
NetworkType,
|
NetworkType,
|
||||||
PlayerUpdateResult,
|
PlayerUpdateResult,
|
||||||
PlayState,
|
PlayState,
|
||||||
|
QueueItem,
|
||||||
RepeatType,
|
RepeatType,
|
||||||
const,
|
const,
|
||||||
)
|
)
|
||||||
@ -359,3 +360,28 @@ def change_data_fixture() -> PlayerUpdateResult:
|
|||||||
def change_data_mapped_ids_fixture() -> PlayerUpdateResult:
|
def change_data_mapped_ids_fixture() -> PlayerUpdateResult:
|
||||||
"""Create player change data for testing."""
|
"""Create player change data for testing."""
|
||||||
return PlayerUpdateResult(updated_player_ids={1: 101})
|
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",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
@ -159,6 +159,32 @@
|
|||||||
'title': 'Music Sources',
|
'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
|
# name: test_state_attributes
|
||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
|
@ -15,6 +15,7 @@ from pyheos import (
|
|||||||
MediaType as HeosMediaType,
|
MediaType as HeosMediaType,
|
||||||
PlayerUpdateResult,
|
PlayerUpdateResult,
|
||||||
PlayState,
|
PlayState,
|
||||||
|
QueueItem,
|
||||||
RepeatType,
|
RepeatType,
|
||||||
SignalHeosEvent,
|
SignalHeosEvent,
|
||||||
SignalType,
|
SignalType,
|
||||||
@ -27,6 +28,7 @@ from syrupy.filters import props
|
|||||||
|
|
||||||
from homeassistant.components.heos.const import (
|
from homeassistant.components.heos.const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
SERVICE_GET_QUEUE,
|
||||||
SERVICE_GROUP_VOLUME_DOWN,
|
SERVICE_GROUP_VOLUME_DOWN,
|
||||||
SERVICE_GROUP_VOLUME_SET,
|
SERVICE_GROUP_VOLUME_SET,
|
||||||
SERVICE_GROUP_VOLUME_UP,
|
SERVICE_GROUP_VOLUME_UP,
|
||||||
@ -1696,3 +1698,27 @@ async def test_media_player_group_fails_wrong_integration(
|
|||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
controller.set_group.assert_not_called()
|
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user