Skip to content

Commit

Permalink
Fix mass service
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelveldt committed Jul 31, 2022
1 parent 5e5dd1a commit 440abf6
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 56 deletions.
150 changes: 109 additions & 41 deletions custom_components/mass/services.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
"""Custom services for the Music Assistant integration."""

from typing import Optional

import voluptuous as vol
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import ServiceCall
from music_assistant import MusicAssistant
from music_assistant.models.enums import QueueOption, RepeatMode
from music_assistant.models.enums import MediaType, QueueOption, RepeatMode
from music_assistant.models.errors import MusicAssistantError
from music_assistant.models.media_items import MediaItemType

from .const import DOMAIN

Expand All @@ -20,28 +24,58 @@
CMD_REPEAT = "repeat"
CMD_SNAPSHOT_CREATE = "snapshot_create"
CMD_SNAPSHOT_RESTORE = "snapshot_restore"
CMD_PLAY_ALERT = "play_alert"

CMD_ARGS_MAP = {
"repeat_mode_one": RepeatMode.ONE,
"repeat_mode_all": RepeatMode.ALL,
"repeat_mode_off": RepeatMode.OFF,
"shuffle_mode_on": True,
"shuffle_mode_off": False,
"play_media_play_now": QueueOption.PLAY,
"play_media_play_next": QueueOption.NEXT,
"play_media_play_add": QueueOption.ADD,
"play_media_play_replace": QueueOption.REPLACE,
CMD_PLAY_ANNOUNCEMENT = "play_announcement"


CMD_MODES_MAP = {
CMD_REPEAT: {
"repeat_mode_one": RepeatMode.ONE,
"repeat_mode_all": RepeatMode.ALL,
"repeat_mode_off": RepeatMode.OFF,
},
CMD_SHUFFLE: {
"shuffle_mode_on": True,
"shuffle_mode_off": False,
},
CMD_PLAY_MEDIA: {
"play_media_now": QueueOption.PLAY,
"play_media_next": QueueOption.NEXT,
"play_media_add": QueueOption.ADD,
"play_media_replace": QueueOption.REPLACE,
"play_media_radio": QueueOption.RADIO,
},
}
ALL_MODES = (subkey for values in CMD_MODES_MAP.values() for subkey in values)
URI_REQUIRED = (CMD_PLAY_MEDIA, CMD_PLAY_ANNOUNCEMENT)


def validate_command_data(data: dict) -> dict:
"""Validate command args/mode."""
cmd = data["command"]
if cmd in CMD_MODES_MAP:
mode = data.get("mode", "")
valid_modes = CMD_MODES_MAP[cmd].keys()
valid_modes_str = ",".join(valid_modes)
if mode not in valid_modes:
raise vol.Invalid(
f"Invalid mode for {cmd} - must be any of {valid_modes_str}"
)
if cmd in URI_REQUIRED:
if not data.get("uri"):
raise vol.Invalid("No URI specified")
return data


QueueCommandServiceSchema = vol.Schema(
{
"entity_id": cv.entity_ids,
"command": str,
"uri": vol.Optional(str),
"mode": vol.Any(*CMD_ARGS_MAP.keys()),
}
vol.All(
{
"entity_id": cv.entity_ids,
"command": str,
"uri": vol.Union(str, [str], None),
"mode": vol.Any(*ALL_MODES),
},
validate_command_data,
)
)


Expand All @@ -52,6 +86,7 @@ def register_services(hass: HomeAssistant, mass: MusicAssistant):
async def handle_queue_command(call: ServiceCall) -> None:
"""Handle queue_command service."""
data = call.data
cmd = data["command"]
if isinstance(data["entity_id"], list):
entity_ids = data["entity_id"]
else:
Expand All @@ -61,37 +96,70 @@ async def handle_queue_command(call: ServiceCall) -> None:
entity_id = entity.attributes.get("source_entity_id", entity_id)
player = mass.players.get_player(entity_id)
queue = player.active_queue
if data["command"] == CMD_PLAY:
if cmd == CMD_PLAY:
await queue.play()
elif data["command"] == CMD_PAUSE:
elif cmd == CMD_PAUSE:
await queue.pause()
elif data["command"] == CMD_NEXT:
elif cmd == CMD_NEXT:
await queue.next()
elif data["command"] == CMD_PREVIOUS:
elif cmd == CMD_PREVIOUS:
await queue.previous()
elif data["command"] == CMD_STOP:
elif cmd == CMD_STOP:
await queue.stop()
elif data["command"] == CMD_CLEAR:
elif cmd == CMD_CLEAR:
await queue.clear()
elif data["command"] == CMD_PLAY_MEDIA:
await queue.play_media(
data["uri"], CMD_ARGS_MAP[data.get("mode", "play_media_play_now")]
)
elif data["command"] == CMD_SHUFFLE:
queue.settings.shuffle_enabled = CMD_ARGS_MAP[
data.get("mode", "shuffle_mode_on")
]
elif data["command"] == CMD_REPEAT:
queue.settings.repeat_mode = CMD_ARGS_MAP[
data.get("mode", "repeat_mode_all")
]
elif data["command"] == CMD_SNAPSHOT_CREATE:
elif cmd == CMD_PLAY_MEDIA:
media_items = []
uris = data["uri"] if isinstance(data["uri"], list) else [data["uri"]]
for uri in uris:
try:
media_items.append(await mass.music.get_item_by_uri(uri))
except MusicAssistantError as err:
# try again with item by name
if item := await get_item_by_name(mass, uri):
media_items.append(item)
else:
raise vol.Invalid(f"Invalid uri: {uri}") from err
await queue.play_media(media_items, CMD_MODES_MAP[cmd][data["mode"]])
elif cmd == CMD_SHUFFLE:
queue.settings.shuffle_enabled = CMD_MODES_MAP[cmd][data["mode"]]
elif cmd == CMD_REPEAT:
queue.settings.repeat_mode = CMD_MODES_MAP[cmd][data["mode"]]
elif cmd == CMD_SNAPSHOT_CREATE:
await queue.snapshot_create()
elif data["command"] == CMD_SNAPSHOT_RESTORE:
elif cmd == CMD_SNAPSHOT_RESTORE:
await queue.snapshot_restore()
elif data["command"] == CMD_PLAY_ALERT:
await queue.play_alert(data["uri"])
elif cmd == CMD_PLAY_ANNOUNCEMENT:
await queue.play_announcement(data["uri"])

hass.services.async_register(
DOMAIN, "queue_command", handle_queue_command, schema=QueueCommandServiceSchema
)


async def get_item_by_name(
mass: MusicAssistant, name: str, media_type: Optional[MediaType] = None
) -> MediaItemType | None:
"""Try to find a media item (such as a playlist) by name."""
# iterate media controllers one by one,
# start with playlists and radio as those are the most common one
for mtype in (
MediaType.PLAYLIST,
MediaType.RADIO,
MediaType.ALBUM,
MediaType.TRACK,
MediaType.ARTIST,
):
if media_type is not None and mtype != media_type:
continue
ctrl = mass.music.get_controller(mtype)
async for item in ctrl.iter_db_items(search=name):
if name.lower() == item.name.lower():
return item
# last resort: global search - pick the top most item
for item in await mass.music.search(name):
if media_type is not None and item.media_type != media_type:
continue
if name.lower() == item.name.lower():
return item
return None
31 changes: 16 additions & 15 deletions custom_components/mass/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

queue_command:
name: Queue command
description: Send a command directly to a Music Assistant player(queue). You may provide the origin media_player as well as the (optional) media_player entity created by MA itself.
description: "Send a command directly to a Music Assistant player(queue), bypassing the need for a Music Assistant generated player. You may provide the origin media_player as well as the (optional) media_player entity created by MA itself. Note: Only use this special service for advanced purposes. Usually you can just the builtin HA services."
target:
entity:
domain: media_player
Expand All @@ -17,18 +17,18 @@ queue_command:
selector:
select:
options:
- "play"
- "pause"
- "next"
- "previous"
- "stop"
- "clear"
- "play_media"
- "shuffle"
- "repeat"
- play
- pause
- next
- previous
- stop
- clear
- play_media
- shuffle
- repeat
- snapshot_create
- snapshot_restore
- play_alert
- play_announcement

uri:
name: Media item to play
Expand All @@ -52,7 +52,8 @@ queue_command:
- "repeat_mode_off"
- "shuffle_mode_on"
- "shuffle_mode_off"
- "play_media_play_now"
- "play_media_play_next"
- "play_media_play_add"
- "play_media_play_replace"
- "play_media_now"
- "play_media_next"
- "play_media_add"
- "play_media_replace"
- "play_media_radio"

0 comments on commit 440abf6

Please sign in to comment.