mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Add definitions for grouping media players (#41193)
* Add definitions for grouping media players See https://github.com/home-assistant/architecture/issues/364 * Fix Google Assistant tests * Define sync versions of async_join_players/async_unjoin * Don't use async API in synchronous test methods * Fix tests and make pylint happy The method name `unjoin` is used by another component, so let's use `unjoin_player` instead. * Fix emulated_hue tests The new media player entity in the `demo` component requires a tiny adjustment in the emulated_hue tests. * Use "target:" in service description * Also use "name:" in service descriptions Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
parent
9fca001eed
commit
5174f63fd8
@ -6,6 +6,7 @@ from homeassistant.components.media_player.const import (
|
||||
MEDIA_TYPE_TVSHOW,
|
||||
REPEAT_MODE_OFF,
|
||||
SUPPORT_CLEAR_PLAYLIST,
|
||||
SUPPORT_GROUPING,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_PLAY,
|
||||
@ -40,6 +41,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||
"Bedroom", "kxopViU98Xo", "Epic sax guy 10 hours", 360000
|
||||
),
|
||||
DemoMusicPlayer(),
|
||||
DemoMusicPlayer("Kitchen"),
|
||||
DemoTVShowPlayer(),
|
||||
]
|
||||
)
|
||||
@ -73,6 +75,7 @@ MUSIC_PLAYER_SUPPORT = (
|
||||
| SUPPORT_TURN_ON
|
||||
| SUPPORT_TURN_OFF
|
||||
| SUPPORT_CLEAR_PLAYLIST
|
||||
| SUPPORT_GROUPING
|
||||
| SUPPORT_PLAY
|
||||
| SUPPORT_SHUFFLE_SET
|
||||
| SUPPORT_REPEAT_SET
|
||||
@ -291,7 +294,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
|
||||
|
||||
|
||||
class DemoMusicPlayer(AbstractDemoPlayer):
|
||||
"""A Demo media player that only supports YouTube."""
|
||||
"""A Demo media player."""
|
||||
|
||||
# We only implement the methods that we support
|
||||
|
||||
@ -318,12 +321,18 @@ class DemoMusicPlayer(AbstractDemoPlayer):
|
||||
),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, name="Walkman"):
|
||||
"""Initialize the demo device."""
|
||||
super().__init__("Walkman")
|
||||
super().__init__(name)
|
||||
self._cur_track = 0
|
||||
self._group_members = []
|
||||
self._repeat = REPEAT_MODE_OFF
|
||||
|
||||
@property
|
||||
def group_members(self):
|
||||
"""List of players which are currently grouped together."""
|
||||
return self._group_members
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
"""Return the content ID of current playing media."""
|
||||
@ -398,6 +407,18 @@ class DemoMusicPlayer(AbstractDemoPlayer):
|
||||
self._repeat = repeat
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def join_players(self, group_members):
|
||||
"""Join `group_members` as a player group with the current player."""
|
||||
self._group_members = [
|
||||
self.entity_id,
|
||||
] + group_members
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def unjoin_player(self):
|
||||
"""Remove this player from any group."""
|
||||
self._group_members = []
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
class DemoTVShowPlayer(AbstractDemoPlayer):
|
||||
"""A Demo media player that only supports YouTube."""
|
||||
|
@ -64,6 +64,7 @@ from homeassistant.loader import bind_hass
|
||||
from .const import (
|
||||
ATTR_APP_ID,
|
||||
ATTR_APP_NAME,
|
||||
ATTR_GROUP_MEMBERS,
|
||||
ATTR_INPUT_SOURCE,
|
||||
ATTR_INPUT_SOURCE_LIST,
|
||||
ATTR_MEDIA_ALBUM_ARTIST,
|
||||
@ -94,11 +95,14 @@ from .const import (
|
||||
MEDIA_CLASS_DIRECTORY,
|
||||
REPEAT_MODES,
|
||||
SERVICE_CLEAR_PLAYLIST,
|
||||
SERVICE_JOIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
SERVICE_SELECT_SOUND_MODE,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
SERVICE_UNJOIN,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
SUPPORT_CLEAR_PLAYLIST,
|
||||
SUPPORT_GROUPING,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_PLAY,
|
||||
@ -299,6 +303,12 @@ async def async_setup(hass, config):
|
||||
"async_media_seek",
|
||||
[SUPPORT_SEEK],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_JOIN,
|
||||
{vol.Required(ATTR_GROUP_MEMBERS): list},
|
||||
"async_join_players",
|
||||
[SUPPORT_GROUPING],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SELECT_SOURCE,
|
||||
{vol.Required(ATTR_INPUT_SOURCE): cv.string},
|
||||
@ -330,6 +340,9 @@ async def async_setup(hass, config):
|
||||
"async_set_shuffle",
|
||||
[SUPPORT_SHUFFLE_SET],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_UNJOIN, {}, "async_unjoin_player", [SUPPORT_GROUPING]
|
||||
)
|
||||
|
||||
component.async_register_entity_service(
|
||||
SERVICE_REPEAT_SET,
|
||||
@ -537,6 +550,11 @@ class MediaPlayerEntity(Entity):
|
||||
"""Return current repeat mode."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def group_members(self):
|
||||
"""List of members which are currently grouped together."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag media player features that are supported."""
|
||||
@ -738,6 +756,11 @@ class MediaPlayerEntity(Entity):
|
||||
"""Boolean if shuffle is supported."""
|
||||
return bool(self.supported_features & SUPPORT_SHUFFLE_SET)
|
||||
|
||||
@property
|
||||
def support_grouping(self):
|
||||
"""Boolean if player grouping is supported."""
|
||||
return bool(self.supported_features & SUPPORT_GROUPING)
|
||||
|
||||
async def async_toggle(self):
|
||||
"""Toggle the power on the media player."""
|
||||
if hasattr(self, "toggle"):
|
||||
@ -846,6 +869,9 @@ class MediaPlayerEntity(Entity):
|
||||
if self.media_image_remotely_accessible:
|
||||
state_attr["entity_picture_local"] = self.media_image_local
|
||||
|
||||
if self.support_grouping:
|
||||
state_attr[ATTR_GROUP_MEMBERS] = self.group_members
|
||||
|
||||
return state_attr
|
||||
|
||||
async def async_browse_media(
|
||||
@ -860,6 +886,22 @@ class MediaPlayerEntity(Entity):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def join_players(self, group_members):
|
||||
"""Join `group_members` as a player group with the current player."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_join_players(self, group_members):
|
||||
"""Join `group_members` as a player group with the current player."""
|
||||
await self.hass.async_add_executor_job(self.join_players, group_members)
|
||||
|
||||
def unjoin_player(self):
|
||||
"""Remove this player from any group."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_unjoin_player(self):
|
||||
"""Remove this player from any group."""
|
||||
await self.hass.async_add_executor_job(self.unjoin_player)
|
||||
|
||||
async def _async_fetch_image_from_cache(self, url):
|
||||
"""Fetch image.
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
ATTR_APP_ID = "app_id"
|
||||
ATTR_APP_NAME = "app_name"
|
||||
ATTR_GROUP_MEMBERS = "group_members"
|
||||
ATTR_INPUT_SOURCE = "source"
|
||||
ATTR_INPUT_SOURCE_LIST = "source_list"
|
||||
ATTR_MEDIA_ALBUM_ARTIST = "media_album_artist"
|
||||
@ -75,9 +76,11 @@ MEDIA_TYPE_URL = "url"
|
||||
MEDIA_TYPE_VIDEO = "video"
|
||||
|
||||
SERVICE_CLEAR_PLAYLIST = "clear_playlist"
|
||||
SERVICE_JOIN = "join"
|
||||
SERVICE_PLAY_MEDIA = "play_media"
|
||||
SERVICE_SELECT_SOUND_MODE = "select_sound_mode"
|
||||
SERVICE_SELECT_SOURCE = "select_source"
|
||||
SERVICE_UNJOIN = "unjoin"
|
||||
|
||||
REPEAT_MODE_ALL = "all"
|
||||
REPEAT_MODE_OFF = "off"
|
||||
@ -103,3 +106,4 @@ SUPPORT_SHUFFLE_SET = 32768
|
||||
SUPPORT_SELECT_SOUND_MODE = 65536
|
||||
SUPPORT_BROWSE_MEDIA = 131072
|
||||
SUPPORT_REPEAT_SET = 262144
|
||||
SUPPORT_GROUPING = 524288
|
||||
|
@ -118,8 +118,8 @@ play_media:
|
||||
media_content_type:
|
||||
name: Content type
|
||||
description:
|
||||
The type of the content to play. Like image, music, tvshow,
|
||||
video, episode, channel or playlist.
|
||||
The type of the content to play. Like image, music, tvshow, video,
|
||||
episode, channel or playlist.
|
||||
required: true
|
||||
example: "music"
|
||||
selector:
|
||||
@ -184,3 +184,24 @@ repeat_set:
|
||||
- "off"
|
||||
- "all"
|
||||
- "one"
|
||||
|
||||
join:
|
||||
description:
|
||||
Group players together. Only works on platforms with support for player
|
||||
groups.
|
||||
name: Join
|
||||
target:
|
||||
fields:
|
||||
group_members:
|
||||
description:
|
||||
The players which will be synced with the target player.
|
||||
example:
|
||||
- "media_player.multiroom_player2"
|
||||
- "media_player.multiroom_player3"
|
||||
|
||||
unjoin:
|
||||
description:
|
||||
Unjoin the player from a group. Only works on platforms with support for
|
||||
player groups.
|
||||
name: Unjoin
|
||||
target:
|
||||
|
@ -442,3 +442,39 @@ async def test_media_image_proxy(hass, hass_client):
|
||||
req = await client.get(state.attributes.get(ATTR_ENTITY_PICTURE))
|
||||
assert req.status == 200
|
||||
assert await req.text() == fake_picture_data
|
||||
|
||||
|
||||
async def test_grouping(hass):
|
||||
"""Test the join/unjoin services."""
|
||||
walkman = "media_player.walkman"
|
||||
kitchen = "media_player.kitchen"
|
||||
|
||||
assert await async_setup_component(
|
||||
hass, mp.DOMAIN, {"media_player": {"platform": "demo"}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(walkman)
|
||||
assert state.attributes.get(mp.ATTR_GROUP_MEMBERS) == []
|
||||
|
||||
await hass.services.async_call(
|
||||
mp.DOMAIN,
|
||||
mp.SERVICE_JOIN,
|
||||
{
|
||||
ATTR_ENTITY_ID: walkman,
|
||||
mp.ATTR_GROUP_MEMBERS: [
|
||||
kitchen,
|
||||
],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
state = hass.states.get(walkman)
|
||||
assert state.attributes.get(mp.ATTR_GROUP_MEMBERS) == [walkman, kitchen]
|
||||
|
||||
await hass.services.async_call(
|
||||
mp.DOMAIN,
|
||||
mp.SERVICE_UNJOIN,
|
||||
{ATTR_ENTITY_ID: walkman},
|
||||
blocking=True,
|
||||
)
|
||||
state = hass.states.get(walkman)
|
||||
assert state.attributes.get(mp.ATTR_GROUP_MEMBERS) == []
|
||||
|
@ -90,6 +90,7 @@ ENTITY_IDS_BY_NUMBER = {
|
||||
"21": "humidifier.hygrostat",
|
||||
"22": "scene.light_on",
|
||||
"23": "scene.light_off",
|
||||
"24": "media_player.kitchen",
|
||||
}
|
||||
|
||||
ENTITY_NUMBERS_BY_ID = {v: k for k, v in ENTITY_IDS_BY_NUMBER.items()}
|
||||
|
@ -192,6 +192,19 @@ DEMO_DEVICES = [
|
||||
"type": "action.devices.types.SETTOP",
|
||||
"willReportState": False,
|
||||
},
|
||||
{
|
||||
"id": "media_player.kitchen",
|
||||
"name": {"name": "Kitchen"},
|
||||
"traits": [
|
||||
"action.devices.traits.OnOff",
|
||||
"action.devices.traits.Volume",
|
||||
"action.devices.traits.Modes",
|
||||
"action.devices.traits.TransportControl",
|
||||
"action.devices.traits.MediaState",
|
||||
],
|
||||
"type": "action.devices.types.SETTOP",
|
||||
"willReportState": False,
|
||||
},
|
||||
{
|
||||
"id": "media_player.living_room",
|
||||
"name": {"name": "Living Room"},
|
||||
|
Loading…
x
Reference in New Issue
Block a user