mirror of
https://github.com/home-assistant/core.git
synced 2026-04-20 08:29:39 +02:00
Add support for sound modes to Music Assistant. (#167838)
This commit is contained in:
@@ -82,3 +82,4 @@ ATTR_CONF_EXPOSE_PLAYER_TO_HA = "expose_player_to_ha"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
PLAYER_OPTIONS_TRANSLATION_KEY_PREFIX = "player_options."
|
||||
SOUND_MODES_TRANSLATION_KEY_PREFIX = "player_sound_mode."
|
||||
|
||||
@@ -60,6 +60,7 @@ from .const import (
|
||||
ATTR_REPEAT_MODE,
|
||||
ATTR_SHUFFLE_ENABLED,
|
||||
DOMAIN,
|
||||
SOUND_MODES_TRANSLATION_KEY_PREFIX,
|
||||
)
|
||||
from .entity import MusicAssistantEntity
|
||||
from .helpers import catch_musicassistant_error
|
||||
@@ -131,6 +132,7 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||
_attr_name = None
|
||||
_attr_media_image_remotely_accessible = True
|
||||
_attr_media_content_type = HAMediaType.MUSIC
|
||||
_attr_translation_key = "ma_media_player"
|
||||
|
||||
def __init__(self, mass: MusicAssistantClient, player_id: str) -> None:
|
||||
"""Initialize MediaPlayer entity."""
|
||||
@@ -140,6 +142,7 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||
self._attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
||||
self._prev_time: float = 0
|
||||
self._source_list_mapping: dict[str, str] = {}
|
||||
self._sound_mode_list_mapping: dict[str, str] = {}
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
@@ -218,6 +221,29 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||
self._source_list_mapping = source_mappings
|
||||
self._attr_source = active_source_name
|
||||
|
||||
# same for sound modes
|
||||
sound_mode_mappings: dict[str, str] = {}
|
||||
for sound_mode in player.sound_mode_list:
|
||||
if sound_mode.passive:
|
||||
# ignore passive sound_mode because HA does not differentiate between
|
||||
# active and passive sound mode
|
||||
continue
|
||||
if (
|
||||
sound_mode.translation_key is None
|
||||
or SOUND_MODES_TRANSLATION_KEY_PREFIX not in sound_mode.translation_key
|
||||
):
|
||||
# MA's data class initializes the translation_key to
|
||||
# player_sound_mode.<id> automatically if it is not given, so we should
|
||||
# always have a non None value
|
||||
continue
|
||||
translation_key = sound_mode.translation_key[
|
||||
len(SOUND_MODES_TRANSLATION_KEY_PREFIX) :
|
||||
]
|
||||
sound_mode_mappings[translation_key] = sound_mode.id
|
||||
self._attr_sound_mode_list = list(sound_mode_mappings.keys())
|
||||
self._sound_mode_list_mapping = sound_mode_mappings
|
||||
self._attr_sound_mode = player.active_sound_mode
|
||||
|
||||
group_members: list[str] = []
|
||||
if player.group_members:
|
||||
group_members = player.group_members
|
||||
@@ -397,6 +423,16 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||
)
|
||||
await self.mass.players.player_command_select_source(self.player_id, source_id)
|
||||
|
||||
@catch_musicassistant_error
|
||||
async def async_select_sound_mode(self, sound_mode: str) -> None:
|
||||
"""Select sound mode."""
|
||||
sound_mode_id = self._sound_mode_list_mapping.get(sound_mode)
|
||||
if sound_mode_id is None:
|
||||
raise ServiceValidationError(
|
||||
f"Sound mode '{sound_mode}' not found for player {self.name}"
|
||||
)
|
||||
await self.mass.players.select_sound_mode(self.player_id, sound_mode_id)
|
||||
|
||||
@catch_musicassistant_error
|
||||
async def _async_handle_play_media(
|
||||
self,
|
||||
@@ -682,4 +718,6 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||
supported_features |= MediaPlayerEntityFeature.TURN_OFF
|
||||
if PlayerFeature.SELECT_SOURCE in self.player.supported_features:
|
||||
supported_features |= MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
if PlayerFeature.SELECT_SOUND_MODE in self.player.supported_features:
|
||||
supported_features |= MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
||||
self._attr_supported_features = supported_features
|
||||
|
||||
@@ -54,6 +54,69 @@
|
||||
"name": "Favorite current song"
|
||||
}
|
||||
},
|
||||
"media_player": {
|
||||
"ma_media_player": {
|
||||
"state_attributes": {
|
||||
"sound_mode": {
|
||||
"state": {
|
||||
"2ch_stereo": "2ch stereo",
|
||||
"5ch_stereo": "5ch stereo",
|
||||
"7ch_stereo": "7ch stereo",
|
||||
"9ch_stereo": "9ch stereo",
|
||||
"11ch_stereo": "11ch stereo",
|
||||
"action_game": "Action game",
|
||||
"adventure": "Adventure",
|
||||
"all_ch_stereo": "All ch stereo",
|
||||
"amsterdam": "Hall in Amsterdam",
|
||||
"arena": "Arena",
|
||||
"bottom_line": "The Bottom Line",
|
||||
"cellar_club": "Cellar club",
|
||||
"chamber": "Chamber",
|
||||
"concert": "Live concert",
|
||||
"disco": "Disco",
|
||||
"drama": "Drama",
|
||||
"enhanced": "Enhanced",
|
||||
"frankfurt": "Hall in Frankfurt",
|
||||
"freiburg": "Church in Freiburg",
|
||||
"game": "Game",
|
||||
"jazz_club": "Jazz club",
|
||||
"mono_movie": "Mono movie",
|
||||
"movie": "Movie",
|
||||
"munich": "Hall in Munich",
|
||||
"munich_a": "Hall in Munich A",
|
||||
"munich_b": "Hall in Munich B",
|
||||
"music": "Music",
|
||||
"music_video": "Music video",
|
||||
"my_surround": "My surround",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"pavilion": "Pavilion",
|
||||
"recital_opera": "Recital/opera",
|
||||
"roleplaying_game": "Roleplaying game",
|
||||
"roxy_theatre": "The Roxy Theatre",
|
||||
"royaumont": "Church in Royaumont",
|
||||
"sci-fi": "Sci-fi",
|
||||
"spectacle": "Spectacle",
|
||||
"sports": "Sports",
|
||||
"standard": "Standard",
|
||||
"stereo": "Stereo",
|
||||
"straight": "Straight",
|
||||
"stuttgart": "Hall in Stuttgart",
|
||||
"surr_decoder": "Surround decoder",
|
||||
"talk_show": "Talk show",
|
||||
"target": "Target",
|
||||
"tokyo": "Church in Tokyo",
|
||||
"tv_program": "TV program",
|
||||
"usa_a": "Hall in USA A",
|
||||
"usa_b": "Hall in USA B",
|
||||
"vienna": "Hall in Vienna",
|
||||
"village_gate": "Village Gate",
|
||||
"village_vanguard": "Village Vanguard",
|
||||
"warehouse_loft": "Warehouse loft"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"bass": {
|
||||
"name": "Bass"
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"set_members",
|
||||
"power",
|
||||
"enqueue",
|
||||
"select_source"
|
||||
"select_source",
|
||||
"select_sound_mode"
|
||||
],
|
||||
"elapsed_time": null,
|
||||
"elapsed_time_last_updated": 0,
|
||||
@@ -193,6 +194,20 @@
|
||||
"can_seek": false,
|
||||
"can_next_previous": false
|
||||
}
|
||||
],
|
||||
"sound_mode_list": [
|
||||
{
|
||||
"id": "munich",
|
||||
"name": "Munich",
|
||||
"passive": false,
|
||||
"translation_key": "player_sound_mode.munich"
|
||||
},
|
||||
{
|
||||
"id": "vienna",
|
||||
"name": "Vienna",
|
||||
"passive": false,
|
||||
"translation_key": "player_sound_mode.vienna"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <MediaPlayerEntityFeature: 8320959>,
|
||||
'translation_key': None,
|
||||
'translation_key': 'ma_media_player',
|
||||
'unique_id': '00:00:00:00:00:02',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
@@ -103,7 +103,7 @@
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <MediaPlayerEntityFeature: 8320959>,
|
||||
'translation_key': None,
|
||||
'translation_key': 'ma_media_player',
|
||||
'unique_id': 'test_group_player_1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
@@ -152,6 +152,10 @@
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'sound_mode_list': list([
|
||||
'munich',
|
||||
'vienna',
|
||||
]),
|
||||
'source_list': list([
|
||||
'Music Assistant Queue',
|
||||
'Line-In',
|
||||
@@ -181,8 +185,8 @@
|
||||
'platform': 'music_assistant',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <MediaPlayerEntityFeature: 8323007>,
|
||||
'translation_key': None,
|
||||
'supported_features': <MediaPlayerEntityFeature: 8388543>,
|
||||
'translation_key': 'ma_media_player',
|
||||
'unique_id': '00:00:00:00:00:01',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
@@ -198,11 +202,15 @@
|
||||
'icon': 'mdi:speaker',
|
||||
'last_non_buffering_state': <MediaPlayerState.OFF: 'off'>,
|
||||
'mass_player_type': 'player',
|
||||
'sound_mode_list': list([
|
||||
'munich',
|
||||
'vienna',
|
||||
]),
|
||||
'source_list': list([
|
||||
'Music Assistant Queue',
|
||||
'Line-In',
|
||||
]),
|
||||
'supported_features': <MediaPlayerEntityFeature: 8323007>,
|
||||
'supported_features': <MediaPlayerEntityFeature: 8388543>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'media_player.test_player_1',
|
||||
|
||||
@@ -24,9 +24,11 @@ from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_SHUFFLE,
|
||||
ATTR_MEDIA_VOLUME_LEVEL,
|
||||
ATTR_MEDIA_VOLUME_MUTED,
|
||||
ATTR_SOUND_MODE,
|
||||
DOMAIN as MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_CLEAR_PLAYLIST,
|
||||
SERVICE_JOIN,
|
||||
SERVICE_SELECT_SOUND_MODE,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
SERVICE_UNJOIN,
|
||||
MediaPlayerEntityFeature,
|
||||
@@ -653,6 +655,31 @@ async def test_media_player_select_source_action(
|
||||
)
|
||||
|
||||
|
||||
async def test_media_player_select_sound_mode_action(
|
||||
hass: HomeAssistant,
|
||||
music_assistant_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test media_player entity select sound mode action."""
|
||||
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
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_SELECT_SOUND_MODE,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_SOUND_MODE: "munich",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
assert music_assistant_client.send_command.call_count == 1
|
||||
assert music_assistant_client.send_command.call_args == call(
|
||||
"players/cmd/select_sound_mode", player_id=mass_player_id, sound_mode="munich"
|
||||
)
|
||||
|
||||
|
||||
async def test_media_player_supported_features(
|
||||
hass: HomeAssistant,
|
||||
music_assistant_client: MagicMock,
|
||||
@@ -686,6 +713,7 @@ async def test_media_player_supported_features(
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.SEARCH_MEDIA
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
| MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
||||
)
|
||||
assert state.attributes["supported_features"] == expected_features
|
||||
# remove power control capability from player, trigger subscription callback
|
||||
|
||||
Reference in New Issue
Block a user