Skip to content

Commit

Permalink
Check for supported features in media_player services (#22878)
Browse files Browse the repository at this point in the history
* Add check for supported features

* Move logic to service helper

* Fix hacked in test for seek

* Test for service required features
  • Loading branch information
andrewsayre authored and balloob committed Apr 10, 2019
1 parent fc7a187 commit 7624d0e
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 79 deletions.
14 changes: 7 additions & 7 deletions homeassistant/components/demo/media_player.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -30,7 +29,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
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 | \
Expand Down
100 changes: 36 additions & 64 deletions homeassistant/components/media_player/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,51 +32,20 @@
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__)
Expand Down Expand Up @@ -197,86 +166,89 @@ 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,
lambda entity, call: entity.async_play_media(
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
Expand Down
6 changes: 4 additions & 2 deletions homeassistant/helpers/entity_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,15 @@ async def async_extract_from_service(self, service, expand_group=True):
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(
Expand Down
14 changes: 11 additions & 3 deletions homeassistant/helpers/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,8 @@ def load_services_files(yaml_files):


@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.
Expand Down Expand Up @@ -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)
]

Expand All @@ -306,14 +308,20 @@ 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 = []

for entity in entities:
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:

This comment has been minimized.

Copy link
@Villhellm

Villhellm Apr 19, 2019

Contributor

This throws an unsupported operand error when supported_features are not an attribute of the entity. This is the case for plex media players when they are idle.

This comment has been minimized.

Copy link
@balloob

balloob Apr 19, 2019

Member

Open an issue.

continue

entity.async_set_context(context)

if isinstance(func, str):
Expand Down
14 changes: 11 additions & 3 deletions tests/components/demo/test_media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,7 @@ def test_prev_next_track(self):
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,
Expand All @@ -212,6 +210,16 @@ def test_play_media(self, mock_seek):
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)
Expand Down
15 changes: 15 additions & 0 deletions tests/helpers/test_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 7624d0e

Please sign in to comment.