diff --git a/homeassistant/components/music_assistant/__init__.py b/homeassistant/components/music_assistant/__init__.py index 68a689d3d9a0c1..1077175ab7a248 100644 --- a/homeassistant/components/music_assistant/__init__.py +++ b/homeassistant/components/music_assistant/__init__.py @@ -27,7 +27,8 @@ ) from .const import ATTR_CONF_EXPOSE_PLAYER_TO_HA, DOMAIN, LOGGER -from .services import get_music_assistant_client, register_actions +from .helpers import get_music_assistant_client +from .services import register_actions if TYPE_CHECKING: from music_assistant_models.event import MassEvent diff --git a/homeassistant/components/music_assistant/helpers.py b/homeassistant/components/music_assistant/helpers.py index b228e99f76f3c2..2f8512dc7c65c0 100644 --- a/homeassistant/components/music_assistant/helpers.py +++ b/homeassistant/components/music_assistant/helpers.py @@ -4,11 +4,18 @@ from collections.abc import Callable, Coroutine import functools -from typing import Any +from typing import TYPE_CHECKING, Any from music_assistant_models.errors import MusicAssistantError -from homeassistant.exceptions import HomeAssistantError +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError + +if TYPE_CHECKING: + from music_assistant_client import MusicAssistantClient + + from . import MusicAssistantConfigEntry def catch_musicassistant_error[**_P, _R]( @@ -26,3 +33,16 @@ async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: raise HomeAssistantError(error_msg) from err return wrapper + + +@callback +def get_music_assistant_client( + hass: HomeAssistant, config_entry_id: str +) -> MusicAssistantClient: + """Get the Music Assistant client for the given config entry.""" + entry: MusicAssistantConfigEntry | None + if not (entry := hass.config_entries.async_get_entry(config_entry_id)): + raise ServiceValidationError("Entry not found") + if entry.state is not ConfigEntryState.LOADED: + raise ServiceValidationError("Entry not loaded") + return entry.runtime_data.mass diff --git a/homeassistant/components/music_assistant/media_player.py b/homeassistant/components/music_assistant/media_player.py index 0eaea6b4e91121..b8faac270eca37 100644 --- a/homeassistant/components/music_assistant/media_player.py +++ b/homeassistant/components/music_assistant/media_player.py @@ -22,11 +22,9 @@ from music_assistant_models.event import MassEvent from music_assistant_models.media_items import ItemMapping, MediaItemType, Track from music_assistant_models.player_queue import PlayerQueue -import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( - ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_EXTRA, BrowseMedia, MediaPlayerDeviceClass, @@ -41,38 +39,26 @@ async_process_play_media_url, ) from homeassistant.const import ATTR_NAME, STATE_OFF, Platform -from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse +from homeassistant.core import HomeAssistant, ServiceResponse from homeassistant.exceptions import HomeAssistantError, ServiceValidationError -from homeassistant.helpers import config_validation as cv, entity_registry as er -from homeassistant.helpers.entity_platform import ( - AddConfigEntryEntitiesCallback, - async_get_current_platform, -) +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.dt import utc_from_timestamp from . import MusicAssistantConfigEntry from .const import ( ATTR_ACTIVE, ATTR_ACTIVE_QUEUE, - ATTR_ALBUM, - ATTR_ANNOUNCE_VOLUME, - ATTR_ARTIST, - ATTR_AUTO_PLAY, ATTR_CURRENT_INDEX, ATTR_CURRENT_ITEM, ATTR_ELAPSED_TIME, ATTR_ITEMS, ATTR_MASS_PLAYER_TYPE, - ATTR_MEDIA_ID, - ATTR_MEDIA_TYPE, ATTR_NEXT_ITEM, ATTR_QUEUE_ID, ATTR_RADIO_MODE, ATTR_REPEAT_MODE, ATTR_SHUFFLE_ENABLED, - ATTR_SOURCE_PLAYER, - ATTR_URL, - ATTR_USE_PRE_ANNOUNCE, DOMAIN, ) from .entity import MusicAssistantEntity @@ -122,11 +108,6 @@ # UNKNOWN is intentionally not mapped - will return None } -SERVICE_PLAY_MEDIA_ADVANCED = "play_media" -SERVICE_PLAY_ANNOUNCEMENT = "play_announcement" -SERVICE_TRANSFER_QUEUE = "transfer_queue" -SERVICE_GET_QUEUE = "get_queue" - async def async_setup_entry( hass: HomeAssistant, @@ -143,44 +124,6 @@ def add_player(player_id: str) -> None: # register callback to add players when they are discovered entry.runtime_data.platform_handlers.setdefault(Platform.MEDIA_PLAYER, add_player) - # 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", - ) - platform.async_register_entity_service( - SERVICE_GET_QUEUE, - schema=None, - func="_async_handle_get_queue", - supports_response=SupportsResponse.ONLY, - ) - class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity): """Representation of MediaPlayerEntity from Music Assistant Player.""" diff --git a/homeassistant/components/music_assistant/services.py b/homeassistant/components/music_assistant/services.py index a0e82ba3315215..25ccfd3c0f9e78 100644 --- a/homeassistant/components/music_assistant/services.py +++ b/homeassistant/components/music_assistant/services.py @@ -4,10 +4,13 @@ from typing import TYPE_CHECKING -from music_assistant_models.enums import MediaType +from music_assistant_models.enums import MediaType, QueueOption import voluptuous as vol -from homeassistant.config_entries import ConfigEntryState +from homeassistant.components.media_player import ( + ATTR_MEDIA_ENQUEUE, + DOMAIN as MEDIA_PLAYER_DOMAIN, +) from homeassistant.const import ATTR_CONFIG_ENTRY_ID from homeassistant.core import ( HomeAssistant, @@ -17,31 +20,41 @@ callback, ) from homeassistant.exceptions import ServiceValidationError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, service from .const import ( + ATTR_ALBUM, ATTR_ALBUM_ARTISTS_ONLY, ATTR_ALBUM_TYPE, ATTR_ALBUMS, + ATTR_ANNOUNCE_VOLUME, + ATTR_ARTIST, ATTR_ARTISTS, ATTR_AUDIOBOOKS, + ATTR_AUTO_PLAY, ATTR_FAVORITE, ATTR_ITEMS, ATTR_LIBRARY_ONLY, ATTR_LIMIT, + ATTR_MEDIA_ID, ATTR_MEDIA_TYPE, ATTR_OFFSET, ATTR_ORDER_BY, ATTR_PLAYLISTS, ATTR_PODCASTS, ATTR_RADIO, + ATTR_RADIO_MODE, ATTR_SEARCH, ATTR_SEARCH_ALBUM, ATTR_SEARCH_ARTIST, ATTR_SEARCH_NAME, + ATTR_SOURCE_PLAYER, ATTR_TRACKS, + ATTR_URL, + ATTR_USE_PRE_ANNOUNCE, DOMAIN, ) +from .helpers import get_music_assistant_client from .schemas import ( LIBRARY_RESULTS_SCHEMA, SEARCH_RESULT_SCHEMA, @@ -49,7 +62,6 @@ ) if TYPE_CHECKING: - from music_assistant_client import MusicAssistantClient from music_assistant_models.media_items import ( Album, Artist, @@ -60,28 +72,18 @@ Track, ) - from . import MusicAssistantConfigEntry - SERVICE_SEARCH = "search" SERVICE_GET_LIBRARY = "get_library" +SERVICE_PLAY_MEDIA_ADVANCED = "play_media" +SERVICE_PLAY_ANNOUNCEMENT = "play_announcement" +SERVICE_TRANSFER_QUEUE = "transfer_queue" +SERVICE_GET_QUEUE = "get_queue" + DEFAULT_OFFSET = 0 DEFAULT_LIMIT = 25 DEFAULT_SORT_ORDER = "name" -@callback -def get_music_assistant_client( - hass: HomeAssistant, config_entry_id: str -) -> MusicAssistantClient: - """Get the Music Assistant client for the given config entry.""" - entry: MusicAssistantConfigEntry | None - if not (entry := hass.config_entries.async_get_entry(config_entry_id)): - raise ServiceValidationError("Entry not found") - if entry.state is not ConfigEntryState.LOADED: - raise ServiceValidationError("Entry not loaded") - return entry.runtime_data.mass - - @callback def register_actions(hass: HomeAssistant) -> None: """Register custom actions.""" @@ -124,6 +126,55 @@ def register_actions(hass: HomeAssistant) -> None: supports_response=SupportsResponse.ONLY, ) + # Platform entity services + service.async_register_platform_entity_service( + hass, + DOMAIN, + SERVICE_PLAY_MEDIA_ADVANCED, + entity_domain=MEDIA_PLAYER_DOMAIN, + schema={ + 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), + }, + func="_async_handle_play_media", + ) + service.async_register_platform_entity_service( + hass, + DOMAIN, + SERVICE_PLAY_ANNOUNCEMENT, + entity_domain=MEDIA_PLAYER_DOMAIN, + schema={ + vol.Required(ATTR_URL): cv.string, + vol.Optional(ATTR_USE_PRE_ANNOUNCE): vol.Coerce(bool), + vol.Optional(ATTR_ANNOUNCE_VOLUME): vol.Coerce(int), + }, + func="_async_handle_play_announcement", + ) + service.async_register_platform_entity_service( + hass, + DOMAIN, + SERVICE_TRANSFER_QUEUE, + entity_domain=MEDIA_PLAYER_DOMAIN, + schema={ + vol.Optional(ATTR_SOURCE_PLAYER): cv.entity_id, + vol.Optional(ATTR_AUTO_PLAY): vol.Coerce(bool), + }, + func="_async_handle_transfer_queue", + ) + service.async_register_platform_entity_service( + hass, + DOMAIN, + SERVICE_GET_QUEUE, + entity_domain=MEDIA_PLAYER_DOMAIN, + schema=None, + func="_async_handle_get_queue", + supports_response=SupportsResponse.ONLY, + ) + async def handle_search(call: ServiceCall) -> ServiceResponse: """Handle queue_command action.""" diff --git a/tests/components/music_assistant/test_media_player.py b/tests/components/music_assistant/test_media_player.py index 7c896a4f3e7340..1ec669fbe86d53 100644 --- a/tests/components/music_assistant/test_media_player.py +++ b/tests/components/music_assistant/test_media_player.py @@ -30,8 +30,7 @@ SERVICE_UNJOIN, MediaPlayerEntityFeature, ) -from homeassistant.components.music_assistant.const import DOMAIN -from homeassistant.components.music_assistant.media_player import ( +from homeassistant.components.music_assistant.const import ( ATTR_ALBUM, ATTR_ANNOUNCE_VOLUME, ATTR_ARTIST, @@ -42,6 +41,9 @@ ATTR_SOURCE_PLAYER, ATTR_URL, ATTR_USE_PRE_ANNOUNCE, + DOMAIN, +) +from homeassistant.components.music_assistant.services import ( SERVICE_GET_QUEUE, SERVICE_PLAY_ANNOUNCEMENT, SERVICE_PLAY_MEDIA_ADVANCED,