Media player API enumeration alignment and feature flags (#149597)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
rwrozelle
2025-08-14 12:24:43 -04:00
committed by GitHub
parent 2248584a0f
commit 6e98446523
2 changed files with 122 additions and 9 deletions

View File

@@ -10,6 +10,7 @@ from urllib.parse import urlparse
from aioesphomeapi import ( from aioesphomeapi import (
EntityInfo, EntityInfo,
MediaPlayerCommand, MediaPlayerCommand,
MediaPlayerEntityFeature as EspMediaPlayerEntityFeature,
MediaPlayerEntityState, MediaPlayerEntityState,
MediaPlayerFormatPurpose, MediaPlayerFormatPurpose,
MediaPlayerInfo, MediaPlayerInfo,
@@ -53,6 +54,31 @@ _STATES: EsphomeEnumMapper[EspMediaPlayerState, MediaPlayerState] = EsphomeEnumM
} }
) )
_FEATURES = {
EspMediaPlayerEntityFeature.PAUSE: MediaPlayerEntityFeature.PAUSE,
EspMediaPlayerEntityFeature.SEEK: MediaPlayerEntityFeature.SEEK,
EspMediaPlayerEntityFeature.VOLUME_SET: MediaPlayerEntityFeature.VOLUME_SET,
EspMediaPlayerEntityFeature.VOLUME_MUTE: MediaPlayerEntityFeature.VOLUME_MUTE,
EspMediaPlayerEntityFeature.PREVIOUS_TRACK: MediaPlayerEntityFeature.PREVIOUS_TRACK,
EspMediaPlayerEntityFeature.NEXT_TRACK: MediaPlayerEntityFeature.NEXT_TRACK,
EspMediaPlayerEntityFeature.TURN_ON: MediaPlayerEntityFeature.TURN_ON,
EspMediaPlayerEntityFeature.TURN_OFF: MediaPlayerEntityFeature.TURN_OFF,
EspMediaPlayerEntityFeature.PLAY_MEDIA: MediaPlayerEntityFeature.PLAY_MEDIA,
EspMediaPlayerEntityFeature.VOLUME_STEP: MediaPlayerEntityFeature.VOLUME_STEP,
EspMediaPlayerEntityFeature.SELECT_SOURCE: MediaPlayerEntityFeature.SELECT_SOURCE,
EspMediaPlayerEntityFeature.STOP: MediaPlayerEntityFeature.STOP,
EspMediaPlayerEntityFeature.CLEAR_PLAYLIST: MediaPlayerEntityFeature.CLEAR_PLAYLIST,
EspMediaPlayerEntityFeature.PLAY: MediaPlayerEntityFeature.PLAY,
EspMediaPlayerEntityFeature.SHUFFLE_SET: MediaPlayerEntityFeature.SHUFFLE_SET,
EspMediaPlayerEntityFeature.SELECT_SOUND_MODE: MediaPlayerEntityFeature.SELECT_SOUND_MODE,
EspMediaPlayerEntityFeature.BROWSE_MEDIA: MediaPlayerEntityFeature.BROWSE_MEDIA,
EspMediaPlayerEntityFeature.REPEAT_SET: MediaPlayerEntityFeature.REPEAT_SET,
EspMediaPlayerEntityFeature.GROUPING: MediaPlayerEntityFeature.GROUPING,
EspMediaPlayerEntityFeature.MEDIA_ANNOUNCE: MediaPlayerEntityFeature.MEDIA_ANNOUNCE,
EspMediaPlayerEntityFeature.MEDIA_ENQUEUE: MediaPlayerEntityFeature.MEDIA_ENQUEUE,
EspMediaPlayerEntityFeature.SEARCH_MEDIA: MediaPlayerEntityFeature.SEARCH_MEDIA,
}
ATTR_BYPASS_PROXY = "bypass_proxy" ATTR_BYPASS_PROXY = "bypass_proxy"
@@ -67,16 +93,12 @@ class EsphomeMediaPlayer(
def _on_static_info_update(self, static_info: EntityInfo) -> None: def _on_static_info_update(self, static_info: EntityInfo) -> None:
"""Set attrs from static info.""" """Set attrs from static info."""
super()._on_static_info_update(static_info) super()._on_static_info_update(static_info)
flags = ( esp_flags = EspMediaPlayerEntityFeature(
MediaPlayerEntityFeature.PLAY_MEDIA self._static_info.feature_flags_compat(self._api_version)
| MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.STOP
| MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.MEDIA_ANNOUNCE
) )
if self._static_info.supports_pause: flags = MediaPlayerEntityFeature(0)
flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY for espflag in esp_flags:
flags |= _FEATURES[espflag]
self._attr_supported_features = flags self._attr_supported_features = flags
self._entry_data.media_player_formats[self.unique_id] = cast( self._entry_data.media_player_formats[self.unique_id] = cast(
MediaPlayerInfo, static_info MediaPlayerInfo, static_info

View File

@@ -29,6 +29,7 @@ from homeassistant.components.media_player import (
SERVICE_PLAY_MEDIA, SERVICE_PLAY_MEDIA,
SERVICE_VOLUME_MUTE, SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET, SERVICE_VOLUME_SET,
STATE_PLAYING,
BrowseMedia, BrowseMedia,
MediaClass, MediaClass,
MediaType, MediaType,
@@ -56,6 +57,8 @@ async def test_media_player_entity(
key=1, key=1,
name="my media_player", name="my media_player",
supports_pause=True, supports_pause=True,
# PLAY_MEDIA,BROWSE_MEDIA,STOP,VOLUME_SET,VOLUME_MUTE,MEDIA_ANNOUNCE,PAUSE,PLAY
feature_flags=1200653,
) )
] ]
states = [ states = [
@@ -156,6 +159,88 @@ async def test_media_player_entity(
mock_client.media_player_command.reset_mock() mock_client.media_player_command.reset_mock()
async def test_media_player_entity_with_undefined_flags(
hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry: MockGenericDeviceEntryType,
) -> None:
"""Test that media_player handles undefined feature flags gracefully."""
# Include existing flags (PAUSE=1, PLAY=16384, VOLUME_SET=4)
# plus undefined bits (bit 6=64, bit 23=8388608)
# Total: 1 + 16384 + 4 + 64 + 8388608 = 8405061
entity_info = [
MediaPlayerInfo(
object_id="mymedia_player_undefined",
key=1,
name="my media_player undefined",
supports_pause=True,
# PAUSE,PLAY,VOLUME_SET + undefined bits 6 and 23
feature_flags=8405061,
)
]
states = [
MediaPlayerEntityState(
key=1, volume=50, muted=False, state=MediaPlayerState.PLAYING
)
]
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
states=states,
)
# Verify entity is created successfully despite undefined flags
state = hass.states.get("media_player.test_my_media_player_undefined")
assert state is not None
assert state.state == STATE_PLAYING
# Verify supported features only include known flags
# Should have PAUSE, PLAY, and VOLUME_SET
supported_features = state.attributes.get("supported_features", 0)
# PAUSE=1, VOLUME_SET=4, PLAY=16384 = 16389
assert supported_features == 16389
# Verify entity works correctly with known features
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_PLAY,
{
ATTR_ENTITY_ID: "media_player.test_my_media_player_undefined",
},
blocking=True,
)
mock_client.media_player_command.assert_has_calls(
[call(1, command=MediaPlayerCommand.PLAY, device_id=0)]
)
mock_client.media_player_command.reset_mock()
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_PAUSE,
{
ATTR_ENTITY_ID: "media_player.test_my_media_player_undefined",
},
blocking=True,
)
mock_client.media_player_command.assert_has_calls(
[call(1, command=MediaPlayerCommand.PAUSE, device_id=0)]
)
mock_client.media_player_command.reset_mock()
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_VOLUME_SET,
{
ATTR_ENTITY_ID: "media_player.test_my_media_player_undefined",
ATTR_MEDIA_VOLUME_LEVEL: 0.7,
},
blocking=True,
)
mock_client.media_player_command.assert_has_calls(
[call(1, volume=0.7, device_id=0)]
)
async def test_media_player_entity_with_source( async def test_media_player_entity_with_source(
hass: HomeAssistant, hass: HomeAssistant,
mock_client: APIClient, mock_client: APIClient,
@@ -202,6 +287,8 @@ async def test_media_player_entity_with_source(
key=1, key=1,
name="my media_player", name="my media_player",
supports_pause=True, supports_pause=True,
# PLAY_MEDIA,BROWSE_MEDIA,STOP,VOLUME_SET,VOLUME_MUTE,MEDIA_ANNOUNCE,PAUSE,PLAY
feature_flags=1200653,
) )
] ]
states = [ states = [
@@ -317,6 +404,8 @@ async def test_media_player_proxy(
key=1, key=1,
name="my media_player", name="my media_player",
supports_pause=True, supports_pause=True,
# PLAY_MEDIA,BROWSE_MEDIA,STOP,VOLUME_SET,VOLUME_MUTE,MEDIA_ANNOUNCE,PAUSE,PLAY
feature_flags=1200653,
supported_formats=[ supported_formats=[
MediaPlayerSupportedFormat( MediaPlayerSupportedFormat(
format="flac", format="flac",
@@ -475,6 +564,8 @@ async def test_media_player_formats_reload_preserves_data(
key=1, key=1,
name="Test Media Player", name="Test Media Player",
supports_pause=True, supports_pause=True,
# PLAY_MEDIA,BROWSE_MEDIA,STOP,VOLUME_SET,VOLUME_MUTE,MEDIA_ANNOUNCE,PAUSE,PLAY
feature_flags=1200653,
supported_formats=supported_formats, supported_formats=supported_formats,
) )
], ],