From 4821c9ec29f4fc02e98306e1489abacf272906f6 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 27 Aug 2025 03:39:33 -0700 Subject: [PATCH] Use media_selector for media_player.play_media (#150721) --- .../components/media_player/__init__.py | 21 ++++++++ .../components/media_player/services.yaml | 14 ++--- .../components/media_player/strings.json | 10 ++-- tests/components/media_player/test_init.py | 53 +++++++++++++++++++ 4 files changed, 82 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index b2cb7d76e8f..01ff31e277c 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -161,6 +161,8 @@ CACHE_LOCK: Final = "lock" CACHE_URL: Final = "url" CACHE_CONTENT: Final = "content" +ATTR_MEDIA = "media" + class MediaPlayerEnqueue(StrEnum): """Enqueue types for playing media.""" @@ -200,6 +202,24 @@ _DEPRECATED_DEVICE_CLASS_RECEIVER = DeprecatedConstantEnum( DEVICE_CLASSES = [cls.value for cls in MediaPlayerDeviceClass] +def _promote_media_fields(data: dict[str, Any]) -> dict[str, Any]: + """If 'media' key exists, promote its fields to the top level.""" + if ATTR_MEDIA in data and isinstance(data[ATTR_MEDIA], dict): + if ATTR_MEDIA_CONTENT_TYPE in data or ATTR_MEDIA_CONTENT_ID in data: + raise vol.Invalid( + f"Play media cannot contain '{ATTR_MEDIA}' and '{ATTR_MEDIA_CONTENT_ID}' or '{ATTR_MEDIA_CONTENT_TYPE}'" + ) + media_data = data[ATTR_MEDIA] + + if ATTR_MEDIA_CONTENT_TYPE in media_data: + data[ATTR_MEDIA_CONTENT_TYPE] = media_data[ATTR_MEDIA_CONTENT_TYPE] + if ATTR_MEDIA_CONTENT_ID in media_data: + data[ATTR_MEDIA_CONTENT_ID] = media_data[ATTR_MEDIA_CONTENT_ID] + + del data[ATTR_MEDIA] + return data + + MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = { vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, @@ -436,6 +456,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_PLAY_MEDIA, vol.All( + _promote_media_fields, cv.make_entity_service_schema(MEDIA_PLAYER_PLAY_MEDIA_SCHEMA), _rewrite_enqueue, _rename_keys( diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index ac359de1a5b..24a04393d94 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -131,17 +131,13 @@ play_media: supported_features: - media_player.MediaPlayerEntityFeature.PLAY_MEDIA fields: - media_content_id: + media: required: true - example: "https://home-assistant.io/images/cast/splash.png" selector: - text: - - media_content_type: - required: true - example: "music" - selector: - text: + media: + example: + media_content_id: "https://home-assistant.io/images/cast/splash.png" + media_content_type: "music" enqueue: filter: diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json index 617cb258af7..c3b96a5250e 100644 --- a/homeassistant/components/media_player/strings.json +++ b/homeassistant/components/media_player/strings.json @@ -242,13 +242,9 @@ "name": "Play media", "description": "Starts playing specified media.", "fields": { - "media_content_id": { - "name": "Content ID", - "description": "The ID of the content to play. Platform dependent." - }, - "media_content_type": { - "name": "Content type", - "description": "The type of the content to play, such as image, music, tv show, video, episode, channel, or playlist." + "media": { + "name": "Media", + "description": "The media selected to play." }, "enqueue": { "name": "Enqueue", diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 2e270eb3b2e..552a94e8723 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -654,3 +654,56 @@ async def test_get_async_get_browse_image_quoting( url = player.get_browse_image_url("album", media_content_id) await client.get(url) mock_browse_image.assert_called_with("album", media_content_id, None) + + +async def test_play_media_via_selector(hass: HomeAssistant) -> None: + """Test that play_media data under 'media' is remapped to top level keys for backward compatibility.""" + await async_setup_component( + hass, "media_player", {"media_player": {"platform": "demo"}} + ) + await hass.async_block_till_done() + + # Fake group support for DemoYoutubePlayer + with patch( + "homeassistant.components.demo.media_player.DemoYoutubePlayer.play_media", + ) as mock_play_media: + await hass.services.async_call( + "media_player", + "play_media", + { + "entity_id": "media_player.bedroom", + "media": { + "media_content_type": "music", + "media_content_id": "1234", + }, + }, + blocking=True, + ) + await hass.services.async_call( + "media_player", + "play_media", + { + "entity_id": "media_player.bedroom", + "media_content_type": "music", + "media_content_id": "1234", + }, + blocking=True, + ) + + assert len(mock_play_media.mock_calls) == 2 + assert mock_play_media.mock_calls[0].args == mock_play_media.mock_calls[1].args + + with pytest.raises(vol.Invalid, match="Play media cannot contain 'media'"): + await hass.services.async_call( + "media_player", + "play_media", + { + "media_content_id": "1234", + "entity_id": "media_player.bedroom", + "media": { + "media_content_type": "music", + "media_content_id": "1234", + }, + }, + blocking=True, + )