From 1b874c603b666888f829d583d36ed57505fc49d3 Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Fri, 11 Sep 2015 18:32:47 -0400 Subject: [PATCH 1/9] rudimentary sonos support --- .../components/media_player/sonos.py | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 homeassistant/components/media_player/sonos.py diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py new file mode 100644 index 00000000000..5599ab4510c --- /dev/null +++ b/homeassistant/components/media_player/sonos.py @@ -0,0 +1,196 @@ +""" +homeassistant.components.media_player.sonos +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides an interface to Sonos players (via SoCo) + +Configuration: + +To use SoCo, add something like this to your configuration: + +media_player: + platform: sonos +""" + +import logging +import datetime + +REQUIREMENTS = ['SoCo>=0.11.1'] + +from homeassistant.components.media_player import ( + MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, + MEDIA_TYPE_MUSIC) + +from homeassistant.const import ( + STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN) + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Sonos platform. """ + + import soco + players = [] + for player in soco.discover(): + device = SonosDevice(player) + players.append(device) + + if players: + return True + + +# pylint: disable=too-many-instance-attributes +# pylint: disable=too-many-public-methods +class SonosDevice(MediaPlayerDevice): + """ Represents a SqueezeBox device. """ + + # pylint: disable=too-many-arguments + def __init__(self, player): + super(SonosDevice, self).__init__() + self._player = player + self._name = player.get_speaker_info()['zone_name'].replace( + ' (R)', '').replace(' (L)', '') + self._status = player.get_current_transport_info()['current_transport_state'] + self._trackinfo = player.get_current_track_info() + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + if self._status == 'PAUSED_PLAYBACK': + return STATE_PAUSED + if self._status == 'PLAYING': + return STATE_PLAYING + if self._status == 'STOPPED': + return STATE_IDLE + return STATE_UNKNOWN + + def update(self): + """ Retrieve latest state. """ + self._status = self._player.get_current_transport_info()['current_transport_state'] + + @property + def volume_level(self): + """ Volume level of the media player (0..1). """ + if 'mixer volume' in self._status: + return self._player.volume / 100.0 + + @property + def is_volume_muted(self): + return self._player.mute + + @property + def media_content_id(self): + """ Content ID of current playing media. """ + return self._trackinfo.get('title', None) + + @property + def media_content_type(self): + """ Content type of current playing media. """ + return MEDIA_TYPE_MUSIC + + @property + def media_duration(self): + """ Duration of current playing media in seconds. """ + dur = self._trackinfo.get('duration', '0:00') + return sum(60 ** x[0] * int(x[1]) for x in + enumerate(reversed(dur.split(':')))) + + @property + def media_image_url(self): + """ Image url of current playing media. """ + if 'album_art' in self._trackinfo: + return self._trackinfo['album_art'] + + @property + def media_title(self): + """ Title of current playing media. """ + if 'artist' in self._trackinfo and 'title' in self._trackinfo: + return '{artist} - {title}'.format( + artist=self._trackinfo['artist'], + title=self._trackinfo['title'] + ) + if 'title' in self._status: + return self._trackinfo['title'] + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return SUPPORT_SONOS + + def turn_off(self): + """ turn_off media player. """ + self._player.pause() + self.update_ha_state() + + def volume_up(self): + """ volume_up media player. """ + self._player.volume += 1 + self.update_ha_state() + + def volume_down(self): + """ volume_down media player. """ + self._player.volume -= 1 + self.update_ha_state() + + def set_volume_level(self, volume): + """ set volume level, range 0..1. """ + self._player.volume = str(int(volume * 100)) + self.update_ha_state() + + def mute_volume(self, mute): + """ mute (true) or unmute (false) media player. """ + self._player.mute = mute + self.update_ha_state() + + def media_play_pause(self): + """ media_play_pause media player. """ + if self.state == STATE_PAUSED: + self._player.play() + else: + self._player.pause() + self.update_ha_state() + + def media_play(self): + """ media_play media player. """ + self._player.play() + self.update_ha_state() + + def media_pause(self): + """ media_pause media player. """ + self._player.pause() + self.update_ha_state() + + def media_next_track(self): + """ Send next track command. """ + self._player.next() + self.update_ha_state() + + def media_previous_track(self): + """ Send next track command. """ + self._player.previous() + self.update_ha_state() + + def media_seek(self, position): + """ Send seek command. """ + self._player.seek(str(datetime.timedelta(seconds=int(position)))) + self.update_ha_state() + + def turn_on(self): + """ turn the media player on. """ + self._player.play() + self.update_ha_state() + + def play_youtube(self, media_id): + """ Plays a YouTube media. """ + raise NotImplementedError() From aa74c4e57a793c1bc3eb61b1920486eba8e20cf4 Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Fri, 11 Sep 2015 18:52:31 -0400 Subject: [PATCH 2/9] fix initialization --- homeassistant/components/media_player/sonos.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 5599ab4510c..12f0458fa16 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -36,13 +36,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Sonos platform. """ import soco - players = [] - for player in soco.discover(): - device = SonosDevice(player) - players.append(device) + add_devices(SonosDevice(p) for p in soco.discover()) - if players: - return True + return True # pylint: disable=too-many-instance-attributes From ae058b7847f66ea1d87e1e2216ea418b9aa19cf8 Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Fri, 11 Sep 2015 18:55:23 -0400 Subject: [PATCH 3/9] tidy up formatting to make travis happy. --- homeassistant/components/media_player/sonos.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 12f0458fa16..195ed5d5142 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -52,7 +52,8 @@ class SonosDevice(MediaPlayerDevice): self._player = player self._name = player.get_speaker_info()['zone_name'].replace( ' (R)', '').replace(' (L)', '') - self._status = player.get_current_transport_info()['current_transport_state'] + self._status = player.get_current_transport_info().get( + 'current_transport_state') self._trackinfo = player.get_current_track_info() @property @@ -73,7 +74,8 @@ class SonosDevice(MediaPlayerDevice): def update(self): """ Retrieve latest state. """ - self._status = self._player.get_current_transport_info()['current_transport_state'] + self._status = self._player.get_current_transport_info().get( + 'current_transport_state') @property def volume_level(self): From a25f7eed2ba86b30fc0615f3fc9d048599890244 Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Fri, 11 Sep 2015 19:38:42 -0400 Subject: [PATCH 4/9] Enable polling and fix metadata updating. Remove unnecessary methods. Include SoCo in requirements_all.txt for CI. Lock down SoCo version to 0.11.1 Add sonos.py to exclusions in .coveragerc --- .coveragerc | 1 + .../components/media_player/sonos.py | 30 +++++++------------ requirements_all.txt | 3 ++ 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/.coveragerc b/.coveragerc index 6bfc048f10f..cfac363496f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -49,6 +49,7 @@ omit = homeassistant/components/media_player/kodi.py homeassistant/components/media_player/mpd.py homeassistant/components/media_player/squeezebox.py + homeassistant/components/media_player/sonos.py homeassistant/components/notify/file.py homeassistant/components/notify/instapush.py homeassistant/components/notify/nma.py diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 195ed5d5142..a15a37f2a2e 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -15,7 +15,7 @@ media_player: import logging import datetime -REQUIREMENTS = ['SoCo>=0.11.1'] +REQUIREMENTS = ['SoCo==0.11.1'] from homeassistant.components.media_player import ( MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET, @@ -44,17 +44,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-public-methods class SonosDevice(MediaPlayerDevice): - """ Represents a SqueezeBox device. """ + """ Represents a Sonos device. """ # pylint: disable=too-many-arguments def __init__(self, player): super(SonosDevice, self).__init__() self._player = player - self._name = player.get_speaker_info()['zone_name'].replace( - ' (R)', '').replace(' (L)', '') - self._status = player.get_current_transport_info().get( - 'current_transport_state') - self._trackinfo = player.get_current_track_info() + self.update() + + @property + def should_poll(self): + return True @property def name(self): @@ -74,8 +74,11 @@ class SonosDevice(MediaPlayerDevice): def update(self): """ Retrieve latest state. """ + self._name = self._player.get_speaker_info()['zone_name'].replace( + ' (R)', '').replace(' (L)', '') self._status = self._player.get_current_transport_info().get( 'current_transport_state') + self._trackinfo = self._player.get_current_track_info() @property def volume_level(self): @@ -129,7 +132,6 @@ class SonosDevice(MediaPlayerDevice): def turn_off(self): """ turn_off media player. """ self._player.pause() - self.update_ha_state() def volume_up(self): """ volume_up media player. """ @@ -151,14 +153,6 @@ class SonosDevice(MediaPlayerDevice): self._player.mute = mute self.update_ha_state() - def media_play_pause(self): - """ media_play_pause media player. """ - if self.state == STATE_PAUSED: - self._player.play() - else: - self._player.pause() - self.update_ha_state() - def media_play(self): """ media_play media player. """ self._player.play() @@ -188,7 +182,3 @@ class SonosDevice(MediaPlayerDevice): """ turn the media player on. """ self._player.play() self.update_ha_state() - - def play_youtube(self, media_id): - """ Plays a YouTube media. """ - raise NotImplementedError() diff --git a/requirements_all.txt b/requirements_all.txt index f44b14d9ba7..e1aed3310a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -130,3 +130,6 @@ https://github.com/balloob/home-assistant-nzb-clients/archive/616cad591540925992 # sensor.vera # light.vera https://github.com/balloob/home-assistant-vera-api/archive/a8f823066ead6c7da6fb5e7abaf16fef62e63364.zip#python-vera==0.1 + +# Sonos bindings (media_player.sonos) +SoCo==0.11.1 From 3679a8078ac21c371cd073a699b075c12bf67b6b Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Fri, 11 Sep 2015 19:44:18 -0400 Subject: [PATCH 5/9] put back play_youtube override --- homeassistant/components/media_player/sonos.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index a15a37f2a2e..4e92a78cf87 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -182,3 +182,7 @@ class SonosDevice(MediaPlayerDevice): """ turn the media player on. """ self._player.play() self.update_ha_state() + + def play_youtube(self, media_id): + """ Plays a YouTube media. """ + raise NotImplementedError() From 350ed9f764da4bc67f01a1a34abe4d9a654a5d91 Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Fri, 11 Sep 2015 19:48:34 -0400 Subject: [PATCH 6/9] remove and disable pylint: disable=abstract-method for play_youtube() --- homeassistant/components/media_player/sonos.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 4e92a78cf87..87d961192df 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -43,6 +43,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-public-methods +# pylint: disable=abstract-method class SonosDevice(MediaPlayerDevice): """ Represents a Sonos device. """ @@ -182,7 +183,3 @@ class SonosDevice(MediaPlayerDevice): """ turn the media player on. """ self._player.play() self.update_ha_state() - - def play_youtube(self, media_id): - """ Plays a YouTube media. """ - raise NotImplementedError() From c3dd94ba047663d12cc677a8b6ec85ece73ab851 Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Fri, 11 Sep 2015 22:43:55 -0400 Subject: [PATCH 7/9] remove unnecessary self.update_ha_state calls --- homeassistant/components/media_player/sonos.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 87d961192df..69b089cd823 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -137,49 +137,39 @@ class SonosDevice(MediaPlayerDevice): def volume_up(self): """ volume_up media player. """ self._player.volume += 1 - self.update_ha_state() def volume_down(self): """ volume_down media player. """ self._player.volume -= 1 - self.update_ha_state() def set_volume_level(self, volume): """ set volume level, range 0..1. """ self._player.volume = str(int(volume * 100)) - self.update_ha_state() def mute_volume(self, mute): """ mute (true) or unmute (false) media player. """ self._player.mute = mute - self.update_ha_state() def media_play(self): """ media_play media player. """ self._player.play() - self.update_ha_state() def media_pause(self): """ media_pause media player. """ self._player.pause() - self.update_ha_state() def media_next_track(self): """ Send next track command. """ self._player.next() - self.update_ha_state() def media_previous_track(self): """ Send next track command. """ self._player.previous() - self.update_ha_state() def media_seek(self, position): """ Send seek command. """ self._player.seek(str(datetime.timedelta(seconds=int(position)))) - self.update_ha_state() def turn_on(self): """ turn the media player on. """ self._player.play() - self.update_ha_state() From e9367d536923e879aebc7a052d346ca37d3fe171 Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Fri, 11 Sep 2015 22:44:37 -0400 Subject: [PATCH 8/9] use own track_utc_time_change to poll every 5 seconds --- homeassistant/components/media_player/sonos.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 69b089cd823..d355049741c 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -22,6 +22,7 @@ from homeassistant.components.media_player import ( SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, MEDIA_TYPE_MUSIC) +from homeassistant.helpers.event import track_utc_time_change from homeassistant.const import ( STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN) @@ -36,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Sonos platform. """ import soco - add_devices(SonosDevice(p) for p in soco.discover()) + add_devices(SonosDevice(hass, p) for p in soco.discover()) return True @@ -48,14 +49,18 @@ class SonosDevice(MediaPlayerDevice): """ Represents a Sonos device. """ # pylint: disable=too-many-arguments - def __init__(self, player): + def __init__(self, hass, player): super(SonosDevice, self).__init__() self._player = player self.update() + track_utc_time_change( + hass, self.update_ha_state, + second=range(0, 60, 5)) + @property def should_poll(self): - return True + return False @property def name(self): From db2140782f7ec8cd8240bc66fe7bd55bd4eb2504 Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Fri, 11 Sep 2015 23:57:34 -0400 Subject: [PATCH 9/9] follow proper calling convention for track_utc_time_change callback --- homeassistant/components/media_player/sonos.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index d355049741c..3bcf6021bda 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -55,13 +55,17 @@ class SonosDevice(MediaPlayerDevice): self.update() track_utc_time_change( - hass, self.update_ha_state, + hass, self.update_sonos, second=range(0, 60, 5)) @property def should_poll(self): return False + def update_sonos(self, now): + """ Updates state, called by track_utc_time_change """ + self.update_ha_state(True) + @property def name(self): """ Returns the name of the device. """