mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Check for supported features in media_player services (#22878)
* Add check for supported features * Move logic to service helper * Fix hacked in test for seek * Test for service required features
This commit is contained in:
parent
fc7a187dd6
commit
7624d0e79f
@ -1,14 +1,13 @@
|
||||
"""Demo implementation of the media player."""
|
||||
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerDevice
|
||||
from homeassistant.components.media_player.const import (
|
||||
MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
|
||||
SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
|
||||
SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOUND_MODE,
|
||||
SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_TURN_OFF,
|
||||
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET)
|
||||
SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||
SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET,
|
||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET)
|
||||
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
@ -30,7 +29,8 @@ DEFAULT_SOUND_MODE = 'Dummy Music'
|
||||
YOUTUBE_PLAYER_SUPPORT = \
|
||||
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | \
|
||||
SUPPORT_SHUFFLE_SET | SUPPORT_SELECT_SOUND_MODE | SUPPORT_SELECT_SOURCE
|
||||
SUPPORT_SHUFFLE_SET | SUPPORT_SELECT_SOUND_MODE | SUPPORT_SELECT_SOURCE | \
|
||||
SUPPORT_SEEK
|
||||
|
||||
MUSIC_PLAYER_SUPPORT = \
|
||||
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
|
@ -32,51 +32,20 @@ from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .const import (
|
||||
ATTR_APP_ID,
|
||||
ATTR_APP_NAME,
|
||||
ATTR_INPUT_SOURCE,
|
||||
ATTR_INPUT_SOURCE_LIST,
|
||||
ATTR_MEDIA_ALBUM_ARTIST,
|
||||
ATTR_MEDIA_ALBUM_NAME,
|
||||
ATTR_MEDIA_ARTIST,
|
||||
ATTR_MEDIA_CHANNEL,
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE,
|
||||
ATTR_MEDIA_DURATION,
|
||||
ATTR_MEDIA_ENQUEUE,
|
||||
ATTR_MEDIA_EPISODE,
|
||||
ATTR_MEDIA_PLAYLIST,
|
||||
ATTR_MEDIA_POSITION,
|
||||
ATTR_MEDIA_POSITION_UPDATED_AT,
|
||||
ATTR_MEDIA_SEASON,
|
||||
ATTR_MEDIA_SEEK_POSITION,
|
||||
ATTR_MEDIA_SERIES_TITLE,
|
||||
ATTR_MEDIA_SHUFFLE,
|
||||
ATTR_MEDIA_TITLE,
|
||||
ATTR_MEDIA_TRACK,
|
||||
ATTR_MEDIA_VOLUME_LEVEL,
|
||||
ATTR_MEDIA_VOLUME_MUTED,
|
||||
ATTR_SOUND_MODE,
|
||||
ATTR_SOUND_MODE_LIST,
|
||||
DOMAIN,
|
||||
SERVICE_CLEAR_PLAYLIST,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
SERVICE_SELECT_SOUND_MODE,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_SEEK,
|
||||
SUPPORT_VOLUME_SET,
|
||||
SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PLAY_MEDIA,
|
||||
SUPPORT_SELECT_SOURCE,
|
||||
SUPPORT_STOP,
|
||||
SUPPORT_CLEAR_PLAYLIST,
|
||||
SUPPORT_PLAY,
|
||||
SUPPORT_SHUFFLE_SET,
|
||||
SUPPORT_SELECT_SOUND_MODE,
|
||||
)
|
||||
ATTR_APP_ID, ATTR_APP_NAME, ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST,
|
||||
ATTR_MEDIA_ALBUM_ARTIST, ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ARTIST,
|
||||
ATTR_MEDIA_CHANNEL, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE,
|
||||
ATTR_MEDIA_DURATION, ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_EPISODE,
|
||||
ATTR_MEDIA_PLAYLIST, ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT,
|
||||
ATTR_MEDIA_SEASON, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_SERIES_TITLE,
|
||||
ATTR_MEDIA_SHUFFLE, ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK,
|
||||
ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_SOUND_MODE,
|
||||
ATTR_SOUND_MODE_LIST, DOMAIN, SERVICE_CLEAR_PLAYLIST, SERVICE_PLAY_MEDIA,
|
||||
SERVICE_SELECT_SOUND_MODE, SERVICE_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST,
|
||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOUND_MODE,
|
||||
SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_TURN_OFF,
|
||||
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET)
|
||||
from .reproduce_state import async_reproduce_states # noqa
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -197,74 +166,77 @@ async def async_setup(hass, config):
|
||||
|
||||
component.async_register_entity_service(
|
||||
SERVICE_TURN_ON, MEDIA_PLAYER_SCHEMA,
|
||||
'async_turn_on'
|
||||
'async_turn_on', SUPPORT_TURN_ON
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_TURN_OFF, MEDIA_PLAYER_SCHEMA,
|
||||
'async_turn_off'
|
||||
'async_turn_off', SUPPORT_TURN_OFF
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_TOGGLE, MEDIA_PLAYER_SCHEMA,
|
||||
'async_toggle'
|
||||
'async_toggle', SUPPORT_TURN_OFF | SUPPORT_TURN_ON
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_VOLUME_UP, MEDIA_PLAYER_SCHEMA,
|
||||
'async_volume_up'
|
||||
'async_volume_up', SUPPORT_VOLUME_SET
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_VOLUME_DOWN, MEDIA_PLAYER_SCHEMA,
|
||||
'async_volume_down'
|
||||
'async_volume_down', SUPPORT_VOLUME_SET
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_MEDIA_PLAY_PAUSE, MEDIA_PLAYER_SCHEMA,
|
||||
'async_media_play_pause'
|
||||
'async_media_play_pause', SUPPORT_PLAY | SUPPORT_PAUSE
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_MEDIA_PLAY, MEDIA_PLAYER_SCHEMA,
|
||||
'async_media_play'
|
||||
'async_media_play', SUPPORT_PLAY
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_MEDIA_PAUSE, MEDIA_PLAYER_SCHEMA,
|
||||
'async_media_pause'
|
||||
'async_media_pause', SUPPORT_PAUSE
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_MEDIA_STOP, MEDIA_PLAYER_SCHEMA,
|
||||
'async_media_stop'
|
||||
'async_media_stop', SUPPORT_STOP
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_MEDIA_NEXT_TRACK, MEDIA_PLAYER_SCHEMA,
|
||||
'async_media_next_track'
|
||||
'async_media_next_track', SUPPORT_NEXT_TRACK
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK, MEDIA_PLAYER_SCHEMA,
|
||||
'async_media_previous_track'
|
||||
'async_media_previous_track', SUPPORT_PREVIOUS_TRACK
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_CLEAR_PLAYLIST, MEDIA_PLAYER_SCHEMA,
|
||||
'async_clear_playlist'
|
||||
'async_clear_playlist', SUPPORT_CLEAR_PLAYLIST
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_VOLUME_SET, MEDIA_PLAYER_SET_VOLUME_SCHEMA,
|
||||
lambda entity, call: entity.async_set_volume_level(
|
||||
volume=call.data[ATTR_MEDIA_VOLUME_LEVEL])
|
||||
volume=call.data[ATTR_MEDIA_VOLUME_LEVEL]),
|
||||
SUPPORT_VOLUME_SET
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_VOLUME_MUTE, MEDIA_PLAYER_MUTE_VOLUME_SCHEMA,
|
||||
lambda entity, call: entity.async_mute_volume(
|
||||
mute=call.data[ATTR_MEDIA_VOLUME_MUTED])
|
||||
mute=call.data[ATTR_MEDIA_VOLUME_MUTED]),
|
||||
SUPPORT_VOLUME_MUTE
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_MEDIA_SEEK, MEDIA_PLAYER_MEDIA_SEEK_SCHEMA,
|
||||
lambda entity, call: entity.async_media_seek(
|
||||
position=call.data[ATTR_MEDIA_SEEK_POSITION])
|
||||
position=call.data[ATTR_MEDIA_SEEK_POSITION]),
|
||||
SUPPORT_SEEK
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SELECT_SOURCE, MEDIA_PLAYER_SELECT_SOURCE_SCHEMA,
|
||||
'async_select_source'
|
||||
'async_select_source', SUPPORT_SELECT_SOURCE
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SELECT_SOUND_MODE, MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA,
|
||||
'async_select_sound_mode'
|
||||
'async_select_sound_mode', SUPPORT_SELECT_SOUND_MODE
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_PLAY_MEDIA, MEDIA_PLAYER_PLAY_MEDIA_SCHEMA,
|
||||
@ -272,11 +244,11 @@ async def async_setup(hass, config):
|
||||
media_type=call.data[ATTR_MEDIA_CONTENT_TYPE],
|
||||
media_id=call.data[ATTR_MEDIA_CONTENT_ID],
|
||||
enqueue=call.data.get(ATTR_MEDIA_ENQUEUE)
|
||||
)
|
||||
), SUPPORT_PLAY_MEDIA
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SHUFFLE_SET, MEDIA_PLAYER_SET_SHUFFLE_SCHEMA,
|
||||
'async_set_shuffle'
|
||||
'async_set_shuffle', SUPPORT_SHUFFLE_SET
|
||||
)
|
||||
|
||||
return True
|
||||
|
@ -179,13 +179,15 @@ class EntityComponent:
|
||||
if entity.available and entity.entity_id in entity_ids]
|
||||
|
||||
@callback
|
||||
def async_register_entity_service(self, name, schema, func):
|
||||
def async_register_entity_service(self, name, schema, func,
|
||||
required_features=None):
|
||||
"""Register an entity service."""
|
||||
async def handle_service(call):
|
||||
"""Handle the service."""
|
||||
service_name = "{}.{}".format(self.domain, name)
|
||||
await self.hass.helpers.service.entity_service_call(
|
||||
self._platforms.values(), func, call, service_name
|
||||
self._platforms.values(), func, call, service_name,
|
||||
required_features
|
||||
)
|
||||
|
||||
self.hass.services.async_register(
|
||||
|
@ -216,7 +216,8 @@ async def async_get_all_descriptions(hass):
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def entity_service_call(hass, platforms, func, call, service_name=''):
|
||||
async def entity_service_call(hass, platforms, func, call, service_name='',
|
||||
required_features=None):
|
||||
"""Handle an entity service call.
|
||||
|
||||
Calls all platforms simultaneously.
|
||||
@ -295,7 +296,8 @@ async def entity_service_call(hass, platforms, func, call, service_name=''):
|
||||
platforms_entities.append(platform_entities)
|
||||
|
||||
tasks = [
|
||||
_handle_service_platform_call(func, data, entities, call.context)
|
||||
_handle_service_platform_call(func, data, entities, call.context,
|
||||
required_features)
|
||||
for platform, entities in zip(platforms, platforms_entities)
|
||||
]
|
||||
|
||||
@ -306,7 +308,8 @@ async def entity_service_call(hass, platforms, func, call, service_name=''):
|
||||
future.result() # pop exception if have
|
||||
|
||||
|
||||
async def _handle_service_platform_call(func, data, entities, context):
|
||||
async def _handle_service_platform_call(func, data, entities, context,
|
||||
required_features):
|
||||
"""Handle a function call."""
|
||||
tasks = []
|
||||
|
||||
@ -314,6 +317,11 @@ async def _handle_service_platform_call(func, data, entities, context):
|
||||
if not entity.available:
|
||||
continue
|
||||
|
||||
# Skip entities that don't have the required feature.
|
||||
if required_features is not None \
|
||||
and not entity.supported_features & required_features:
|
||||
continue
|
||||
|
||||
entity.async_set_context(context)
|
||||
|
||||
if isinstance(func, str):
|
||||
|
@ -184,9 +184,7 @@ class TestDemoMediaPlayer(unittest.TestCase):
|
||||
state = self.hass.states.get(ent_id)
|
||||
assert 1 == state.attributes.get('media_episode')
|
||||
|
||||
@patch('homeassistant.components.demo.media_player.DemoYoutubePlayer.'
|
||||
'media_seek', autospec=True)
|
||||
def test_play_media(self, mock_seek):
|
||||
def test_play_media(self):
|
||||
"""Test play_media ."""
|
||||
assert setup_component(
|
||||
self.hass, mp.DOMAIN,
|
||||
@ -212,6 +210,16 @@ class TestDemoMediaPlayer(unittest.TestCase):
|
||||
state.attributes.get('supported_features'))
|
||||
assert 'some_id' == state.attributes.get('media_content_id')
|
||||
|
||||
@patch('homeassistant.components.demo.media_player.DemoYoutubePlayer.'
|
||||
'media_seek', autospec=True)
|
||||
def test_seek(self, mock_seek):
|
||||
"""Test seek."""
|
||||
assert setup_component(
|
||||
self.hass, mp.DOMAIN,
|
||||
{'media_player': {'platform': 'demo'}})
|
||||
ent_id = 'media_player.living_room'
|
||||
state = self.hass.states.get(ent_id)
|
||||
assert state.attributes['supported_features'] & mp.SUPPORT_SEEK
|
||||
assert not mock_seek.called
|
||||
with pytest.raises(vol.Invalid):
|
||||
common.media_seek(self.hass, None, ent_id)
|
||||
|
@ -37,11 +37,13 @@ def mock_entities():
|
||||
entity_id='light.kitchen',
|
||||
available=True,
|
||||
should_poll=False,
|
||||
supported_features=1,
|
||||
)
|
||||
living_room = Mock(
|
||||
entity_id='light.living_room',
|
||||
available=True,
|
||||
should_poll=False,
|
||||
supported_features=0,
|
||||
)
|
||||
entities = OrderedDict()
|
||||
entities[kitchen.entity_id] = kitchen
|
||||
@ -269,6 +271,19 @@ def test_async_get_all_descriptions(hass):
|
||||
assert 'fields' in descriptions[logger.DOMAIN]['set_level']
|
||||
|
||||
|
||||
async def test_call_with_required_features(hass, mock_entities):
|
||||
"""Test service calls invoked only if entity has required feautres."""
|
||||
test_service_mock = Mock(return_value=mock_coro())
|
||||
await service.entity_service_call(hass, [
|
||||
Mock(entities=mock_entities)
|
||||
], test_service_mock, ha.ServiceCall('test_domain', 'test_service', {
|
||||
'entity_id': 'all'
|
||||
}), required_features=1)
|
||||
assert len(mock_entities) == 2
|
||||
# Called once because only one of the entities had the required features
|
||||
assert test_service_mock.call_count == 1
|
||||
|
||||
|
||||
async def test_call_context_user_not_exist(hass):
|
||||
"""Check we don't allow deleted users to do things."""
|
||||
with pytest.raises(exceptions.UnknownUser) as err:
|
||||
|
Loading…
x
Reference in New Issue
Block a user