mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Add actions to Music Assistant integration (#129515)
Co-authored-by: Franck Nijhof <git@frenck.dev> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
3eb483c1b0
commit
3485ce9c71
@ -28,13 +28,13 @@ from .const import DOMAIN, LOGGER
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from music_assistant_models.event import MassEvent
|
from music_assistant_models.event import MassEvent
|
||||||
|
|
||||||
type MusicAssistantConfigEntry = ConfigEntry[MusicAssistantEntryData]
|
|
||||||
|
|
||||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||||
|
|
||||||
CONNECT_TIMEOUT = 10
|
CONNECT_TIMEOUT = 10
|
||||||
LISTEN_READY_TIMEOUT = 30
|
LISTEN_READY_TIMEOUT = 30
|
||||||
|
|
||||||
|
type MusicAssistantConfigEntry = ConfigEntry[MusicAssistantEntryData]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MusicAssistantEntryData:
|
class MusicAssistantEntryData:
|
||||||
@ -47,7 +47,7 @@ class MusicAssistantEntryData:
|
|||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: MusicAssistantConfigEntry
|
hass: HomeAssistant, entry: MusicAssistantConfigEntry
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Set up from a config entry."""
|
"""Set up Music Assistant from a config entry."""
|
||||||
http_session = async_get_clientsession(hass, verify_ssl=False)
|
http_session = async_get_clientsession(hass, verify_ssl=False)
|
||||||
mass_url = entry.data[CONF_URL]
|
mass_url = entry.data[CONF_URL]
|
||||||
mass = MusicAssistantClient(mass_url, http_session)
|
mass = MusicAssistantClient(mass_url, http_session)
|
||||||
@ -97,6 +97,7 @@ async def async_setup_entry(
|
|||||||
listen_task.cancel()
|
listen_task.cancel()
|
||||||
raise ConfigEntryNotReady("Music Assistant client not ready") from err
|
raise ConfigEntryNotReady("Music Assistant client not ready") from err
|
||||||
|
|
||||||
|
# store the listen task and mass client in the entry data
|
||||||
entry.runtime_data = MusicAssistantEntryData(mass, listen_task)
|
entry.runtime_data = MusicAssistantEntryData(mass, listen_task)
|
||||||
|
|
||||||
# If the listen task is already failed, we need to raise ConfigEntryNotReady
|
# If the listen task is already failed, we need to raise ConfigEntryNotReady
|
||||||
|
7
homeassistant/components/music_assistant/icons.json
Normal file
7
homeassistant/components/music_assistant/icons.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"services": {
|
||||||
|
"play_media": { "service": "mdi:play" },
|
||||||
|
"play_announcement": { "service": "mdi:bullhorn" },
|
||||||
|
"transfer_queue": { "service": "mdi:transfer" }
|
||||||
|
}
|
||||||
|
}
|
@ -13,15 +13,18 @@ from music_assistant_models.enums import (
|
|||||||
EventType,
|
EventType,
|
||||||
MediaType,
|
MediaType,
|
||||||
PlayerFeature,
|
PlayerFeature,
|
||||||
|
PlayerState as MassPlayerState,
|
||||||
QueueOption,
|
QueueOption,
|
||||||
RepeatMode as MassRepeatMode,
|
RepeatMode as MassRepeatMode,
|
||||||
)
|
)
|
||||||
from music_assistant_models.errors import MediaNotFoundError, MusicAssistantError
|
from music_assistant_models.errors import MediaNotFoundError, MusicAssistantError
|
||||||
from music_assistant_models.event import MassEvent
|
from music_assistant_models.event import MassEvent
|
||||||
from music_assistant_models.media_items import ItemMapping, MediaItemType, Track
|
from music_assistant_models.media_items import ItemMapping, MediaItemType, Track
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import media_source
|
from homeassistant.components import media_source
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
|
ATTR_MEDIA_ENQUEUE,
|
||||||
ATTR_MEDIA_EXTRA,
|
ATTR_MEDIA_EXTRA,
|
||||||
BrowseMedia,
|
BrowseMedia,
|
||||||
MediaPlayerDeviceClass,
|
MediaPlayerDeviceClass,
|
||||||
@ -37,7 +40,11 @@ from homeassistant.const import STATE_OFF
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.entity_platform import (
|
||||||
|
AddEntitiesCallback,
|
||||||
|
async_get_current_platform,
|
||||||
|
)
|
||||||
from homeassistant.util.dt import utc_from_timestamp
|
from homeassistant.util.dt import utc_from_timestamp
|
||||||
|
|
||||||
from . import MusicAssistantConfigEntry
|
from . import MusicAssistantConfigEntry
|
||||||
@ -79,6 +86,9 @@ QUEUE_OPTION_MAP = {
|
|||||||
MediaPlayerEnqueue.REPLACE: QueueOption.REPLACE,
|
MediaPlayerEnqueue.REPLACE: QueueOption.REPLACE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SERVICE_PLAY_MEDIA_ADVANCED = "play_media"
|
||||||
|
SERVICE_PLAY_ANNOUNCEMENT = "play_announcement"
|
||||||
|
SERVICE_TRANSFER_QUEUE = "transfer_queue"
|
||||||
ATTR_RADIO_MODE = "radio_mode"
|
ATTR_RADIO_MODE = "radio_mode"
|
||||||
ATTR_MEDIA_ID = "media_id"
|
ATTR_MEDIA_ID = "media_id"
|
||||||
ATTR_MEDIA_TYPE = "media_type"
|
ATTR_MEDIA_TYPE = "media_type"
|
||||||
@ -138,6 +148,38 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
async_add_entities(mass_players)
|
async_add_entities(mass_players)
|
||||||
|
|
||||||
|
# add platform service for play_media with advanced options
|
||||||
|
platform = async_get_current_platform()
|
||||||
|
platform.async_register_entity_service(
|
||||||
|
SERVICE_PLAY_MEDIA_ADVANCED,
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_MEDIA_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||||
|
vol.Optional(ATTR_MEDIA_TYPE): vol.Coerce(MediaType),
|
||||||
|
vol.Optional(ATTR_MEDIA_ENQUEUE): vol.Coerce(QueueOption),
|
||||||
|
vol.Optional(ATTR_ARTIST): cv.string,
|
||||||
|
vol.Optional(ATTR_ALBUM): cv.string,
|
||||||
|
vol.Optional(ATTR_RADIO_MODE): vol.Coerce(bool),
|
||||||
|
},
|
||||||
|
"_async_handle_play_media",
|
||||||
|
)
|
||||||
|
platform.async_register_entity_service(
|
||||||
|
SERVICE_PLAY_ANNOUNCEMENT,
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_URL): cv.string,
|
||||||
|
vol.Optional(ATTR_USE_PRE_ANNOUNCE): vol.Coerce(bool),
|
||||||
|
vol.Optional(ATTR_ANNOUNCE_VOLUME): vol.Coerce(int),
|
||||||
|
},
|
||||||
|
"_async_handle_play_announcement",
|
||||||
|
)
|
||||||
|
platform.async_register_entity_service(
|
||||||
|
SERVICE_TRANSFER_QUEUE,
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_SOURCE_PLAYER): cv.entity_id,
|
||||||
|
vol.Optional(ATTR_AUTO_PLAY): vol.Coerce(bool),
|
||||||
|
},
|
||||||
|
"_async_handle_transfer_queue",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||||
"""Representation of MediaPlayerEntity from Music Assistant Player."""
|
"""Representation of MediaPlayerEntity from Music Assistant Player."""
|
||||||
@ -376,6 +418,8 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
|||||||
async def _async_handle_play_media(
|
async def _async_handle_play_media(
|
||||||
self,
|
self,
|
||||||
media_id: list[str],
|
media_id: list[str],
|
||||||
|
artist: str | None = None,
|
||||||
|
album: str | None = None,
|
||||||
enqueue: MediaPlayerEnqueue | QueueOption | None = None,
|
enqueue: MediaPlayerEnqueue | QueueOption | None = None,
|
||||||
radio_mode: bool | None = None,
|
radio_mode: bool | None = None,
|
||||||
media_type: str | None = None,
|
media_type: str | None = None,
|
||||||
@ -402,6 +446,14 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
|||||||
elif await asyncio.to_thread(os.path.isfile, media_id_str):
|
elif await asyncio.to_thread(os.path.isfile, media_id_str):
|
||||||
media_uris.append(media_id_str)
|
media_uris.append(media_id_str)
|
||||||
continue
|
continue
|
||||||
|
# last resort: search for media item by name/search
|
||||||
|
if item := await self.mass.music.get_item_by_name(
|
||||||
|
name=media_id_str,
|
||||||
|
artist=artist,
|
||||||
|
album=album,
|
||||||
|
media_type=MediaType(media_type) if media_type else None,
|
||||||
|
):
|
||||||
|
media_uris.append(item.uri)
|
||||||
|
|
||||||
if not media_uris:
|
if not media_uris:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
@ -435,6 +487,32 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
|||||||
self.player_id, url, use_pre_announce, announce_volume
|
self.player_id, url, use_pre_announce, announce_volume
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@catch_musicassistant_error
|
||||||
|
async def _async_handle_transfer_queue(
|
||||||
|
self, source_player: str | None = None, auto_play: bool | None = None
|
||||||
|
) -> None:
|
||||||
|
"""Transfer the current queue to another player."""
|
||||||
|
if not source_player:
|
||||||
|
# no source player given; try to find a playing player(queue)
|
||||||
|
for queue in self.mass.player_queues:
|
||||||
|
if queue.state == MassPlayerState.PLAYING:
|
||||||
|
source_queue_id = queue.queue_id
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
"Source player not specified and no playing player found."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# resolve HA entity_id to MA player_id
|
||||||
|
entity_registry = er.async_get(self.hass)
|
||||||
|
if (entity := entity_registry.async_get(source_player)) is None:
|
||||||
|
raise HomeAssistantError("Source player not available.")
|
||||||
|
source_queue_id = entity.unique_id # unique_id is the MA player_id
|
||||||
|
target_queue_id = self.player_id
|
||||||
|
await self.mass.player_queues.transfer_queue(
|
||||||
|
source_queue_id, target_queue_id, auto_play
|
||||||
|
)
|
||||||
|
|
||||||
async def async_browse_media(
|
async def async_browse_media(
|
||||||
self,
|
self,
|
||||||
media_content_type: MediaType | str | None = None,
|
media_content_type: MediaType | str | None = None,
|
||||||
|
90
homeassistant/components/music_assistant/services.yaml
Normal file
90
homeassistant/components/music_assistant/services.yaml
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# Descriptions for Music Assistant custom services
|
||||||
|
|
||||||
|
play_media:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: media_player
|
||||||
|
integration: music_assistant
|
||||||
|
supported_features:
|
||||||
|
- media_player.MediaPlayerEntityFeature.PLAY_MEDIA
|
||||||
|
fields:
|
||||||
|
media_id:
|
||||||
|
required: true
|
||||||
|
example: "spotify://playlist/aabbccddeeff"
|
||||||
|
selector:
|
||||||
|
object:
|
||||||
|
media_type:
|
||||||
|
example: "playlist"
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
translation_key: media_type
|
||||||
|
options:
|
||||||
|
- artist
|
||||||
|
- album
|
||||||
|
- playlist
|
||||||
|
- track
|
||||||
|
- radio
|
||||||
|
artist:
|
||||||
|
example: "Queen"
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
album:
|
||||||
|
example: "News of the world"
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
enqueue:
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- "play"
|
||||||
|
- "replace"
|
||||||
|
- "next"
|
||||||
|
- "replace_next"
|
||||||
|
- "add"
|
||||||
|
translation_key: enqueue
|
||||||
|
radio_mode:
|
||||||
|
advanced: true
|
||||||
|
selector:
|
||||||
|
boolean:
|
||||||
|
|
||||||
|
play_announcement:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: media_player
|
||||||
|
integration: music_assistant
|
||||||
|
supported_features:
|
||||||
|
- media_player.MediaPlayerEntityFeature.PLAY_MEDIA
|
||||||
|
- media_player.MediaPlayerEntityFeature.MEDIA_ANNOUNCE
|
||||||
|
fields:
|
||||||
|
url:
|
||||||
|
required: true
|
||||||
|
example: "http://someremotesite.com/doorbell.mp3"
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
use_pre_announce:
|
||||||
|
example: "true"
|
||||||
|
selector:
|
||||||
|
boolean:
|
||||||
|
announce_volume:
|
||||||
|
example: 75
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1
|
||||||
|
max: 100
|
||||||
|
step: 1
|
||||||
|
|
||||||
|
transfer_queue:
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
domain: media_player
|
||||||
|
integration: music_assistant
|
||||||
|
fields:
|
||||||
|
source_player:
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain: media_player
|
||||||
|
integration: music_assistant
|
||||||
|
auto_play:
|
||||||
|
example: "true"
|
||||||
|
selector:
|
||||||
|
boolean:
|
@ -37,6 +37,70 @@
|
|||||||
"description": "Check if there are updates available for the Music Assistant Server and/or integration."
|
"description": "Check if there are updates available for the Music Assistant Server and/or integration."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"services": {
|
||||||
|
"play_media": {
|
||||||
|
"name": "Play media",
|
||||||
|
"description": "Play media on a Music Assistant player with more fine-grained control options.",
|
||||||
|
"fields": {
|
||||||
|
"media_id": {
|
||||||
|
"name": "Media ID(s)",
|
||||||
|
"description": "URI or name of the item you want to play. Specify a list if you want to play/enqueue multiple items."
|
||||||
|
},
|
||||||
|
"media_type": {
|
||||||
|
"name": "Media type",
|
||||||
|
"description": "The type of the content to play. Such as artist, album, track or playlist. Will be auto-determined if omitted."
|
||||||
|
},
|
||||||
|
"enqueue": {
|
||||||
|
"name": "Enqueue",
|
||||||
|
"description": "If the content should be played now or added to the queue."
|
||||||
|
},
|
||||||
|
"artist": {
|
||||||
|
"name": "Artist name",
|
||||||
|
"description": "When specifying a track or album by name in the Media ID field, you can optionally restrict results by this artist name."
|
||||||
|
},
|
||||||
|
"album": {
|
||||||
|
"name": "Album name",
|
||||||
|
"description": "When specifying a track by name in the Media ID field, you can optionally restrict results by this album name."
|
||||||
|
},
|
||||||
|
"radio_mode": {
|
||||||
|
"name": "Enable radio mode",
|
||||||
|
"description": "Enable radio mode to auto-generate a playlist based on the selection."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"play_announcement": {
|
||||||
|
"name": "Play announcement",
|
||||||
|
"description": "Play announcement on a Music Assistant player with more fine-grained control options.",
|
||||||
|
"fields": {
|
||||||
|
"url": {
|
||||||
|
"name": "URL",
|
||||||
|
"description": "URL to the notification sound."
|
||||||
|
},
|
||||||
|
"use_pre_announce": {
|
||||||
|
"name": "Use pre-announce",
|
||||||
|
"description": "Use pre-announcement sound for the announcement. Omit to use the player default."
|
||||||
|
},
|
||||||
|
"announce_volume": {
|
||||||
|
"name": "Announce volume",
|
||||||
|
"description": "Use a forced volume level for the announcement. Omit to use player default."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"transfer_queue": {
|
||||||
|
"name": "Transfer queue",
|
||||||
|
"description": "Transfer the player's queue to another player.",
|
||||||
|
"fields": {
|
||||||
|
"source_player": {
|
||||||
|
"name": "Source media player",
|
||||||
|
"description": "The source media player which has the queue you want to transfer. When omitted, the first playing player will be used."
|
||||||
|
},
|
||||||
|
"auto_play": {
|
||||||
|
"name": "Auto play",
|
||||||
|
"description": "Start playing the queue on the target player. Omit to use the default behavior."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"selector": {
|
"selector": {
|
||||||
"enqueue": {
|
"enqueue": {
|
||||||
"options": {
|
"options": {
|
||||||
@ -46,6 +110,15 @@
|
|||||||
"replace": "Play now and clear queue",
|
"replace": "Play now and clear queue",
|
||||||
"replace_next": "Play next and clear queue"
|
"replace_next": "Play next and clear queue"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"media_type": {
|
||||||
|
"options": {
|
||||||
|
"artist": "Artist",
|
||||||
|
"album": "Album",
|
||||||
|
"track": "Track",
|
||||||
|
"playlist": "Playlist",
|
||||||
|
"radio": "Radio"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,13 @@
|
|||||||
|
|
||||||
from unittest.mock import MagicMock, call
|
from unittest.mock import MagicMock, call
|
||||||
|
|
||||||
|
from music_assistant_models.enums import MediaType, QueueOption
|
||||||
|
from music_assistant_models.media_items import Track
|
||||||
|
import pytest
|
||||||
from syrupy import SnapshotAssertion
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
|
ATTR_MEDIA_ENQUEUE,
|
||||||
ATTR_MEDIA_REPEAT,
|
ATTR_MEDIA_REPEAT,
|
||||||
ATTR_MEDIA_SEEK_POSITION,
|
ATTR_MEDIA_SEEK_POSITION,
|
||||||
ATTR_MEDIA_SHUFFLE,
|
ATTR_MEDIA_SHUFFLE,
|
||||||
@ -13,6 +17,23 @@ from homeassistant.components.media_player import (
|
|||||||
DOMAIN as MEDIA_PLAYER_DOMAIN,
|
DOMAIN as MEDIA_PLAYER_DOMAIN,
|
||||||
SERVICE_CLEAR_PLAYLIST,
|
SERVICE_CLEAR_PLAYLIST,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.music_assistant.const import DOMAIN as MASS_DOMAIN
|
||||||
|
from homeassistant.components.music_assistant.media_player import (
|
||||||
|
ATTR_ALBUM,
|
||||||
|
ATTR_ANNOUNCE_VOLUME,
|
||||||
|
ATTR_ARTIST,
|
||||||
|
ATTR_AUTO_PLAY,
|
||||||
|
ATTR_MEDIA_ID,
|
||||||
|
ATTR_MEDIA_TYPE,
|
||||||
|
ATTR_RADIO_MODE,
|
||||||
|
ATTR_SOURCE_PLAYER,
|
||||||
|
ATTR_URL,
|
||||||
|
ATTR_USE_PRE_ANNOUNCE,
|
||||||
|
SERVICE_PLAY_ANNOUNCEMENT,
|
||||||
|
SERVICE_PLAY_MEDIA_ADVANCED,
|
||||||
|
SERVICE_TRANSFER_QUEUE,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import HomeAssistantError
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
SERVICE_MEDIA_NEXT_TRACK,
|
SERVICE_MEDIA_NEXT_TRACK,
|
||||||
@ -35,6 +56,15 @@ from homeassistant.helpers import entity_registry as er
|
|||||||
|
|
||||||
from .common import setup_integration_from_fixtures, snapshot_music_assistant_entities
|
from .common import setup_integration_from_fixtures, snapshot_music_assistant_entities
|
||||||
|
|
||||||
|
from tests.common import AsyncMock
|
||||||
|
|
||||||
|
MOCK_TRACK = Track(
|
||||||
|
item_id="1",
|
||||||
|
provider="library",
|
||||||
|
name="Test Track",
|
||||||
|
provider_mappings={},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_media_player(
|
async def test_media_player(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -110,11 +140,11 @@ async def test_media_player_seek_action(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_media_player_volume_action(
|
async def test_media_player_volume_set_action(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
music_assistant_client: MagicMock,
|
music_assistant_client: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test media_player entity volume action."""
|
"""Test media_player entity volume_set action."""
|
||||||
await setup_integration_from_fixtures(hass, music_assistant_client)
|
await setup_integration_from_fixtures(hass, music_assistant_client)
|
||||||
entity_id = "media_player.test_player_1"
|
entity_id = "media_player.test_player_1"
|
||||||
mass_player_id = "00:00:00:00:00:01"
|
mass_player_id = "00:00:00:00:00:01"
|
||||||
@ -261,3 +291,227 @@ async def test_media_player_clear_playlist_action(
|
|||||||
assert music_assistant_client.send_command.call_args == call(
|
assert music_assistant_client.send_command.call_args == call(
|
||||||
"player_queues/clear", queue_id=mass_player_id
|
"player_queues/clear", queue_id=mass_player_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_media_player_play_media_action(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
music_assistant_client: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test media_player (advanced) play_media action."""
|
||||||
|
await setup_integration_from_fixtures(hass, music_assistant_client)
|
||||||
|
entity_id = "media_player.test_player_1"
|
||||||
|
mass_player_id = "00:00:00:00:00:01"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
|
||||||
|
# test simple play_media call with URI as media_id and no media type
|
||||||
|
await hass.services.async_call(
|
||||||
|
MASS_DOMAIN,
|
||||||
|
SERVICE_PLAY_MEDIA_ADVANCED,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
ATTR_MEDIA_ID: "spotify://track/1234",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert music_assistant_client.send_command.call_count == 1
|
||||||
|
assert music_assistant_client.send_command.call_args == call(
|
||||||
|
"player_queues/play_media",
|
||||||
|
queue_id=mass_player_id,
|
||||||
|
media=["spotify://track/1234"],
|
||||||
|
option=None,
|
||||||
|
radio_mode=False,
|
||||||
|
start_item=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# test simple play_media call with URI and enqueue specified
|
||||||
|
music_assistant_client.send_command.reset_mock()
|
||||||
|
await hass.services.async_call(
|
||||||
|
MASS_DOMAIN,
|
||||||
|
SERVICE_PLAY_MEDIA_ADVANCED,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
ATTR_MEDIA_ID: "spotify://track/1234",
|
||||||
|
ATTR_MEDIA_ENQUEUE: "add",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert music_assistant_client.send_command.call_count == 1
|
||||||
|
assert music_assistant_client.send_command.call_args == call(
|
||||||
|
"player_queues/play_media",
|
||||||
|
queue_id=mass_player_id,
|
||||||
|
media=["spotify://track/1234"],
|
||||||
|
option=QueueOption.ADD,
|
||||||
|
radio_mode=False,
|
||||||
|
start_item=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# test basic play_media call with URL and radio mode specified
|
||||||
|
music_assistant_client.send_command.reset_mock()
|
||||||
|
await hass.services.async_call(
|
||||||
|
MASS_DOMAIN,
|
||||||
|
SERVICE_PLAY_MEDIA_ADVANCED,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
ATTR_MEDIA_ID: "spotify://track/1234",
|
||||||
|
ATTR_RADIO_MODE: True,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert music_assistant_client.send_command.call_count == 1
|
||||||
|
assert music_assistant_client.send_command.call_args == call(
|
||||||
|
"player_queues/play_media",
|
||||||
|
queue_id=mass_player_id,
|
||||||
|
media=["spotify://track/1234"],
|
||||||
|
option=None,
|
||||||
|
radio_mode=True,
|
||||||
|
start_item=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# test play_media call with media id and media type specified
|
||||||
|
music_assistant_client.send_command.reset_mock()
|
||||||
|
music_assistant_client.music.get_item = AsyncMock(return_value=MOCK_TRACK)
|
||||||
|
await hass.services.async_call(
|
||||||
|
MASS_DOMAIN,
|
||||||
|
SERVICE_PLAY_MEDIA_ADVANCED,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
ATTR_MEDIA_ID: "1",
|
||||||
|
ATTR_MEDIA_TYPE: "track",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert music_assistant_client.music.get_item.call_count == 1
|
||||||
|
assert music_assistant_client.music.get_item.call_args == call(
|
||||||
|
MediaType.TRACK, "1", "library"
|
||||||
|
)
|
||||||
|
assert music_assistant_client.send_command.call_count == 1
|
||||||
|
assert music_assistant_client.send_command.call_args == call(
|
||||||
|
"player_queues/play_media",
|
||||||
|
queue_id=mass_player_id,
|
||||||
|
media=[MOCK_TRACK.uri],
|
||||||
|
option=None,
|
||||||
|
radio_mode=False,
|
||||||
|
start_item=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# test play_media call by name
|
||||||
|
music_assistant_client.send_command.reset_mock()
|
||||||
|
music_assistant_client.music.get_item_by_name = AsyncMock(return_value=MOCK_TRACK)
|
||||||
|
await hass.services.async_call(
|
||||||
|
MASS_DOMAIN,
|
||||||
|
SERVICE_PLAY_MEDIA_ADVANCED,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
ATTR_MEDIA_ID: "test",
|
||||||
|
ATTR_ARTIST: "artist",
|
||||||
|
ATTR_ALBUM: "album",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert music_assistant_client.music.get_item_by_name.call_count == 1
|
||||||
|
assert music_assistant_client.music.get_item_by_name.call_args == call(
|
||||||
|
name="test",
|
||||||
|
artist="artist",
|
||||||
|
album="album",
|
||||||
|
media_type=None,
|
||||||
|
)
|
||||||
|
assert music_assistant_client.send_command.call_count == 1
|
||||||
|
assert music_assistant_client.send_command.call_args == call(
|
||||||
|
"player_queues/play_media",
|
||||||
|
queue_id=mass_player_id,
|
||||||
|
media=[MOCK_TRACK.uri],
|
||||||
|
option=None,
|
||||||
|
radio_mode=False,
|
||||||
|
start_item=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_media_player_play_announcement_action(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
music_assistant_client: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test media_player play_announcement action."""
|
||||||
|
await setup_integration_from_fixtures(hass, music_assistant_client)
|
||||||
|
entity_id = "media_player.test_player_1"
|
||||||
|
mass_player_id = "00:00:00:00:00:01"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
await hass.services.async_call(
|
||||||
|
MASS_DOMAIN,
|
||||||
|
SERVICE_PLAY_ANNOUNCEMENT,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
ATTR_URL: "http://blah.com/announcement.mp3",
|
||||||
|
ATTR_USE_PRE_ANNOUNCE: True,
|
||||||
|
ATTR_ANNOUNCE_VOLUME: 50,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert music_assistant_client.send_command.call_count == 1
|
||||||
|
assert music_assistant_client.send_command.call_args == call(
|
||||||
|
"players/cmd/play_announcement",
|
||||||
|
player_id=mass_player_id,
|
||||||
|
url="http://blah.com/announcement.mp3",
|
||||||
|
use_pre_announce=True,
|
||||||
|
volume_level=50,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_media_player_transfer_queue_action(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
music_assistant_client: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test media_player transfer_queu action."""
|
||||||
|
await setup_integration_from_fixtures(hass, music_assistant_client)
|
||||||
|
entity_id = "media_player.test_player_1"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
await hass.services.async_call(
|
||||||
|
MASS_DOMAIN,
|
||||||
|
SERVICE_TRANSFER_QUEUE,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
ATTR_SOURCE_PLAYER: "media_player.my_super_test_player_2",
|
||||||
|
ATTR_AUTO_PLAY: True,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert music_assistant_client.send_command.call_count == 1
|
||||||
|
assert music_assistant_client.send_command.call_args == call(
|
||||||
|
"player_queues/transfer",
|
||||||
|
source_queue_id="00:00:00:00:00:02",
|
||||||
|
target_queue_id="00:00:00:00:00:01",
|
||||||
|
auto_play=True,
|
||||||
|
require_schema=25,
|
||||||
|
)
|
||||||
|
# test again with invalid source player
|
||||||
|
music_assistant_client.send_command.reset_mock()
|
||||||
|
with pytest.raises(HomeAssistantError, match="Source player not available."):
|
||||||
|
await hass.services.async_call(
|
||||||
|
MASS_DOMAIN,
|
||||||
|
SERVICE_TRANSFER_QUEUE,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
ATTR_SOURCE_PLAYER: "media_player.blah_blah",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
# test again with no source player specified (which picks first playing playerqueue)
|
||||||
|
music_assistant_client.send_command.reset_mock()
|
||||||
|
await hass.services.async_call(
|
||||||
|
MASS_DOMAIN,
|
||||||
|
SERVICE_TRANSFER_QUEUE,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: entity_id,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert music_assistant_client.send_command.call_count == 1
|
||||||
|
assert music_assistant_client.send_command.call_args == call(
|
||||||
|
"player_queues/transfer",
|
||||||
|
source_queue_id="test_group_player_1",
|
||||||
|
target_queue_id="00:00:00:00:00:01",
|
||||||
|
auto_play=None,
|
||||||
|
require_schema=25,
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user