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
|
import os
|
||||||
from typing import TYPE_CHECKING, Any, Concatenate
|
from typing import TYPE_CHECKING, Any, Concatenate
|
||||||
|
|
||||||
|
from music_assistant_models.constants import PLAYER_CONTROL_NONE
|
||||||
from music_assistant_models.enums import (
|
from music_assistant_models.enums import (
|
||||||
EventType,
|
EventType,
|
||||||
MediaType,
|
MediaType,
|
||||||
@ -80,19 +81,14 @@ if TYPE_CHECKING:
|
|||||||
from music_assistant_client import MusicAssistantClient
|
from music_assistant_client import MusicAssistantClient
|
||||||
from music_assistant_models.player import Player
|
from music_assistant_models.player import Player
|
||||||
|
|
||||||
SUPPORTED_FEATURES = (
|
SUPPORTED_FEATURES_BASE = (
|
||||||
MediaPlayerEntityFeature.PAUSE
|
MediaPlayerEntityFeature.STOP
|
||||||
| MediaPlayerEntityFeature.VOLUME_SET
|
|
||||||
| MediaPlayerEntityFeature.STOP
|
|
||||||
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||||
| MediaPlayerEntityFeature.NEXT_TRACK
|
| MediaPlayerEntityFeature.NEXT_TRACK
|
||||||
| MediaPlayerEntityFeature.SHUFFLE_SET
|
| MediaPlayerEntityFeature.SHUFFLE_SET
|
||||||
| MediaPlayerEntityFeature.REPEAT_SET
|
| MediaPlayerEntityFeature.REPEAT_SET
|
||||||
| MediaPlayerEntityFeature.TURN_ON
|
|
||||||
| MediaPlayerEntityFeature.TURN_OFF
|
|
||||||
| MediaPlayerEntityFeature.PLAY
|
| MediaPlayerEntityFeature.PLAY
|
||||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
|
||||||
| MediaPlayerEntityFeature.CLEAR_PLAYLIST
|
| MediaPlayerEntityFeature.CLEAR_PLAYLIST
|
||||||
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||||
| MediaPlayerEntityFeature.MEDIA_ENQUEUE
|
| MediaPlayerEntityFeature.MEDIA_ENQUEUE
|
||||||
@ -212,11 +208,7 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
|||||||
"""Initialize MediaPlayer entity."""
|
"""Initialize MediaPlayer entity."""
|
||||||
super().__init__(mass, player_id)
|
super().__init__(mass, player_id)
|
||||||
self._attr_icon = self.player.icon.replace("mdi-", "mdi:")
|
self._attr_icon = self.player.icon.replace("mdi-", "mdi:")
|
||||||
self._attr_supported_features = SUPPORTED_FEATURES
|
self._set_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._attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
self._attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
||||||
self._prev_time: float = 0
|
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
|
@property
|
||||||
def active_queue(self) -> PlayerQueue | None:
|
def active_queue(self) -> PlayerQueue | None:
|
||||||
"""Return the active queue for this player (if any)."""
|
"""Return the active queue for this player (if any)."""
|
||||||
@ -682,3 +687,20 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
|||||||
if isinstance(queue_option, MediaPlayerEnqueue):
|
if isinstance(queue_option, MediaPlayerEnqueue):
|
||||||
queue_option = QUEUE_OPTION_MAP.get(queue_option)
|
queue_option = QUEUE_OPTION_MAP.get(queue_option)
|
||||||
return 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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
|
from music_assistant_models.api import MassEvent
|
||||||
from music_assistant_models.enums import EventType
|
from music_assistant_models.enums import EventType
|
||||||
from music_assistant_models.media_items import Album, Artist, Playlist, Radio, Track
|
from music_assistant_models.media_items import Album, Artist, Playlist, Radio, Track
|
||||||
from music_assistant_models.player import Player
|
from music_assistant_models.player import Player
|
||||||
@ -134,15 +136,42 @@ async def trigger_subscription_callback(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
client: MagicMock,
|
client: MagicMock,
|
||||||
event: EventType = EventType.PLAYER_UPDATED,
|
event: EventType = EventType.PLAYER_UPDATED,
|
||||||
|
object_id: str | None = None,
|
||||||
data: Any = None,
|
data: Any = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Trigger a subscription callback."""
|
"""Trigger a subscription callback."""
|
||||||
# trigger callback on all subscribers
|
# trigger callback on all subscribers
|
||||||
for sub in client.subscribe_events.call_args_list:
|
for sub in client.subscribe.call_args_list:
|
||||||
callback = sub.kwargs["callback"]
|
cb_func = sub.kwargs.get("cb_func", sub.args[0])
|
||||||
event_filter = sub.kwargs.get("event_filter")
|
event_filter = sub.kwargs.get(
|
||||||
if event_filter in (None, event):
|
"event_filter", sub.args[1] if len(sub.args) > 1 else None
|
||||||
callback(event, data)
|
)
|
||||||
|
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()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +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.constants import PLAYER_CONTROL_NONE
|
||||||
|
from music_assistant_models.enums import (
|
||||||
|
EventType,
|
||||||
|
MediaType,
|
||||||
|
PlayerFeature,
|
||||||
|
QueueOption,
|
||||||
|
)
|
||||||
from music_assistant_models.media_items import Track
|
from music_assistant_models.media_items import Track
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy import SnapshotAssertion
|
from syrupy import SnapshotAssertion
|
||||||
@ -20,6 +26,7 @@ from homeassistant.components.media_player import (
|
|||||||
SERVICE_CLEAR_PLAYLIST,
|
SERVICE_CLEAR_PLAYLIST,
|
||||||
SERVICE_JOIN,
|
SERVICE_JOIN,
|
||||||
SERVICE_UNJOIN,
|
SERVICE_UNJOIN,
|
||||||
|
MediaPlayerEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.components.music_assistant.const import DOMAIN as MASS_DOMAIN
|
from homeassistant.components.music_assistant.const import DOMAIN as MASS_DOMAIN
|
||||||
from homeassistant.components.music_assistant.media_player import (
|
from homeassistant.components.music_assistant.media_player import (
|
||||||
@ -59,7 +66,11 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
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
|
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
|
# no call is made, this info comes from the cached queue data
|
||||||
assert music_assistant_client.send_command.call_count == 0
|
assert music_assistant_client.send_command.call_count == 0
|
||||||
assert response == snapshot(exclude=paths(f"{entity_id}.elapsed_time"))
|
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