Browse media support in universal media player (#85668)

Allow forward and override browse media in universal media player
This commit is contained in:
Artem Draft 2023-02-23 11:17:46 +03:00 committed by GitHub
parent 27ebee1501
commit 6474297d1f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 128 additions and 49 deletions

View file

@ -45,6 +45,7 @@ from homeassistant.components.media_player import (
MediaPlayerEntityFeature, MediaPlayerEntityFeature,
MediaPlayerState, MediaPlayerState,
) )
from homeassistant.components.media_player.browse_media import BrowseMedia
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_ENTITY_PICTURE, ATTR_ENTITY_PICTURE,
@ -78,6 +79,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
TrackTemplate, TrackTemplate,
@ -93,6 +95,7 @@ ATTR_ACTIVE_CHILD = "active_child"
CONF_ATTRS = "attributes" CONF_ATTRS = "attributes"
CONF_CHILDREN = "children" CONF_CHILDREN = "children"
CONF_COMMANDS = "commands" CONF_COMMANDS = "commands"
CONF_BROWSE_MEDIA_ENTITY = "browse_media_entity"
STATES_ORDER = [ STATES_ORDER = [
STATE_UNKNOWN, STATE_UNKNOWN,
@ -119,6 +122,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_ATTRS, default={}): vol.Or( vol.Optional(CONF_ATTRS, default={}): vol.Or(
cv.ensure_list(ATTRS_SCHEMA), ATTRS_SCHEMA cv.ensure_list(ATTRS_SCHEMA), ATTRS_SCHEMA
), ),
vol.Optional(CONF_BROWSE_MEDIA_ENTITY): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_TEMPLATE): cv.template,
@ -136,17 +140,7 @@ async def async_setup_platform(
"""Set up the universal media players.""" """Set up the universal media players."""
await async_setup_reload_service(hass, "universal", ["media_player"]) await async_setup_reload_service(hass, "universal", ["media_player"])
player = UniversalMediaPlayer( player = UniversalMediaPlayer(hass, config)
hass,
config.get(CONF_NAME),
config.get(CONF_CHILDREN),
config.get(CONF_COMMANDS),
config.get(CONF_ATTRS),
config.get(CONF_UNIQUE_ID),
config.get(CONF_DEVICE_CLASS),
config.get(CONF_STATE_TEMPLATE),
)
async_add_entities([player]) async_add_entities([player])
@ -158,30 +152,25 @@ class UniversalMediaPlayer(MediaPlayerEntity):
def __init__( def __init__(
self, self,
hass, hass,
name, config,
children,
commands,
attributes,
unique_id=None,
device_class=None,
state_template=None,
): ):
"""Initialize the Universal media device.""" """Initialize the Universal media device."""
self.hass = hass self.hass = hass
self._name = name self._name = config.get(CONF_NAME)
self._children = children self._children = config.get(CONF_CHILDREN)
self._cmds = commands self._cmds = config.get(CONF_COMMANDS)
self._attrs = {} self._attrs = {}
for key, val in attributes.items(): for key, val in config.get(CONF_ATTRS).items():
attr = list(map(str.strip, val.split("|", 1))) attr = list(map(str.strip, val.split("|", 1)))
if len(attr) == 1: if len(attr) == 1:
attr.append(None) attr.append(None)
self._attrs[key] = attr self._attrs[key] = attr
self._child_state = None self._child_state = None
self._state_template_result = None self._state_template_result = None
self._state_template = state_template self._state_template = config.get(CONF_STATE_TEMPLATE)
self._device_class = device_class self._device_class = config.get(CONF_DEVICE_CLASS)
self._attr_unique_id = unique_id self._attr_unique_id = config.get(CONF_UNIQUE_ID)
self._browse_media_entity = config.get(CONF_BROWSE_MEDIA_ENTITY)
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Subscribe to children and template state changes.""" """Subscribe to children and template state changes."""
@ -497,6 +486,9 @@ class UniversalMediaPlayer(MediaPlayerEntity):
if SERVICE_PLAY_MEDIA in self._cmds: if SERVICE_PLAY_MEDIA in self._cmds:
flags |= MediaPlayerEntityFeature.PLAY_MEDIA flags |= MediaPlayerEntityFeature.PLAY_MEDIA
if self._browse_media_entity:
flags |= MediaPlayerEntityFeature.BROWSE_MEDIA
if SERVICE_CLEAR_PLAYLIST in self._cmds: if SERVICE_CLEAR_PLAYLIST in self._cmds:
flags |= MediaPlayerEntityFeature.CLEAR_PLAYLIST flags |= MediaPlayerEntityFeature.CLEAR_PLAYLIST
@ -628,6 +620,20 @@ class UniversalMediaPlayer(MediaPlayerEntity):
# Delegate to turn_on or turn_off by default # Delegate to turn_on or turn_off by default
await super().async_toggle() await super().async_toggle()
async def async_browse_media(
self,
media_content_type: str | None = None,
media_content_id: str | None = None,
) -> BrowseMedia:
"""Return a BrowseMedia instance."""
entity_id = self._browse_media_entity
if not entity_id and self._child_state:
entity_id = self._child_state.entity_id
component: EntityComponent[MediaPlayerEntity] = self.hass.data[DOMAIN]
if entity_id and (entity := component.get_entity(entity_id)):
return await entity.async_browse_media(media_content_type, media_content_id)
raise NotImplementedError()
async def async_update(self) -> None: async def async_update(self) -> None:
"""Update state in HA.""" """Update state in HA."""
self._child_state = None self._child_state = None

View file

@ -9,7 +9,8 @@ from homeassistant import config as hass_config
import homeassistant.components.input_number as input_number import homeassistant.components.input_number as input_number
import homeassistant.components.input_select as input_select import homeassistant.components.input_select as input_select
import homeassistant.components.media_player as media_player import homeassistant.components.media_player as media_player
from homeassistant.components.media_player import MediaPlayerEntityFeature from homeassistant.components.media_player import MediaClass, MediaPlayerEntityFeature
from homeassistant.components.media_player.browse_media import BrowseMedia
import homeassistant.components.switch as switch import homeassistant.components.switch as switch
import homeassistant.components.universal.media_player as universal import homeassistant.components.universal.media_player as universal
from homeassistant.const import ( from homeassistant.const import (
@ -36,6 +37,15 @@ CONFIG_CHILDREN_ONLY = {
], ],
} }
MOCK_BROWSE_MEDIA = BrowseMedia(
media_class=MediaClass.APP,
media_content_id="mock-id",
media_content_type="mock-type",
title="Mock Title",
can_play=False,
can_expand=True,
)
def validate_config(config): def validate_config(config):
"""Use the platform schema to validate configuration.""" """Use the platform schema to validate configuration."""
@ -376,7 +386,7 @@ async def test_master_state(hass: HomeAssistant) -> None:
"""Test master state property.""" """Test master state property."""
config = validate_config(CONFIG_CHILDREN_ONLY) config = validate_config(CONFIG_CHILDREN_ONLY)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
assert ump.master_state is None assert ump.master_state is None
@ -387,7 +397,7 @@ async def test_master_state_with_attrs(
"""Test master state property.""" """Test master state property."""
config = validate_config(config_children_and_attr) config = validate_config(config_children_and_attr)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
assert ump.master_state == STATE_OFF assert ump.master_state == STATE_OFF
hass.states.async_set(mock_states.mock_state_switch_id, STATE_ON) hass.states.async_set(mock_states.mock_state_switch_id, STATE_ON)
@ -402,7 +412,7 @@ async def test_master_state_with_bad_attrs(
config["attributes"]["state"] = "bad.entity_id" config["attributes"]["state"] = "bad.entity_id"
config = validate_config(config) config = validate_config(config)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
assert ump.master_state == STATE_OFF assert ump.master_state == STATE_OFF
@ -411,7 +421,7 @@ async def test_active_child_state(hass: HomeAssistant, mock_states) -> None:
"""Test active child state property.""" """Test active child state property."""
config = validate_config(CONFIG_CHILDREN_ONLY) config = validate_config(CONFIG_CHILDREN_ONLY)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
await ump.async_update() await ump.async_update()
@ -452,7 +462,7 @@ async def test_name(hass: HomeAssistant) -> None:
"""Test name property.""" """Test name property."""
config = validate_config(CONFIG_CHILDREN_ONLY) config = validate_config(CONFIG_CHILDREN_ONLY)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
assert config["name"] == ump.name assert config["name"] == ump.name
@ -461,7 +471,7 @@ async def test_polling(hass: HomeAssistant) -> None:
"""Test should_poll property.""" """Test should_poll property."""
config = validate_config(CONFIG_CHILDREN_ONLY) config = validate_config(CONFIG_CHILDREN_ONLY)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
assert ump.should_poll is False assert ump.should_poll is False
@ -470,7 +480,7 @@ async def test_state_children_only(hass: HomeAssistant, mock_states) -> None:
"""Test media player state with only children.""" """Test media player state with only children."""
config = validate_config(CONFIG_CHILDREN_ONLY) config = validate_config(CONFIG_CHILDREN_ONLY)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
await ump.async_update() await ump.async_update()
@ -489,7 +499,7 @@ async def test_state_with_children_and_attrs(
"""Test media player with children and master state.""" """Test media player with children and master state."""
config = validate_config(config_children_and_attr) config = validate_config(config_children_and_attr)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
await ump.async_update() await ump.async_update()
@ -514,7 +524,7 @@ async def test_volume_level(hass: HomeAssistant, mock_states) -> None:
"""Test volume level property.""" """Test volume level property."""
config = validate_config(CONFIG_CHILDREN_ONLY) config = validate_config(CONFIG_CHILDREN_ONLY)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
await ump.async_update() await ump.async_update()
@ -538,7 +548,7 @@ async def test_media_image_url(hass: HomeAssistant, mock_states) -> None:
test_url = "test_url" test_url = "test_url"
config = validate_config(CONFIG_CHILDREN_ONLY) config = validate_config(CONFIG_CHILDREN_ONLY)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
await ump.async_update() await ump.async_update()
@ -558,7 +568,7 @@ async def test_is_volume_muted_children_only(hass: HomeAssistant, mock_states) -
"""Test is volume muted property w/ children only.""" """Test is volume muted property w/ children only."""
config = validate_config(CONFIG_CHILDREN_ONLY) config = validate_config(CONFIG_CHILDREN_ONLY)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
await ump.async_update() await ump.async_update()
@ -583,7 +593,7 @@ async def test_sound_mode_list_children_and_attr(
"""Test sound mode list property w/ children and attrs.""" """Test sound mode list property w/ children and attrs."""
config = validate_config(config_children_and_attr) config = validate_config(config_children_and_attr)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
assert ump.sound_mode_list == "['music', 'movie']" assert ump.sound_mode_list == "['music', 'movie']"
@ -599,7 +609,7 @@ async def test_source_list_children_and_attr(
"""Test source list property w/ children and attrs.""" """Test source list property w/ children and attrs."""
config = validate_config(config_children_and_attr) config = validate_config(config_children_and_attr)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
assert ump.source_list == "['dvd', 'htpc']" assert ump.source_list == "['dvd', 'htpc']"
@ -613,7 +623,7 @@ async def test_sound_mode_children_and_attr(
"""Test sound modeproperty w/ children and attrs.""" """Test sound modeproperty w/ children and attrs."""
config = validate_config(config_children_and_attr) config = validate_config(config_children_and_attr)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
assert ump.sound_mode == "music" assert ump.sound_mode == "music"
@ -627,7 +637,7 @@ async def test_source_children_and_attr(
"""Test source property w/ children and attrs.""" """Test source property w/ children and attrs."""
config = validate_config(config_children_and_attr) config = validate_config(config_children_and_attr)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
assert ump.source == "dvd" assert ump.source == "dvd"
@ -641,7 +651,7 @@ async def test_volume_level_children_and_attr(
"""Test volume level property w/ children and attrs.""" """Test volume level property w/ children and attrs."""
config = validate_config(config_children_and_attr) config = validate_config(config_children_and_attr)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
assert ump.volume_level == 0 assert ump.volume_level == 0
@ -655,7 +665,7 @@ async def test_is_volume_muted_children_and_attr(
"""Test is volume muted property w/ children and attrs.""" """Test is volume muted property w/ children and attrs."""
config = validate_config(config_children_and_attr) config = validate_config(config_children_and_attr)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
assert not ump.is_volume_muted assert not ump.is_volume_muted
@ -669,7 +679,7 @@ async def test_supported_features_children_only(
"""Test supported media commands with only children.""" """Test supported media commands with only children."""
config = validate_config(CONFIG_CHILDREN_ONLY) config = validate_config(CONFIG_CHILDREN_ONLY)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
await ump.async_update() await ump.async_update()
@ -709,9 +719,10 @@ async def test_supported_features_children_and_cmds(
"play_media": excmd, "play_media": excmd,
"clear_playlist": excmd, "clear_playlist": excmd,
} }
config["browse_media_entity"] = "media_player.test"
config = validate_config(config) config = validate_config(config)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
await ump.async_update() await ump.async_update()
@ -737,6 +748,7 @@ async def test_supported_features_children_and_cmds(
| MediaPlayerEntityFeature.PREVIOUS_TRACK | MediaPlayerEntityFeature.PREVIOUS_TRACK
| MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.PLAY_MEDIA
| MediaPlayerEntityFeature.CLEAR_PLAYLIST | MediaPlayerEntityFeature.CLEAR_PLAYLIST
| MediaPlayerEntityFeature.BROWSE_MEDIA
) )
assert check_flags == ump.supported_features assert check_flags == ump.supported_features
@ -926,7 +938,7 @@ async def test_supported_features_play_pause(
config["commands"] = {"media_play_pause": excmd} config["commands"] = {"media_play_pause": excmd}
config = validate_config(config) config = validate_config(config)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
await ump.async_update() await ump.async_update()
@ -946,7 +958,7 @@ async def test_service_call_no_active_child(
"""Test a service call to children with no active child.""" """Test a service call to children with no active child."""
config = validate_config(config_children_and_attr) config = validate_config(config_children_and_attr)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
await ump.async_update() await ump.async_update()
@ -966,7 +978,7 @@ async def test_service_call_to_child(hass: HomeAssistant, mock_states) -> None:
"""Test service calls that should be routed to a child.""" """Test service calls that should be routed to a child."""
config = validate_config(CONFIG_CHILDREN_ONLY) config = validate_config(CONFIG_CHILDREN_ONLY)
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
await ump.async_update() await ump.async_update()
@ -1045,7 +1057,7 @@ async def test_service_call_to_command(hass: HomeAssistant, mock_states) -> None
service = async_mock_service(hass, "test", "turn_off") service = async_mock_service(hass, "test", "turn_off")
ump = universal.UniversalMediaPlayer(hass, **config) ump = universal.UniversalMediaPlayer(hass, config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
await ump.async_update() await ump.async_update()
@ -1084,6 +1096,67 @@ async def test_state_template(hass: HomeAssistant) -> None:
assert hass.states.get("media_player.tv").state == STATE_OFF assert hass.states.get("media_player.tv").state == STATE_OFF
async def test_browse_media(hass: HomeAssistant):
"""Test browse media."""
await async_setup_component(
hass, "media_player", {"media_player": {"platform": "demo"}}
)
await hass.async_block_till_done()
config = {
"name": "test",
"platform": "universal",
"children": [
"media_player.bedroom",
],
}
config = validate_config(config)
ump = universal.UniversalMediaPlayer(hass, config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
await ump.async_update()
with patch(
"homeassistant.components.demo.media_player.MediaPlayerEntity.supported_features",
MediaPlayerEntityFeature.BROWSE_MEDIA,
), patch(
"homeassistant.components.demo.media_player.MediaPlayerEntity.async_browse_media",
return_value=MOCK_BROWSE_MEDIA,
):
result = await ump.async_browse_media()
assert result == MOCK_BROWSE_MEDIA
async def test_browse_media_override(hass: HomeAssistant):
"""Test browse media override."""
await async_setup_component(
hass, "media_player", {"media_player": {"platform": "demo"}}
)
await hass.async_block_till_done()
config = {
"name": "test",
"platform": "universal",
"children": [
"media_player.mock1",
],
"browse_media_entity": "media_player.bedroom",
}
config = validate_config(config)
ump = universal.UniversalMediaPlayer(hass, config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"])
await ump.async_update()
with patch(
"homeassistant.components.demo.media_player.MediaPlayerEntity.supported_features",
MediaPlayerEntityFeature.BROWSE_MEDIA,
), patch(
"homeassistant.components.demo.media_player.MediaPlayerEntity.async_browse_media",
return_value=MOCK_BROWSE_MEDIA,
):
result = await ump.async_browse_media()
assert result == MOCK_BROWSE_MEDIA
async def test_device_class(hass: HomeAssistant) -> None: async def test_device_class(hass: HomeAssistant) -> None:
"""Test device_class property.""" """Test device_class property."""
hass.states.async_set("sensor.test_sensor", "on") hass.states.async_set("sensor.test_sensor", "on")