mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Fix Music Assistant media player entity features (#139428)
* Fix Music Assistant supported media player features * Update supported features when player config changes * Add tests
This commit is contained in:
parent
59eb323f8d
commit
f111a2c34a
@ -9,6 +9,7 @@ import functools
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, Concatenate
|
||||
|
||||
from music_assistant_models.constants import PLAYER_CONTROL_NONE
|
||||
from music_assistant_models.enums import (
|
||||
EventType,
|
||||
MediaType,
|
||||
@ -80,19 +81,14 @@ if TYPE_CHECKING:
|
||||
from music_assistant_client import MusicAssistantClient
|
||||
from music_assistant_models.player import Player
|
||||
|
||||
SUPPORTED_FEATURES = (
|
||||
MediaPlayerEntityFeature.PAUSE
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.STOP
|
||||
SUPPORTED_FEATURES_BASE = (
|
||||
MediaPlayerEntityFeature.STOP
|
||||
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||
| MediaPlayerEntityFeature.NEXT_TRACK
|
||||
| MediaPlayerEntityFeature.SHUFFLE_SET
|
||||
| MediaPlayerEntityFeature.REPEAT_SET
|
||||
| MediaPlayerEntityFeature.TURN_ON
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.PLAY
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.CLEAR_PLAYLIST
|
||||
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||
| MediaPlayerEntityFeature.MEDIA_ENQUEUE
|
||||
@ -212,11 +208,7 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||
"""Initialize MediaPlayer entity."""
|
||||
super().__init__(mass, player_id)
|
||||
self._attr_icon = self.player.icon.replace("mdi-", "mdi:")
|
||||
self._attr_supported_features = SUPPORTED_FEATURES
|
||||
if PlayerFeature.SET_MEMBERS in self.player.supported_features:
|
||||
self._attr_supported_features |= MediaPlayerEntityFeature.GROUPING
|
||||
if PlayerFeature.VOLUME_MUTE in self.player.supported_features:
|
||||
self._attr_supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
self._set_supported_features()
|
||||
self._attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
||||
self._prev_time: float = 0
|
||||
|
||||
@ -241,6 +233,19 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||
)
|
||||
)
|
||||
|
||||
# we subscribe to the player config changed event to update
|
||||
# the supported features of the player
|
||||
async def player_config_changed(event: MassEvent) -> None:
|
||||
self._set_supported_features()
|
||||
await self.async_on_update()
|
||||
self.async_write_ha_state()
|
||||
|
||||
self.async_on_remove(
|
||||
self.mass.subscribe(
|
||||
player_config_changed, EventType.PLAYER_CONFIG_UPDATED, self.player_id
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def active_queue(self) -> PlayerQueue | None:
|
||||
"""Return the active queue for this player (if any)."""
|
||||
@ -682,3 +687,20 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||
if isinstance(queue_option, MediaPlayerEnqueue):
|
||||
queue_option = QUEUE_OPTION_MAP.get(queue_option)
|
||||
return queue_option
|
||||
|
||||
def _set_supported_features(self) -> None:
|
||||
"""Set supported features based on player capabilities."""
|
||||
supported_features = SUPPORTED_FEATURES_BASE
|
||||
if PlayerFeature.SET_MEMBERS in self.player.supported_features:
|
||||
supported_features |= MediaPlayerEntityFeature.GROUPING
|
||||
if PlayerFeature.PAUSE in self.player.supported_features:
|
||||
supported_features |= MediaPlayerEntityFeature.PAUSE
|
||||
if self.player.mute_control != PLAYER_CONTROL_NONE:
|
||||
supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
if self.player.volume_control != PLAYER_CONTROL_NONE:
|
||||
supported_features |= MediaPlayerEntityFeature.VOLUME_STEP
|
||||
supported_features |= MediaPlayerEntityFeature.VOLUME_SET
|
||||
if self.player.power_control != PLAYER_CONTROL_NONE:
|
||||
supported_features |= MediaPlayerEntityFeature.TURN_ON
|
||||
supported_features |= MediaPlayerEntityFeature.TURN_OFF
|
||||
self._attr_supported_features = supported_features
|
||||
|
@ -2,9 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from music_assistant_models.api import MassEvent
|
||||
from music_assistant_models.enums import EventType
|
||||
from music_assistant_models.media_items import Album, Artist, Playlist, Radio, Track
|
||||
from music_assistant_models.player import Player
|
||||
@ -134,15 +136,42 @@ async def trigger_subscription_callback(
|
||||
hass: HomeAssistant,
|
||||
client: MagicMock,
|
||||
event: EventType = EventType.PLAYER_UPDATED,
|
||||
object_id: str | None = None,
|
||||
data: Any = None,
|
||||
) -> None:
|
||||
"""Trigger a subscription callback."""
|
||||
# trigger callback on all subscribers
|
||||
for sub in client.subscribe_events.call_args_list:
|
||||
callback = sub.kwargs["callback"]
|
||||
event_filter = sub.kwargs.get("event_filter")
|
||||
if event_filter in (None, event):
|
||||
callback(event, data)
|
||||
for sub in client.subscribe.call_args_list:
|
||||
cb_func = sub.kwargs.get("cb_func", sub.args[0])
|
||||
event_filter = sub.kwargs.get(
|
||||
"event_filter", sub.args[1] if len(sub.args) > 1 else None
|
||||
)
|
||||
id_filter = sub.kwargs.get(
|
||||
"id_filter", sub.args[2] if len(sub.args) > 2 else None
|
||||
)
|
||||
if not (
|
||||
event_filter is None
|
||||
or event == event_filter
|
||||
or (isinstance(event_filter, list) and event in event_filter)
|
||||
):
|
||||
continue
|
||||
if not (
|
||||
id_filter is None
|
||||
or object_id == id_filter
|
||||
or (isinstance(id_filter, list) and object_id in id_filter)
|
||||
):
|
||||
continue
|
||||
|
||||
event = MassEvent(
|
||||
event=event,
|
||||
object_id=object_id,
|
||||
data=data,
|
||||
)
|
||||
if asyncio.iscoroutinefunction(cb_func):
|
||||
await cb_func(event)
|
||||
else:
|
||||
cb_func(event)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
|
@ -2,7 +2,13 @@
|
||||
|
||||
from unittest.mock import MagicMock, call
|
||||
|
||||
from music_assistant_models.enums import MediaType, QueueOption
|
||||
from music_assistant_models.constants import PLAYER_CONTROL_NONE
|
||||
from music_assistant_models.enums import (
|
||||
EventType,
|
||||
MediaType,
|
||||
PlayerFeature,
|
||||
QueueOption,
|
||||
)
|
||||
from music_assistant_models.media_items import Track
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
@ -20,6 +26,7 @@ from homeassistant.components.media_player import (
|
||||
SERVICE_CLEAR_PLAYLIST,
|
||||
SERVICE_JOIN,
|
||||
SERVICE_UNJOIN,
|
||||
MediaPlayerEntityFeature,
|
||||
)
|
||||
from homeassistant.components.music_assistant.const import DOMAIN as MASS_DOMAIN
|
||||
from homeassistant.components.music_assistant.media_player import (
|
||||
@ -59,7 +66,11 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
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,
|
||||
trigger_subscription_callback,
|
||||
)
|
||||
|
||||
from tests.common import AsyncMock
|
||||
|
||||
@ -607,3 +618,104 @@ async def test_media_player_get_queue_action(
|
||||
# no call is made, this info comes from the cached queue data
|
||||
assert music_assistant_client.send_command.call_count == 0
|
||||
assert response == snapshot(exclude=paths(f"{entity_id}.elapsed_time"))
|
||||
|
||||
|
||||
async def test_media_player_supported_features(
|
||||
hass: HomeAssistant,
|
||||
music_assistant_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test if media_player entity supported features are cortrectly (re)mapped."""
|
||||
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
|
||||
expected_features = (
|
||||
MediaPlayerEntityFeature.STOP
|
||||
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||
| MediaPlayerEntityFeature.NEXT_TRACK
|
||||
| MediaPlayerEntityFeature.SHUFFLE_SET
|
||||
| MediaPlayerEntityFeature.REPEAT_SET
|
||||
| MediaPlayerEntityFeature.PLAY
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.CLEAR_PLAYLIST
|
||||
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||
| MediaPlayerEntityFeature.MEDIA_ENQUEUE
|
||||
| MediaPlayerEntityFeature.MEDIA_ANNOUNCE
|
||||
| MediaPlayerEntityFeature.SEEK
|
||||
| MediaPlayerEntityFeature.PAUSE
|
||||
| MediaPlayerEntityFeature.GROUPING
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
| MediaPlayerEntityFeature.TURN_ON
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
)
|
||||
assert state.attributes["supported_features"] == expected_features
|
||||
# remove power control capability from player, trigger subscription callback
|
||||
# and check if the supported features got updated
|
||||
music_assistant_client.players._players[
|
||||
mass_player_id
|
||||
].power_control = PLAYER_CONTROL_NONE
|
||||
await trigger_subscription_callback(
|
||||
hass, music_assistant_client, EventType.PLAYER_CONFIG_UPDATED, mass_player_id
|
||||
)
|
||||
expected_features &= ~MediaPlayerEntityFeature.TURN_ON
|
||||
expected_features &= ~MediaPlayerEntityFeature.TURN_OFF
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes["supported_features"] == expected_features
|
||||
|
||||
# remove volume control capability from player, trigger subscription callback
|
||||
# and check if the supported features got updated
|
||||
music_assistant_client.players._players[
|
||||
mass_player_id
|
||||
].volume_control = PLAYER_CONTROL_NONE
|
||||
await trigger_subscription_callback(
|
||||
hass, music_assistant_client, EventType.PLAYER_CONFIG_UPDATED, mass_player_id
|
||||
)
|
||||
expected_features &= ~MediaPlayerEntityFeature.VOLUME_SET
|
||||
expected_features &= ~MediaPlayerEntityFeature.VOLUME_STEP
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes["supported_features"] == expected_features
|
||||
|
||||
# remove mute control capability from player, trigger subscription callback
|
||||
# and check if the supported features got updated
|
||||
music_assistant_client.players._players[
|
||||
mass_player_id
|
||||
].mute_control = PLAYER_CONTROL_NONE
|
||||
await trigger_subscription_callback(
|
||||
hass, music_assistant_client, EventType.PLAYER_CONFIG_UPDATED, mass_player_id
|
||||
)
|
||||
expected_features &= ~MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes["supported_features"] == expected_features
|
||||
|
||||
# remove pause capability from player, trigger subscription callback
|
||||
# and check if the supported features got updated
|
||||
music_assistant_client.players._players[mass_player_id].supported_features.remove(
|
||||
PlayerFeature.PAUSE
|
||||
)
|
||||
await trigger_subscription_callback(
|
||||
hass, music_assistant_client, EventType.PLAYER_CONFIG_UPDATED, mass_player_id
|
||||
)
|
||||
expected_features &= ~MediaPlayerEntityFeature.PAUSE
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes["supported_features"] == expected_features
|
||||
|
||||
# remove grouping capability from player, trigger subscription callback
|
||||
# and check if the supported features got updated
|
||||
music_assistant_client.players._players[mass_player_id].supported_features.remove(
|
||||
PlayerFeature.SET_MEMBERS
|
||||
)
|
||||
await trigger_subscription_callback(
|
||||
hass, music_assistant_client, EventType.PLAYER_CONFIG_UPDATED, mass_player_id
|
||||
)
|
||||
expected_features &= ~MediaPlayerEntityFeature.GROUPING
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes["supported_features"] == expected_features
|
||||
|
Loading…
x
Reference in New Issue
Block a user