diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index 2d43d40bfb3..e7fcd84d299 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -10,6 +10,7 @@ from urllib.parse import urlparse from aioesphomeapi import ( EntityInfo, MediaPlayerCommand, + MediaPlayerEntityFeature as EspMediaPlayerEntityFeature, MediaPlayerEntityState, MediaPlayerFormatPurpose, 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" @@ -67,16 +93,12 @@ class EsphomeMediaPlayer( def _on_static_info_update(self, static_info: EntityInfo) -> None: """Set attrs from static info.""" super()._on_static_info_update(static_info) - flags = ( - MediaPlayerEntityFeature.PLAY_MEDIA - | MediaPlayerEntityFeature.BROWSE_MEDIA - | MediaPlayerEntityFeature.STOP - | MediaPlayerEntityFeature.VOLUME_SET - | MediaPlayerEntityFeature.VOLUME_MUTE - | MediaPlayerEntityFeature.MEDIA_ANNOUNCE + esp_flags = EspMediaPlayerEntityFeature( + self._static_info.feature_flags_compat(self._api_version) ) - if self._static_info.supports_pause: - flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY + flags = MediaPlayerEntityFeature(0) + for espflag in esp_flags: + flags |= _FEATURES[espflag] self._attr_supported_features = flags self._entry_data.media_player_formats[self.unique_id] = cast( MediaPlayerInfo, static_info diff --git a/tests/components/esphome/test_media_player.py b/tests/components/esphome/test_media_player.py index 232f7e1f06e..efc060bb136 100644 --- a/tests/components/esphome/test_media_player.py +++ b/tests/components/esphome/test_media_player.py @@ -29,6 +29,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, + STATE_PLAYING, BrowseMedia, MediaClass, MediaType, @@ -56,6 +57,8 @@ async def test_media_player_entity( key=1, name="my media_player", supports_pause=True, + # PLAY_MEDIA,BROWSE_MEDIA,STOP,VOLUME_SET,VOLUME_MUTE,MEDIA_ANNOUNCE,PAUSE,PLAY + feature_flags=1200653, ) ] states = [ @@ -156,6 +159,88 @@ async def test_media_player_entity( 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( hass: HomeAssistant, mock_client: APIClient, @@ -202,6 +287,8 @@ async def test_media_player_entity_with_source( key=1, name="my media_player", supports_pause=True, + # PLAY_MEDIA,BROWSE_MEDIA,STOP,VOLUME_SET,VOLUME_MUTE,MEDIA_ANNOUNCE,PAUSE,PLAY + feature_flags=1200653, ) ] states = [ @@ -317,6 +404,8 @@ async def test_media_player_proxy( key=1, name="my media_player", supports_pause=True, + # PLAY_MEDIA,BROWSE_MEDIA,STOP,VOLUME_SET,VOLUME_MUTE,MEDIA_ANNOUNCE,PAUSE,PLAY + feature_flags=1200653, supported_formats=[ MediaPlayerSupportedFormat( format="flac", @@ -475,6 +564,8 @@ async def test_media_player_formats_reload_preserves_data( key=1, name="Test Media Player", supports_pause=True, + # PLAY_MEDIA,BROWSE_MEDIA,STOP,VOLUME_SET,VOLUME_MUTE,MEDIA_ANNOUNCE,PAUSE,PLAY + feature_flags=1200653, supported_formats=supported_formats, ) ],