diff --git a/homeassistant/components/heos/const.py b/homeassistant/components/heos/const.py index 6d603f7ad30..789fbc12b8e 100644 --- a/homeassistant/components/heos/const.py +++ b/homeassistant/components/heos/const.py @@ -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" diff --git a/homeassistant/components/heos/icons.json b/homeassistant/components/heos/icons.json index d7a998b6aec..c957ac1939c 100644 --- a/homeassistant/components/heos/icons.json +++ b/homeassistant/components/heos/icons.json @@ -1,5 +1,8 @@ { "services": { + "get_queue": { + "service": "mdi:playlist-music" + }, "group_volume_set": { "service": "mdi:volume-medium" }, diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 311190ccb74..9cd01051b95 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -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.""" diff --git a/homeassistant/components/heos/services.yaml b/homeassistant/components/heos/services.yaml index 8f3a43421f6..fa79bd03096 100644 --- a/homeassistant/components/heos/services.yaml +++ b/homeassistant/components/heos/services.yaml @@ -1,3 +1,9 @@ +get_queue: + target: + entity: + integration: heos + domain: media_player + group_volume_set: target: entity: diff --git a/homeassistant/components/heos/strings.json b/homeassistant/components/heos/strings.json index 593c437accc..38e3349b7c0 100644 --- a/homeassistant/components/heos/strings.json +++ b/homeassistant/components/heos/strings.json @@ -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." diff --git a/tests/components/heos/__init__.py b/tests/components/heos/__init__.py index cb4313bbd10..34eba8a9c76 100644 --- a/tests/components/heos/__init__.py +++ b/tests/components/heos/__init__.py @@ -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() diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py index 5d06d1812ea..835e4436398 100644 --- a/tests/components/heos/conftest.py +++ b/tests/components/heos/conftest.py @@ -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", + ), + ] diff --git a/tests/components/heos/snapshots/test_media_player.ambr b/tests/components/heos/snapshots/test_media_player.ambr index 4cf84363ba0..d366a7f6317 100644 --- a/tests/components/heos/snapshots/test_media_player.ambr +++ b/tests/components/heos/snapshots/test_media_player.ambr @@ -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({ diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index d5bc8cab488..474d606b5b1 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -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