From 665c740056fc61af069daa2c1ce9806fd21027e5 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 14 Sep 2024 15:58:58 +0200 Subject: [PATCH] Bump Music Assistant client to 2.2.4 + add new transfer queue service (#2918) --- custom_components/mass/manifest.json | 21 ++------ custom_components/mass/media_player.py | 57 ++++++++++++++++++--- custom_components/mass/services.py | 3 ++ custom_components/mass/services.yaml | 24 +++++++++ custom_components/mass/strings.json | 18 +++++++ custom_components/mass/translations/en.json | 18 +++++++ 6 files changed, 117 insertions(+), 24 deletions(-) diff --git a/custom_components/mass/manifest.json b/custom_components/mass/manifest.json index bc797a60..941ae513 100644 --- a/custom_components/mass/manifest.json +++ b/custom_components/mass/manifest.json @@ -1,25 +1,14 @@ { "domain": "mass", "name": "Music Assistant", - "after_dependencies": [ - "media_source", - "media_player" - ], - "codeowners": [ - "@music-assistant" - ], + "after_dependencies": ["media_source", "media_player"], + "codeowners": ["@music-assistant"], "config_flow": true, "documentation": "https://music-assistant.io", "iot_class": "local_push", "issue_tracker": "https://github.com/music-assistant/hass-music-assistant/issues", - "loggers": [ - "music_assistant" - ], - "requirements": [ - "music-assistant==2.1.3" - ], + "loggers": ["music_assistant"], + "requirements": ["music-assistant==2.2.4"], "version": "0.0.0", - "zeroconf": [ - "_mass._tcp.local." - ] + "zeroconf": ["_mass._tcp.local."] } diff --git a/custom_components/mass/media_player.py b/custom_components/mass/media_player.py index 1a3f5128..b38c9698 100644 --- a/custom_components/mass/media_player.py +++ b/custom_components/mass/media_player.py @@ -62,7 +62,6 @@ ATTR_ACTIVE_GROUP, ATTR_ACTIVE_QUEUE, ATTR_GROUP_LEADER, - ATTR_GROUP_MEMBERS, ATTR_MASS_PLAYER_ID, ATTR_MASS_PLAYER_TYPE, ATTR_QUEUE_INDEX, @@ -117,6 +116,7 @@ SERVICE_PLAY_MEDIA_ADVANCED = "play_media" SERVICE_PLAY_ANNOUNCEMEMT = "play_announcement" +SERVICE_TRANSFER_QUEUE = "transfer_queue" ATTR_RADIO_MODE = "radio_mode" ATTR_MEDIA_ID = "media_id" ATTR_MEDIA_TYPE = "media_type" @@ -125,6 +125,8 @@ ATTR_URL = "url" ATTR_USE_PRE_ANNOUNCE = "use_pre_announce" ATTR_ANNOUNCE_VOLUME = "announce_volume" +ATTR_SOURCE_PLAYER = "source_player" +ATTR_AUTO_PLAY = "auto_play" # pylint: disable=too-many-public-methods @@ -204,6 +206,14 @@ async def handle_player_added(event: MassEvent) -> None: }, "_async_play_announcement", ) + platform.async_register_entity_service( + SERVICE_TRANSFER_QUEUE, + { + vol.Required(ATTR_SOURCE_PLAYER): cv.entity_id, + vol.Optional(ATTR_AUTO_PLAY): vol.Coerce(bool), + }, + "_async_transfer_queue", + ) class MassPlayer(MassBaseEntity, MediaPlayerEntity): @@ -247,7 +257,7 @@ async def async_added_to_hass(self) -> None: async def queue_time_updated(event: MassEvent) -> None: if event.object_id != self.player.active_source: return - if abs(self._prev_time - event.data) > 5: + if abs((self._prev_time or 0) - event.data) > 5: await self.async_on_update() self.async_write_ha_state() self._prev_time = event.data @@ -267,7 +277,6 @@ def extra_state_attributes(self) -> Mapping[str, Any]: attrs = { ATTR_MASS_PLAYER_ID: self.player_id, ATTR_MASS_PLAYER_TYPE: player.type.value, - ATTR_GROUP_MEMBERS: player.group_childs, ATTR_GROUP_LEADER: player.synced_to, ATTR_ACTIVE_QUEUE: player.active_source, ATTR_ACTIVE_GROUP: player.active_group, @@ -304,11 +313,26 @@ async def async_on_update(self) -> None: queue = self.mass.player_queues.get(player.active_source) # update generic attributes if player.powered: - self._attr_state = STATE_MAPPING[self.player.state] + self._attr_state = STATE_MAPPING.get(self.player.state) else: self._attr_state = STATE_OFF - self._attr_group_members = player.group_childs - self._attr_volume_level = player.volume_level / 100 + + # translate MA group_childs to HA group_members as entity id's + # TODO: find a way to optimize this a tiny bit more + # e.g. by holding a lookup dict in memory on integration level + group_members_entity_ids: list[str] = [] + if player.group_childs: + for state in self.hass.states.async_all("media_player"): + if not (mass_player_id := state.attributes.get("mass_player_id")): + continue + if mass_player_id not in player.group_childs: + continue + group_members_entity_ids.append(state.entity_id) + self._attr_group_members = group_members_entity_ids + + self._attr_volume_level = ( + player.volume_level / 100 if player.volume_level is not None else None + ) self._attr_is_volume_muted = player.volume_muted self._update_media_attributes(player, queue) self._update_media_image_url(player, queue) @@ -523,6 +547,21 @@ async def _async_play_announcement( self.player_id, url, use_pre_announce, announce_volume ) + @catch_musicassistant_error + async def _async_transfer_queue( + self, source_player: str, auto_play: bool | None = None + ) -> None: + """Transfer the current queue to another player.""" + # resolve HA entity_id to MA player_id + if (hass_state := self.hass.states.get(source_player)) is None: + return # guard + if (mass_player_id := hass_state.attributes.get("mass_player_id")) is None: + return # guard + if queue := self.mass.player_queues.get(self.player.active_source): + await self.mass.player_queues.transfer_queue( + mass_player_id, queue.queue_id, auto_play + ) + async def async_browse_media( self, media_content_type=None, media_content_id=None ) -> BrowseMedia: @@ -638,8 +677,10 @@ def _update_media_attributes( self._attr_shuffle = None self._attr_repeat = None self._attr_media_position = player.elapsed_time - self._attr_media_position_updated_at = from_utc_timestamp( - player.elapsed_time_last_updated + self._attr_media_position_updated_at = ( + from_utc_timestamp(player.elapsed_time_last_updated) + if player.elapsed_time_last_updated + else None ) self._prev_time = player.elapsed_time return diff --git a/custom_components/mass/services.py b/custom_components/mass/services.py index bb93eb8a..214e86cc 100644 --- a/custom_components/mass/services.py +++ b/custom_components/mass/services.py @@ -24,6 +24,7 @@ ATTR_SEARCH_ARTIST = "artist" ATTR_SEARCH_ALBUM = "album" ATTR_LIMIT = "limit" +ATTR_LIBRARY_ONLY = "library_only" @callback @@ -46,6 +47,7 @@ async def handle_search(call: ServiceCall) -> ServiceResponse: search_query=search_name, media_types=call.data.get(ATTR_MEDIA_TYPE, MediaType.ALL), limit=call.data[ATTR_LIMIT], + library_only=call.data[ATTR_LIBRARY_ONLY], ) # return limited result to prevent it being too verbose @@ -92,6 +94,7 @@ def compact_item(item: dict[str, Any]) -> dict[str, Any]: vol.Optional(ATTR_SEARCH_ARTIST): cv.string, vol.Optional(ATTR_SEARCH_ALBUM): cv.string, vol.Optional(ATTR_LIMIT, default=5): vol.Coerce(int), + vol.Optional(ATTR_LIBRARY_ONLY, default=False): cv.boolean, } ), supports_response=SupportsResponse.ONLY, diff --git a/custom_components/mass/services.yaml b/custom_components/mass/services.yaml index b2cf8263..6ae5d73c 100644 --- a/custom_components/mass/services.yaml +++ b/custom_components/mass/services.yaml @@ -82,6 +82,24 @@ play_announcement: max: 100 step: 1 +transfer_queue: + target: + entity: + domain: media_player + integration: mass + fields: + source_entity: + required: true + selector: + entity: + domain: media_player + integration: mass + auto_play: + required: false + example: "true" + selector: + boolean: + search: fields: name: @@ -121,3 +139,9 @@ search: min: 1 max: 100 step: 1 + library_only: + required: false + example: "true" + default: false + selector: + boolean: diff --git a/custom_components/mass/strings.json b/custom_components/mass/strings.json index 25aa7989..0294740c 100644 --- a/custom_components/mass/strings.json +++ b/custom_components/mass/strings.json @@ -79,6 +79,20 @@ } } }, + "transfer_queue": { + "name": "Transfer Queue", + "description": "Transfer the player's queue to another player.", + "fields": { + "source_entity": { + "name": "Source media player", + "description": "The source media player which queue you want to transfer." + }, + "auto_play": { + "name": "Auto play", + "description": "Start playing the queue on the target player. Omit to use the default behavior." + } + } + }, "search": { "name": "Search Music Assistant", "description": "Perform a global search on the Music Assistant library and all providers.", @@ -102,6 +116,10 @@ "limit": { "name": "Limit", "description": "Maximum number of items to return (per media type)." + }, + "library_only": { + "name": "Only library items", + "description": "Only include results that are in the library." } } } diff --git a/custom_components/mass/translations/en.json b/custom_components/mass/translations/en.json index bf33d9a1..f1a51323 100644 --- a/custom_components/mass/translations/en.json +++ b/custom_components/mass/translations/en.json @@ -80,6 +80,20 @@ } } }, + "transfer_queue": { + "name": "Transfer Queue", + "description": "Transfer the player's queue to another player.", + "fields": { + "source_entity": { + "name": "Source media player", + "description": "The source media player which queue you want to transfer." + }, + "auto_play": { + "name": "Auto play", + "description": "Start playing the queue on the target player. Omit to use the default behavior." + } + } + }, "search": { "name": "Search Music Assistant", "description": "Perform a global search on the Music Assistant library and all providers.", @@ -103,6 +117,10 @@ "limit": { "name": "Limit", "description": "Maximum number of items to return (per media type)." + }, + "library_only": { + "name": "Only library items", + "description": "Only include results that are in the library." } } }