Expose media_player async_browse_media as service (#116452)

* initial commit

* make fields optional

* x

* ruff issues

* ruff issues

* ruff issues

* ruff issues

* update example

* update description

* use constants

* Update homeassistant/components/media_player/strings.json

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* update service call metadata

* update description

* patch the demo

* Update homeassistant/components/media_player/strings.json

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* revert unrelated change

* update test metadata

* update test metadata

* change patch target to be more specific

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Pete Sage 2025-02-11 19:06:13 -05:00 committed by GitHub
parent da1e3c29ed
commit 1393f417ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 123 additions and 1 deletions

View File

@ -52,7 +52,7 @@ from homeassistant.const import ( # noqa: F401
STATE_PLAYING,
STATE_STANDBY,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, SupportsResponse
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.deprecation import (
@ -124,6 +124,7 @@ from .const import ( # noqa: F401
CONTENT_AUTH_EXPIRY_TIME,
DOMAIN,
REPEAT_MODES,
SERVICE_BROWSE_MEDIA,
SERVICE_CLEAR_PLAYLIST,
SERVICE_JOIN,
SERVICE_PLAY_MEDIA,
@ -201,6 +202,12 @@ MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = {
vol.Optional(ATTR_MEDIA_EXTRA, default={}): dict,
}
MEDIA_PLAYER_BROWSE_MEDIA_SCHEMA = {
vol.Optional(ATTR_MEDIA_CONTENT_TYPE): cv.string,
vol.Optional(ATTR_MEDIA_CONTENT_ID): cv.string,
}
ATTR_TO_PROPERTY = [
ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_VOLUME_MUTED,
@ -431,6 +438,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"async_play_media",
[MediaPlayerEntityFeature.PLAY_MEDIA],
)
component.async_register_entity_service(
SERVICE_BROWSE_MEDIA,
{
vol.Optional(ATTR_MEDIA_CONTENT_TYPE): cv.string,
vol.Optional(ATTR_MEDIA_CONTENT_ID): cv.string,
},
"async_browse_media",
supports_response=SupportsResponse.ONLY,
)
component.async_register_entity_service(
SERVICE_SHUFFLE_SET,
{vol.Required(ATTR_MEDIA_SHUFFLE): cv.boolean},

View File

@ -173,6 +173,7 @@ _DEPRECATED_MEDIA_TYPE_VIDEO = DeprecatedConstantEnum(MediaType.VIDEO, "2025.10"
SERVICE_CLEAR_PLAYLIST = "clear_playlist"
SERVICE_JOIN = "join"
SERVICE_PLAY_MEDIA = "play_media"
SERVICE_BROWSE_MEDIA = "browse_media"
SERVICE_SELECT_SOUND_MODE = "select_sound_mode"
SERVICE_SELECT_SOURCE = "select_source"
SERVICE_UNJOIN = "unjoin"

View File

@ -32,6 +32,9 @@
}
},
"services": {
"browse_media": {
"service": "mdi:folder-search"
},
"clear_playlist": {
"service": "mdi:playlist-remove"
},

View File

@ -165,6 +165,22 @@ play_media:
selector:
boolean:
browse_media:
target:
entity:
domain: media_player
fields:
media_content_type:
required: false
example: "music"
selector:
text:
media_content_id:
required: false
example: "A:ALBUMARTIST/Beatles"
selector:
text:
select_source:
target:
entity:

View File

@ -260,6 +260,20 @@
}
}
},
"browse_media": {
"name": "Browse media",
"description": "Browses the available media.",
"fields": {
"media_content_id": {
"name": "Content ID",
"description": "The ID of the content to browse. Integration dependent."
},
"media_content_type": {
"name": "Content type",
"description": "The type of the content to browse, such as image, music, tv show, video, episode, channel, or playlist."
}
}
},
"select_source": {
"name": "Select source",
"description": "Sends the media player the command to change input source.",

View File

@ -10,12 +10,15 @@ import voluptuous as vol
from homeassistant.components import media_player
from homeassistant.components.media_player import (
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
BrowseMedia,
MediaClass,
MediaPlayerEnqueue,
MediaPlayerEntity,
MediaPlayerEntityFeature,
)
from homeassistant.components.media_player.const import SERVICE_BROWSE_MEDIA
from homeassistant.components.websocket_api import TYPE_RESULT
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF
from homeassistant.core import HomeAssistant
@ -339,6 +342,75 @@ async def test_media_browse(
assert msg["result"] == {"bla": "yo"}
async def test_media_browse_service(hass: HomeAssistant) -> None:
"""Test browsing media using service call."""
await async_setup_component(
hass, "media_player", {"media_player": {"platform": "demo"}}
)
await hass.async_block_till_done()
with patch(
"homeassistant.components.demo.media_player.DemoBrowsePlayer.async_browse_media",
return_value=BrowseMedia(
media_class=MediaClass.DIRECTORY,
media_content_id="mock-id",
media_content_type="mock-type",
title="Mock Title",
can_play=False,
can_expand=True,
children=[
BrowseMedia(
media_class=MediaClass.ALBUM,
media_content_id="album1 content id",
media_content_type="album",
title="Album 1",
can_play=True,
can_expand=True,
),
BrowseMedia(
media_class=MediaClass.ALBUM,
media_content_id="album2 content id",
media_content_type="album",
title="Album 2",
can_play=True,
can_expand=True,
),
],
),
) as mock_browse_media:
result = await hass.services.async_call(
"media_player",
SERVICE_BROWSE_MEDIA,
{
ATTR_ENTITY_ID: "media_player.browse",
ATTR_MEDIA_CONTENT_TYPE: "album",
ATTR_MEDIA_CONTENT_ID: "title=Album*",
},
blocking=True,
return_response=True,
)
mock_browse_media.assert_called_with(
media_content_type="album", media_content_id="title=Album*"
)
browse_res: BrowseMedia = result["media_player.browse"]
assert browse_res.title == "Mock Title"
assert browse_res.media_class == "directory"
assert browse_res.media_content_type == "mock-type"
assert browse_res.media_content_id == "mock-id"
assert browse_res.can_play is False
assert browse_res.can_expand is True
assert len(browse_res.children) == 2
assert browse_res.children[0].title == "Album 1"
assert browse_res.children[0].media_class == "album"
assert browse_res.children[0].media_content_id == "album1 content id"
assert browse_res.children[0].media_content_type == "album"
assert browse_res.children[1].title == "Album 2"
assert browse_res.children[1].media_class == "album"
assert browse_res.children[1].media_content_id == "album2 content id"
assert browse_res.children[1].media_content_type == "album"
async def test_group_members_available_when_off(hass: HomeAssistant) -> None:
"""Test that group_members are still available when media_player is off."""
await async_setup_component(