mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +00:00
Async support in media player component (#4995)
* Async support in media player component * Removed redundant new tests * Update to new 'reduce coroutine' standards * Remove extra create_task
This commit is contained in:
parent
b78cf4772d
commit
2a7a419ff3
@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player/
|
||||
"""
|
||||
import asyncio
|
||||
import functools as ft
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
@ -100,20 +101,62 @@ SUPPORT_SELECT_SOURCE = 2048
|
||||
SUPPORT_STOP = 4096
|
||||
SUPPORT_CLEAR_PLAYLIST = 8192
|
||||
|
||||
# simple services that only take entity_id(s) as optional argument
|
||||
# Service call validation schemas
|
||||
MEDIA_PLAYER_SCHEMA = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
})
|
||||
|
||||
MEDIA_PLAYER_SET_VOLUME_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float,
|
||||
})
|
||||
|
||||
MEDIA_PLAYER_MUTE_VOLUME_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required(ATTR_MEDIA_VOLUME_MUTED): cv.boolean,
|
||||
})
|
||||
|
||||
MEDIA_PLAYER_MEDIA_SEEK_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required(ATTR_MEDIA_SEEK_POSITION):
|
||||
vol.All(vol.Coerce(float), vol.Range(min=0)),
|
||||
})
|
||||
|
||||
MEDIA_PLAYER_SELECT_SOURCE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required(ATTR_INPUT_SOURCE): cv.string,
|
||||
})
|
||||
|
||||
MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string,
|
||||
vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
|
||||
vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean,
|
||||
})
|
||||
|
||||
SERVICE_TO_METHOD = {
|
||||
SERVICE_TURN_ON: 'turn_on',
|
||||
SERVICE_TURN_OFF: 'turn_off',
|
||||
SERVICE_TOGGLE: 'toggle',
|
||||
SERVICE_VOLUME_UP: 'volume_up',
|
||||
SERVICE_VOLUME_DOWN: 'volume_down',
|
||||
SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause',
|
||||
SERVICE_MEDIA_PLAY: 'media_play',
|
||||
SERVICE_MEDIA_PAUSE: 'media_pause',
|
||||
SERVICE_MEDIA_STOP: 'media_stop',
|
||||
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
|
||||
SERVICE_CLEAR_PLAYLIST: 'clear_playlist'
|
||||
SERVICE_TURN_ON: {'method': 'async_turn_on'},
|
||||
SERVICE_TURN_OFF: {'method': 'async_turn_off'},
|
||||
SERVICE_TOGGLE: {'method': 'async_toggle'},
|
||||
SERVICE_VOLUME_UP: {'method': 'async_volume_up'},
|
||||
SERVICE_VOLUME_DOWN: {'method': 'async_volume_down'},
|
||||
SERVICE_MEDIA_PLAY_PAUSE: {'method': 'async_media_play_pause'},
|
||||
SERVICE_MEDIA_PLAY: {'method': 'async_media_play'},
|
||||
SERVICE_MEDIA_PAUSE: {'method': 'async_media_pause'},
|
||||
SERVICE_MEDIA_STOP: {'method': 'async_media_stop'},
|
||||
SERVICE_MEDIA_NEXT_TRACK: {'method': 'async_media_next_track'},
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK: {'method': 'async_media_previous_track'},
|
||||
SERVICE_CLEAR_PLAYLIST: {'method': 'async_clear_playlist'},
|
||||
SERVICE_VOLUME_SET: {
|
||||
'method': 'async_set_volume_level',
|
||||
'schema': MEDIA_PLAYER_SET_VOLUME_SCHEMA},
|
||||
SERVICE_VOLUME_MUTE: {
|
||||
'method': 'async_mute_volume',
|
||||
'schema': MEDIA_PLAYER_MUTE_VOLUME_SCHEMA},
|
||||
SERVICE_MEDIA_SEEK: {
|
||||
'method': 'async_media_seek',
|
||||
'schema': MEDIA_PLAYER_MEDIA_SEEK_SCHEMA},
|
||||
SERVICE_SELECT_SOURCE: {
|
||||
'method': 'async_select_source',
|
||||
'schema': MEDIA_PLAYER_SELECT_SOURCE_SCHEMA},
|
||||
SERVICE_PLAY_MEDIA: {
|
||||
'method': 'async_play_media',
|
||||
'schema': MEDIA_PLAYER_PLAY_MEDIA_SCHEMA},
|
||||
}
|
||||
|
||||
ATTR_TO_PROPERTY = [
|
||||
@ -141,34 +184,6 @@ ATTR_TO_PROPERTY = [
|
||||
ATTR_INPUT_SOURCE_LIST,
|
||||
]
|
||||
|
||||
# Service call validation schemas
|
||||
MEDIA_PLAYER_SCHEMA = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
})
|
||||
|
||||
MEDIA_PLAYER_MUTE_VOLUME_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required(ATTR_MEDIA_VOLUME_MUTED): cv.boolean,
|
||||
})
|
||||
|
||||
MEDIA_PLAYER_SET_VOLUME_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float,
|
||||
})
|
||||
|
||||
MEDIA_PLAYER_MEDIA_SEEK_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required(ATTR_MEDIA_SEEK_POSITION):
|
||||
vol.All(vol.Coerce(float), vol.Range(min=0)),
|
||||
})
|
||||
|
||||
MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string,
|
||||
vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
|
||||
vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean,
|
||||
})
|
||||
|
||||
MEDIA_PLAYER_SELECT_SOURCE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||
vol.Required(ATTR_INPUT_SOURCE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
"""
|
||||
@ -304,109 +319,67 @@ def clear_playlist(hass, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_CLEAR_PLAYLIST, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Track states and offer events for media_players."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
hass.http.register_view(MediaPlayerImageView(component.entities))
|
||||
|
||||
component.setup(config)
|
||||
yield from component.async_setup(config)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
descriptions = yield from hass.loop.run_in_executor(
|
||||
None, load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def media_player_service_handler(service):
|
||||
@asyncio.coroutine
|
||||
def async_service_handler(service):
|
||||
"""Map services to methods on MediaPlayerDevice."""
|
||||
method = SERVICE_TO_METHOD[service.service]
|
||||
method = SERVICE_TO_METHOD.get(service.service)
|
||||
if not method:
|
||||
return
|
||||
|
||||
for player in component.extract_from_service(service):
|
||||
getattr(player, method)()
|
||||
params = {}
|
||||
if service.service == SERVICE_VOLUME_SET:
|
||||
params['volume'] = service.data.get(ATTR_MEDIA_VOLUME_LEVEL)
|
||||
elif service.service == SERVICE_VOLUME_MUTE:
|
||||
params['mute'] = service.data.get(ATTR_MEDIA_VOLUME_MUTED)
|
||||
elif service.service == SERVICE_MEDIA_SEEK:
|
||||
params['position'] = service.data.get(ATTR_MEDIA_SEEK_POSITION)
|
||||
elif service.service == SERVICE_SELECT_SOURCE:
|
||||
params['source'] = service.data.get(ATTR_INPUT_SOURCE)
|
||||
elif service.service == SERVICE_PLAY_MEDIA:
|
||||
params['media_type'] = \
|
||||
service.data.get(ATTR_MEDIA_CONTENT_TYPE)
|
||||
params['media_id'] = service.data.get(ATTR_MEDIA_CONTENT_ID)
|
||||
params[ATTR_MEDIA_ENQUEUE] = \
|
||||
service.data.get(ATTR_MEDIA_ENQUEUE)
|
||||
target_players = component.async_extract_from_service(service)
|
||||
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
update_tasks = []
|
||||
for player in target_players:
|
||||
yield from getattr(player, method['method'])(**params)
|
||||
|
||||
for player in target_players:
|
||||
if not player.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = player.async_update_ha_state(True)
|
||||
if hasattr(player, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
else:
|
||||
yield from update_coro
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
for service in SERVICE_TO_METHOD:
|
||||
hass.services.register(DOMAIN, service, media_player_service_handler,
|
||||
descriptions.get(service),
|
||||
schema=MEDIA_PLAYER_SCHEMA)
|
||||
|
||||
def volume_set_service(service):
|
||||
"""Set specified volume on the media player."""
|
||||
volume = service.data.get(ATTR_MEDIA_VOLUME_LEVEL)
|
||||
|
||||
for player in component.extract_from_service(service):
|
||||
player.set_volume_level(volume)
|
||||
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service,
|
||||
descriptions.get(SERVICE_VOLUME_SET),
|
||||
schema=MEDIA_PLAYER_SET_VOLUME_SCHEMA)
|
||||
|
||||
def volume_mute_service(service):
|
||||
"""Mute (true) or unmute (false) the media player."""
|
||||
mute = service.data.get(ATTR_MEDIA_VOLUME_MUTED)
|
||||
|
||||
for player in component.extract_from_service(service):
|
||||
player.mute_volume(mute)
|
||||
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service,
|
||||
descriptions.get(SERVICE_VOLUME_MUTE),
|
||||
schema=MEDIA_PLAYER_MUTE_VOLUME_SCHEMA)
|
||||
|
||||
def media_seek_service(service):
|
||||
"""Seek to a position."""
|
||||
position = service.data.get(ATTR_MEDIA_SEEK_POSITION)
|
||||
|
||||
for player in component.extract_from_service(service):
|
||||
player.media_seek(position)
|
||||
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service,
|
||||
descriptions.get(SERVICE_MEDIA_SEEK),
|
||||
schema=MEDIA_PLAYER_MEDIA_SEEK_SCHEMA)
|
||||
|
||||
def select_source_service(service):
|
||||
"""Change input to selected source."""
|
||||
input_source = service.data.get(ATTR_INPUT_SOURCE)
|
||||
|
||||
for player in component.extract_from_service(service):
|
||||
player.select_source(input_source)
|
||||
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SELECT_SOURCE,
|
||||
select_source_service,
|
||||
descriptions.get(SERVICE_SELECT_SOURCE),
|
||||
schema=MEDIA_PLAYER_SELECT_SOURCE_SCHEMA)
|
||||
|
||||
def play_media_service(service):
|
||||
"""Play specified media_id on the media player."""
|
||||
media_type = service.data.get(ATTR_MEDIA_CONTENT_TYPE)
|
||||
media_id = service.data.get(ATTR_MEDIA_CONTENT_ID)
|
||||
enqueue = service.data.get(ATTR_MEDIA_ENQUEUE)
|
||||
|
||||
kwargs = {
|
||||
ATTR_MEDIA_ENQUEUE: enqueue,
|
||||
}
|
||||
|
||||
for player in component.extract_from_service(service):
|
||||
player.play_media(media_type, media_id, **kwargs)
|
||||
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_PLAY_MEDIA, play_media_service,
|
||||
descriptions.get(SERVICE_PLAY_MEDIA),
|
||||
schema=MEDIA_PLAYER_PLAY_MEDIA_SCHEMA)
|
||||
schema = SERVICE_TO_METHOD[service].get(
|
||||
'schema', MEDIA_PLAYER_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, async_service_handler,
|
||||
descriptions.get(service), schema=schema)
|
||||
|
||||
return True
|
||||
|
||||
@ -548,54 +521,158 @@ class MediaPlayerDevice(Entity):
|
||||
"""Turn the media player on."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_on(self):
|
||||
"""Turn the media player on.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.turn_on)
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn the media player off."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_turn_off(self):
|
||||
"""Turn the media player off.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.turn_off)
|
||||
|
||||
def mute_volume(self, mute):
|
||||
"""Mute the volume."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_mute_volume(self, mute):
|
||||
"""Mute the volume.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.mute_volume, mute)
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set volume level, range 0..1."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_set_volume_level(self, volume):
|
||||
"""Set volume level, range 0..1.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.set_volume_level, volume)
|
||||
|
||||
def media_play(self):
|
||||
"""Send play commmand."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_media_play(self):
|
||||
"""Send play commmand.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.media_play)
|
||||
|
||||
def media_pause(self):
|
||||
"""Send pause command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_media_pause(self):
|
||||
"""Send pause command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.media_pause)
|
||||
|
||||
def media_stop(self):
|
||||
"""Send stop command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_media_stop(self):
|
||||
"""Send stop command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.media_stop)
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send previous track command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_media_previous_track(self):
|
||||
"""Send previous track command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.media_previous_track)
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send next track command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_media_next_track(self):
|
||||
"""Send next track command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.media_next_track)
|
||||
|
||||
def media_seek(self, position):
|
||||
"""Send seek command."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def play_media(self, media_type, media_id):
|
||||
def async_media_seek(self, position):
|
||||
"""Send seek command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.media_seek, position)
|
||||
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Play a piece of media."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_play_media(self, media_type, media_id, **kwargs):
|
||||
"""Play a piece of media.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.play_media, media_type, media_id, **kwargs))
|
||||
|
||||
def select_source(self, source):
|
||||
"""Select input source."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_select_source(self, source):
|
||||
"""Select input source.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.select_source, source)
|
||||
|
||||
def clear_playlist(self):
|
||||
"""Clear players playlist."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def async_clear_playlist(self):
|
||||
"""Clear players playlist.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.loop.run_in_executor(
|
||||
None, self.clear_playlist)
|
||||
|
||||
# No need to overwrite these.
|
||||
@property
|
||||
def support_pause(self):
|
||||
@ -654,16 +731,40 @@ class MediaPlayerDevice(Entity):
|
||||
else:
|
||||
self.turn_off()
|
||||
|
||||
def async_toggle(self):
|
||||
"""Toggle the power on the media player.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
if self.state in [STATE_OFF, STATE_IDLE]:
|
||||
return self.async_turn_on()
|
||||
else:
|
||||
return self.async_turn_off()
|
||||
|
||||
def volume_up(self):
|
||||
"""Turn volume up for media player."""
|
||||
if self.volume_level < 1:
|
||||
self.set_volume_level(min(1, self.volume_level + .1))
|
||||
|
||||
def async_volume_up(self):
|
||||
"""Turn volume up for media player.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.async_set_volume_level(min(1, self.volume_level + .1))
|
||||
|
||||
def volume_down(self):
|
||||
"""Turn volume down for media player."""
|
||||
if self.volume_level > 0:
|
||||
self.set_volume_level(max(0, self.volume_level - .1))
|
||||
|
||||
def async_volume_down(self):
|
||||
"""Turn volume down for media player.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.async_set_volume_level(max(0, self.volume_level - .1))
|
||||
|
||||
def media_play_pause(self):
|
||||
"""Play or pause the media player."""
|
||||
if self.state == STATE_PLAYING:
|
||||
@ -671,6 +772,16 @@ class MediaPlayerDevice(Entity):
|
||||
else:
|
||||
self.media_play()
|
||||
|
||||
def async_media_play_pause(self):
|
||||
"""Play or pause the media player.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
if self.state == STATE_PLAYING:
|
||||
return self.async_media_pause()
|
||||
else:
|
||||
return self.async_media_play()
|
||||
|
||||
@property
|
||||
def entity_picture(self):
|
||||
"""Return image of the media playing."""
|
||||
|
@ -69,7 +69,6 @@ class TestDemoMediaPlayer(unittest.TestCase):
|
||||
self.hass, mp.DOMAIN,
|
||||
{'media_player': {'platform': 'demo'}})
|
||||
state = self.hass.states.get(entity_id)
|
||||
print(state)
|
||||
assert 1.0 == state.attributes.get('volume_level')
|
||||
|
||||
mp.set_volume_level(self.hass, None, entity_id)
|
||||
@ -203,7 +202,7 @@ class TestDemoMediaPlayer(unittest.TestCase):
|
||||
state.attributes.get('supported_media_commands'))
|
||||
|
||||
@patch('homeassistant.components.media_player.demo.DemoYoutubePlayer.'
|
||||
'media_seek')
|
||||
'media_seek', autospec=True)
|
||||
def test_play_media(self, mock_seek):
|
||||
"""Test play_media ."""
|
||||
assert setup_component(
|
||||
|
Loading…
x
Reference in New Issue
Block a user