add media_player/clear_playlist and line-in/tv support to sonos (#2527)

* add media_player/clear_playlist and line-in/tv support to sonos

* add support source radio

* fix bug

* print TV/Line-In as media_title

* implement universal player

* add to demo platform

* Update demo.py

Better handling for demo object

* add unit tests

* fix unit test
This commit is contained in:
Pascal Vizeli 2016-07-15 18:00:41 +02:00 committed by Paulus Schoutsen
parent c1798dbe1f
commit 6694f29918
7 changed files with 117 additions and 11 deletions

View File

@ -31,6 +31,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
SERVICE_PLAY_MEDIA = 'play_media'
SERVICE_SELECT_SOURCE = 'select_source'
SERVICE_CLEAR_PLAYLIST = 'clear_playlist'
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
@ -75,6 +76,7 @@ SUPPORT_PLAY_MEDIA = 512
SUPPORT_VOLUME_STEP = 1024
SUPPORT_SELECT_SOURCE = 2048
SUPPORT_STOP = 4096
SUPPORT_CLEAR_PLAYLIST = 8192
# simple services that only take entity_id(s) as optional argument
SERVICE_TO_METHOD = {
@ -89,7 +91,8 @@ SERVICE_TO_METHOD = {
SERVICE_MEDIA_STOP: 'media_stop',
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
SERVICE_SELECT_SOURCE: 'select_source'
SERVICE_SELECT_SOURCE: 'select_source',
SERVICE_CLEAR_PLAYLIST: 'clear_playlist'
}
ATTR_TO_PROPERTY = [
@ -272,6 +275,12 @@ def select_source(hass, source, entity_id=None):
hass.services.call(DOMAIN, SERVICE_SELECT_SOURCE, data)
def clear_playlist(hass, entity_id=None):
"""Send the media player the command for clear playlist."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_CLEAR_PLAYLIST, data)
def setup(hass, config):
"""Track states and offer events for media_players."""
component = EntityComponent(
@ -542,6 +551,10 @@ class MediaPlayerDevice(Entity):
"""Select input source."""
raise NotImplementedError()
def clear_playlist(self):
"""Clear players playlist."""
raise NotImplementedError()
# No need to overwrite these.
@property
def support_pause(self):
@ -588,6 +601,11 @@ class MediaPlayerDevice(Entity):
"""Boolean if select source command supported."""
return bool(self.supported_media_commands & SUPPORT_SELECT_SOURCE)
@property
def support_clear_playlist(self):
"""Boolean if clear playlist command supported."""
return bool(self.supported_media_commands & SUPPORT_CLEAR_PLAYLIST)
def toggle(self):
"""Toggle the power on the media player."""
if self.state in [STATE_OFF, STATE_IDLE]:

View File

@ -8,7 +8,7 @@ from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, MediaPlayerDevice)
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
@ -32,7 +32,7 @@ YOUTUBE_PLAYER_SUPPORT = \
MUSIC_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_CLEAR_PLAYLIST
NETFLIX_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
@ -214,12 +214,12 @@ class DemoMusicPlayer(AbstractDemoPlayer):
@property
def media_title(self):
"""Return the title of current playing media."""
return self.tracks[self._cur_track][1]
return self.tracks[self._cur_track][1] if len(self.tracks) > 0 else ""
@property
def media_artist(self):
"""Return the artist of current playing media (Music track only)."""
return self.tracks[self._cur_track][0]
return self.tracks[self._cur_track][0] if len(self.tracks) > 0 else ""
@property
def media_album_name(self):
@ -257,6 +257,13 @@ class DemoMusicPlayer(AbstractDemoPlayer):
self._cur_track += 1
self.update_ha_state()
def clear_playlist(self):
"""Clear players playlist."""
self.tracks = []
self._cur_track = 0
self._player_state = STATE_OFF
self.update_ha_state()
class DemoTVShowPlayer(AbstractDemoPlayer):
"""A Demo media player that only supports YouTube."""

View File

@ -146,6 +146,14 @@ select_source:
description: Name of the source to switch to. Platform dependent.
example: 'video1'
clear_playlist:
description: Send the media player the command to clear players playlist.
fields:
entity_id:
description: Name(s) of entites to change source on
example: 'media_player.living_room_chromecast'
sonos_group_players:
description: Send Sonos media player the command for grouping all players into one (party mode).

View File

@ -12,7 +12,8 @@ from os import path
from homeassistant.components.media_player import (
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF)
from homeassistant.config import load_yaml_config_file
@ -31,13 +32,17 @@ _REQUESTS_LOGGER.setLevel(logging.ERROR)
SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA |\
SUPPORT_SEEK
SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST | SUPPORT_SELECT_SOURCE
SERVICE_GROUP_PLAYERS = 'sonos_group_players'
SERVICE_UNJOIN = 'sonos_unjoin'
SERVICE_SNAPSHOT = 'sonos_snapshot'
SERVICE_RESTORE = 'sonos_restore'
SUPPORT_SOURCE_LINEIN = 'Line-in'
SUPPORT_SOURCE_TV = 'TV'
SUPPORT_SOURCE_RADIO = 'Radio'
# pylint: disable=unused-argument, too-many-locals
def setup_platform(hass, config, add_devices, discovery_info=None):
@ -162,12 +167,12 @@ class SonosDevice(MediaPlayerDevice):
# pylint: disable=too-many-arguments
def __init__(self, hass, player):
"""Initialize the Sonos device."""
from soco.snapshot import Snapshot
self.hass = hass
self.volume_increment = 5
super(SonosDevice, self).__init__()
self._player = player
self.update()
from soco.snapshot import Snapshot
self.soco_snapshot = Snapshot(self._player)
@property
@ -268,6 +273,10 @@ class SonosDevice(MediaPlayerDevice):
)
if 'title' in self._status:
return self._trackinfo['title']
if self._player.is_playing_line_in:
return SUPPORT_SOURCE_LINEIN
if self._player.is_playing_tv:
return SUPPORT_SOURCE_TV
@property
def supported_media_commands(self):
@ -290,6 +299,36 @@ class SonosDevice(MediaPlayerDevice):
"""Mute (true) or unmute (false) media player."""
self._player.mute = mute
def select_source(self, source):
"""Select input source."""
if source == SUPPORT_SOURCE_LINEIN:
self._player.switch_to_line_in()
elif source == SUPPORT_SOURCE_TV:
self._player.switch_to_tv()
@property
def source_list(self):
"""List of available input sources."""
source = []
# generate list of supported device
source.append(SUPPORT_SOURCE_LINEIN)
source.append(SUPPORT_SOURCE_TV)
source.append(SUPPORT_SOURCE_RADIO)
return source
@property
def source(self):
"""Name of the current input source."""
if self._player.is_playing_line_in:
return SUPPORT_SOURCE_LINEIN
if self._player.is_playing_tv:
return SUPPORT_SOURCE_TV
if self._player.is_playing_radio:
return SUPPORT_SOURCE_RADIO
return None
@only_if_coordinator
def turn_off(self):
"""Turn off media player."""
@ -320,6 +359,11 @@ class SonosDevice(MediaPlayerDevice):
"""Send seek command."""
self._player.seek(str(datetime.timedelta(seconds=int(position))))
@only_if_coordinator
def clear_playlist(self):
"""Clear players playlist."""
self._player.clear_queue()
@only_if_coordinator
def turn_on(self):
"""Turn the media player on."""

View File

@ -17,8 +17,9 @@ from homeassistant.components.media_player import (
ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED,
ATTR_SUPPORTED_MEDIA_COMMANDS, DOMAIN, SERVICE_PLAY_MEDIA,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, ATTR_INPUT_SOURCE,
SERVICE_SELECT_SOURCE, MediaPlayerDevice)
SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST,
ATTR_INPUT_SOURCE, SERVICE_SELECT_SOURCE, SERVICE_CLEAR_PLAYLIST,
MediaPlayerDevice)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, CONF_NAME, SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE,
@ -349,6 +350,9 @@ class UniversalMediaPlayer(MediaPlayerDevice):
if SERVICE_SELECT_SOURCE in self._cmds:
flags |= SUPPORT_SELECT_SOURCE
if SERVICE_CLEAR_PLAYLIST in self._cmds:
flags |= SUPPORT_CLEAR_PLAYLIST
return flags
@property
@ -424,6 +428,10 @@ class UniversalMediaPlayer(MediaPlayerDevice):
data = {ATTR_INPUT_SOURCE: source}
self._call_service(SERVICE_SELECT_SOURCE, data)
def clear_playlist(self):
"""Clear players playlist."""
self._call_service(SERVICE_CLEAR_PLAYLIST)
def update(self):
"""Update state in HA."""
for child_name in self._children:

View File

@ -38,6 +38,15 @@ class TestDemoMediaPlayer(unittest.TestCase):
state = self.hass.states.get(entity_id)
assert 'xbox' == state.attributes.get('source')
def test_clear_playlist(self):
"""Test clear playlist."""
assert mp.setup(self.hass, {'media_player': {'platform': 'demo'}})
assert self.hass.states.is_state(entity_id, 'playing')
mp.clear_playlist(self.hass, entity_id)
self.hass.pool.block_till_done()
assert self.hass.states.is_state(entity_id, 'off')
def test_volume_services(self):
"""Test the volume service."""
assert mp.setup(self.hass, {'media_player': {'platform': 'demo'}})

View File

@ -25,6 +25,7 @@ class MockMediaPlayer(media_player.MediaPlayerDevice):
self._media_title = None
self._supported_media_commands = 0
self._source = None
self._tracks = 12
self.service_calls = {
'turn_on': mock_service(
@ -59,6 +60,9 @@ class MockMediaPlayer(media_player.MediaPlayerDevice):
'select_source': mock_service(
hass, media_player.DOMAIN,
media_player.SERVICE_SELECT_SOURCE),
'clear_playlist': mock_service(
hass, media_player.DOMAIN,
media_player.SERVICE_CLEAR_PLAYLIST),
}
@property
@ -114,6 +118,10 @@ class MockMediaPlayer(media_player.MediaPlayerDevice):
"""Set the input source."""
self._state = source
def clear_playlist(self):
"""Clear players playlist."""
self._tracks = 0
class TestMediaPlayer(unittest.TestCase):
"""Test the media_player module."""
@ -510,6 +518,10 @@ class TestMediaPlayer(unittest.TestCase):
self.assertEqual(
1, len(self.mock_mp_2.service_calls['select_source']))
ump.clear_playlist()
self.assertEqual(
1, len(self.mock_mp_2.service_calls['clear_playlist']))
def test_service_call_to_command(self):
"""Test service call to command."""
config = self.config_children_only