mirror of
https://github.com/home-assistant/core.git
synced 2025-09-11 07:41:35 +02:00
Media player API enumeration alignment and feature flags (#149597)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
@@ -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
|
||||||
|
@@ -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,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
Reference in New Issue
Block a user