From cb3f14a862f32145fd0e07ce344652d0b9c3178b Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Fri, 11 Sep 2015 23:06:03 -0400 Subject: [PATCH 01/10] add iTunes component --- .../components/media_player/itunes.py | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 homeassistant/components/media_player/itunes.py diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py new file mode 100644 index 00000000000..4bb5eca1bd9 --- /dev/null +++ b/homeassistant/components/media_player/itunes.py @@ -0,0 +1,252 @@ +""" +homeassistant.components.media_player.itunes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides an interface to iTunes-API (https://github.com/maddox/itunes-api) + +Configuration: + +To use iTunes you will need to add something like the following to +your configuration.yaml file. + +media_player: + platform: itunes + name: iTunes + host: http://192.168.1.16 + port: 8181 + +Variables: + +name +*Optional +The name of the device. + +url +*Required +The URL of your set up and running version of iTunes-API. Example: http://192.168.1.50:8181 + +""" +import logging + +from homeassistant.components.media_player import ( + MediaPlayerDevice, MEDIA_TYPE_MUSIC, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK) +from homeassistant.const import ( + STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF) + +try: + import requests +except ImportError: + requests = None + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK + +class Itunes(object): + + def __init__(self, host, port): + self.host = host + self.port = port + + @property + def _base_url(self): + return self.host + ":" + str(self.port) + + def _request(self, method, path, params=None): + url = self._base_url + path + + try: + if method == 'GET': + r = requests.get(url) + elif method == "POST": + r = requests.put(url, params) + elif method == "PUT": + r = requests.put(url, params) + elif method == "DELETE": + r = requests.delete(url) + + return r.json() + except requests.exceptions.HTTPError: + return {'player_state': 'error'} + except requests.exceptions.RequestException: + return {'player_state': 'offline'} + + def _command(self, named_command): + return self._request('PUT', '/' + named_command) + + def now_playing(self): + return self._request('GET', '/now_playing') + + def set_volume(self, level): + return self._request('PUT', '/volume', {'level': level}) + + def set_muted(self, muted): + return self._request('PUT', '/mute', {'muted': muted}) + + def play(self): + return self._command('play') + + def pause(self): + return self._command('pause') + + def next(self): + return self._command('next') + + def previous(self): + return self._command('previous') + + def artwork_url(self): + return self._base_url + '/artwork' + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the itunes platform. """ + + add_devices([ + ItunesDevice( + config.get('name', 'iTunes'), + config.get('host'), + config.get('port') + ) + ]) + +class ItunesDevice(MediaPlayerDevice): + """ Represents a iTunes-API instance. """ + + # pylint: disable=too-many-public-methods + + def __init__(self, name, host, port): + self._name = name + self._host = host + self._port = port + + self.client = Itunes(self._host, self._port) + + self.current_volume = None + self.muted = None + self.current_title = None + self.current_album = None + self.current_artist = None + self.current_playlist = None + self.content_id = None + + self.player_state = None + + self.update() + + def updateState(self, state_hash): + self.player_state = state_hash.get('player_state', None) + + self.current_volume = state_hash.get('volume', 0) + self.muted = state_hash.get('muted', None) + self.current_title = state_hash.get('name', None) + self.current_album = state_hash.get('album', None) + self.current_artist = state_hash.get('artist', None) + self.current_playlist = state_hash.get('playlist', None) + self.content_id = state_hash.get('id', None) + + @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.player_state == 'offline' or self.player_state == None: + return 'offline' + + if self.player_state == 'error': + return 'error' + + if self.player_state == 'stopped': + return STATE_IDLE + + if self.player_state == 'paused': + return STATE_PAUSED + else: + return STATE_PLAYING + + def update(self): + """ Retrieve latest state. """ + now_playing = self.client.now_playing() + self.updateState(now_playing) + + @property + def is_volume_muted(self): + """ Boolean if volume is currently muted. """ + return self.muted + + @property + def volume_level(self): + return self.current_volume/100.0 + + @property + def media_content_id(self): + """ Content ID of current playing media. """ + return self.content_id + + @property + def media_content_type(self): + return MEDIA_TYPE_MUSIC + + @property + def media_image_url(self): + """ Image url of current playing media. """ + + if (self.player_state == STATE_PLAYING or self.player_state == STATE_IDLE or self.player_state == STATE_PAUSED) and self.current_title != None: + return self.client.artwork_url() + else: + return "https://cloud.githubusercontent.com/assets/260/9829355/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png" + + @property + def media_title(self): + """ Title of current playing media. """ + return self.current_title + + @property + def media_artist(self): + """ Artist of current playing media. (Music track only) """ + return self.current_artist + + @property + def media_album_name(self): + """ Album of current playing media. (Music track only) """ + return self.current_album + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return SUPPORT_ITUNES + + def set_volume_level(self, volume): + """ set volume level, range 0..1. """ + r = self.client.set_volume(int(volume * 100)) + self.updateState(r) + + def mute_volume(self, mute): + """ mute (true) or unmute (false) media player. """ + r = self.client.set_muted(mute) + self.updateState(r) + + def media_play(self): + """ media_play media player. """ + r = self.client.play() + self.updateState(r) + + def media_pause(self): + """ media_pause media player. """ + r = self.client.pause() + self.updateState(r) + + def media_next_track(self): + """ media_next media player. """ + r = self.client.next() + self.updateState(r) + + def media_previous_track(self): + """ media_previous media player. """ + r = self.client.previous() + self.updateState(r) From a459368998e98625e1d92d0d2d070bd8de1700cc Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Fri, 11 Sep 2015 23:06:17 -0400 Subject: [PATCH 02/10] add itunes.py to .coveragerc --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 6bfc048f10f..4bf1f5e4b21 100644 --- a/.coveragerc +++ b/.coveragerc @@ -46,6 +46,7 @@ omit = homeassistant/components/light/limitlessled.py homeassistant/components/media_player/cast.py homeassistant/components/media_player/denon.py + homeassistant/components/media_player/itunes.py homeassistant/components/media_player/kodi.py homeassistant/components/media_player/mpd.py homeassistant/components/media_player/squeezebox.py From 6d9b618f1c73775bfe43784312cc0bb0e61ba36b Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Fri, 11 Sep 2015 23:06:48 -0400 Subject: [PATCH 03/10] add mention of iTunes to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 26bb0b998f5..6b1b1353392 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Examples of devices it can interface it: * Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), and [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/) * [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, [Edimax](http://www.edimax.com/) switches, [Efergy](https://efergy.com) energy monitoring, RFXtrx sensors, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors - * [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), and [Kodi (XBMC)](http://kodi.tv/) + * [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), [Kodi (XBMC)](http://kodi.tv/), and iTunes (by way of [itunes-api](https://github.com/maddox/itunes-api)) * Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/) * Integrate data from the [Bitcoin](https://bitcoin.org) network, meteorological data from [OpenWeatherMap](http://openweathermap.org/) and [Forecast.io](https://forecast.io/), [Transmission](http://www.transmissionbt.com/), or [SABnzbd](http://sabnzbd.org). * [See full list of supported devices](https://home-assistant.io/components/) From b9f5ec9e2ca6621fe1b4542f4b5a45b85c72d5da Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Fri, 11 Sep 2015 23:49:43 -0400 Subject: [PATCH 04/10] style fixes --- .../components/media_player/itunes.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 4bb5eca1bd9..363f34b2063 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -22,16 +22,17 @@ The name of the device. url *Required -The URL of your set up and running version of iTunes-API. Example: http://192.168.1.50:8181 +URL of your running version of iTunes-API. Example: http://192.168.1.50:8181 """ import logging from homeassistant.components.media_player import ( - MediaPlayerDevice, MEDIA_TYPE_MUSIC, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK) + MediaPlayerDevice, MEDIA_TYPE_MUSIC, SUPPORT_PAUSE, SUPPORT_SEEK, + SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, + SUPPORT_NEXT_TRACK) from homeassistant.const import ( - STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF) + STATE_IDLE, STATE_PLAYING, STATE_PAUSED) try: import requests @@ -43,6 +44,7 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK + class Itunes(object): def __init__(self, host, port): @@ -103,6 +105,7 @@ class Itunes(object): def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the itunes platform. """ + add_devices([ ItunesDevice( config.get('name', 'iTunes'), @@ -111,6 +114,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ) ]) + class ItunesDevice(MediaPlayerDevice): """ Represents a iTunes-API instance. """ @@ -155,7 +159,7 @@ class ItunesDevice(MediaPlayerDevice): def state(self): """ Returns the state of the device. """ - if self.player_state == 'offline' or self.player_state == None: + if self.player_state == 'offline' or self.player_state is None: return 'offline' if self.player_state == 'error': @@ -196,11 +200,15 @@ class ItunesDevice(MediaPlayerDevice): def media_image_url(self): """ Image url of current playing media. """ - if (self.player_state == STATE_PLAYING or self.player_state == STATE_IDLE or self.player_state == STATE_PAUSED) and self.current_title != None: + if (self.player_state == STATE_PLAYING or + self.player_state == STATE_IDLE or + self.player_state == STATE_PAUSED) and + self.current_title is not None: return self.client.artwork_url() else: - return "https://cloud.githubusercontent.com/assets/260/9829355/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png" - + return 'https://cloud.githubusercontent.com/assets/260/9829355' + '/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png' + @property def media_title(self): """ Title of current playing media. """ From 9d750368ff7b04220d69f0ba1f78b239634d30ee Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Sat, 12 Sep 2015 00:16:51 -0400 Subject: [PATCH 05/10] moar style fixes --- homeassistant/components/media_player/itunes.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 363f34b2063..a430c2b39ac 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -102,10 +102,11 @@ class Itunes(object): return self._base_url + '/artwork' # pylint: disable=unused-argument + + def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the itunes platform. """ - add_devices([ ItunesDevice( config.get('name', 'iTunes'), @@ -200,15 +201,13 @@ class ItunesDevice(MediaPlayerDevice): def media_image_url(self): """ Image url of current playing media. """ - if (self.player_state == STATE_PLAYING or - self.player_state == STATE_IDLE or - self.player_state == STATE_PAUSED) and - self.current_title is not None: + if self.player_state in (STATE_PLAYING, STATE_IDLE, STATE_PAUSED) and \ + self.current_title is not None: return self.client.artwork_url() else: return 'https://cloud.githubusercontent.com/assets/260/9829355' - '/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png' - + '/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png' + @property def media_title(self): """ Title of current playing media. """ From 2b6e0da405c7bbfb62526ea449acdd2dad713ef9 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Sat, 12 Sep 2015 00:23:04 -0400 Subject: [PATCH 06/10] add docstring --- homeassistant/components/media_player/itunes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index a430c2b39ac..3006bed959d 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -46,6 +46,7 @@ SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ class Itunes(object): + """ itunes-api client. """ def __init__(self, host, port): self.host = host From 705238eb780871bf4e7cc5b38f983787b28c1010 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Sat, 12 Sep 2015 00:23:12 -0400 Subject: [PATCH 07/10] dat slash --- homeassistant/components/media_player/itunes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 3006bed959d..9b6daf7b38b 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -206,7 +206,7 @@ class ItunesDevice(MediaPlayerDevice): self.current_title is not None: return self.client.artwork_url() else: - return 'https://cloud.githubusercontent.com/assets/260/9829355' + return 'https://cloud.githubusercontent.com/assets/260/9829355' \ '/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png' @property From 34dee0c134b1866adac8e700fbc9c77fc51e3fae Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Sat, 12 Sep 2015 00:42:11 -0400 Subject: [PATCH 08/10] style and docs --- .../components/media_player/itunes.py | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 9b6daf7b38b..40321511fdc 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -54,56 +54,68 @@ class Itunes(object): @property def _base_url(self): + """ Returns the base url for endpoints. """ return self.host + ":" + str(self.port) def _request(self, method, path, params=None): + """ Makes the actual request and returns the parsed response. """ url = self._base_url + path try: if method == 'GET': - r = requests.get(url) + response = requests.get(url) elif method == "POST": - r = requests.put(url, params) + response = requests.put(url, params) elif method == "PUT": - r = requests.put(url, params) + response = requests.put(url, params) elif method == "DELETE": - r = requests.delete(url) + response = requests.delete(url) - return r.json() + return response.json() except requests.exceptions.HTTPError: return {'player_state': 'error'} except requests.exceptions.RequestException: return {'player_state': 'offline'} def _command(self, named_command): + """ Makes a request for a controlling command. """ return self._request('PUT', '/' + named_command) def now_playing(self): + """ Returns the current state. """ return self._request('GET', '/now_playing') def set_volume(self, level): + """ Sets the volume and returns the current state, level 0-100. """ return self._request('PUT', '/volume', {'level': level}) def set_muted(self, muted): + """ Mutes and returns the current state, muted True or False. """ return self._request('PUT', '/mute', {'muted': muted}) def play(self): + """ Sets playback to play and returns the current state. """ return self._command('play') def pause(self): + """ Sets playback to paused and returns the current state. """ return self._command('pause') def next(self): + """ Skips to the next track and returns the current state. """ return self._command('next') def previous(self): + """ Skips back and returns the current state. """ return self._command('previous') def artwork_url(self): + """ Returns a URL of the current track's album art. """ return self._base_url + '/artwork' # pylint: disable=unused-argument - +# pylint: disable=abstract-method +# pylint: disable=too-many-instance-attributes def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the itunes platform. """ @@ -141,7 +153,8 @@ class ItunesDevice(MediaPlayerDevice): self.update() - def updateState(self, state_hash): + def update_state(self, state_hash): + """ Update all the state properties with the passed in dictionary. """ self.player_state = state_hash.get('player_state', None) self.current_volume = state_hash.get('volume', 0) @@ -178,7 +191,7 @@ class ItunesDevice(MediaPlayerDevice): def update(self): """ Retrieve latest state. """ now_playing = self.client.now_playing() - self.updateState(now_playing) + self.update_state(now_playing) @property def is_volume_muted(self): @@ -187,6 +200,7 @@ class ItunesDevice(MediaPlayerDevice): @property def volume_level(self): + """ Volume level of the media player (0..1). """ return self.current_volume/100.0 @property @@ -196,6 +210,7 @@ class ItunesDevice(MediaPlayerDevice): @property def media_content_type(self): + """ Content type of current playing media. """ return MEDIA_TYPE_MUSIC @property @@ -206,7 +221,7 @@ class ItunesDevice(MediaPlayerDevice): self.current_title is not None: return self.client.artwork_url() else: - return 'https://cloud.githubusercontent.com/assets/260/9829355' \ + return 'https://cloud.githubusercontent.com/assets/260/9829355' '/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png' @property @@ -231,30 +246,30 @@ class ItunesDevice(MediaPlayerDevice): def set_volume_level(self, volume): """ set volume level, range 0..1. """ - r = self.client.set_volume(int(volume * 100)) - self.updateState(r) + response = self.client.set_volume(int(volume * 100)) + self.update_state(response) def mute_volume(self, mute): """ mute (true) or unmute (false) media player. """ - r = self.client.set_muted(mute) - self.updateState(r) + response = self.client.set_muted(mute) + self.update_state(response) def media_play(self): """ media_play media player. """ - r = self.client.play() - self.updateState(r) + response = self.client.play() + self.update_state(response) def media_pause(self): """ media_pause media player. """ - r = self.client.pause() - self.updateState(r) + response = self.client.pause() + self.update_state(response) def media_next_track(self): """ media_next media player. """ - r = self.client.next() - self.updateState(r) + response = self.client.next() + self.update_state(response) def media_previous_track(self): """ media_previous media player. """ - r = self.client.previous() - self.updateState(r) + response = self.client.previous() + self.update_state(response) From f41786d893e5d3e71099cf0462154c047c877d67 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Sat, 12 Sep 2015 00:49:34 -0400 Subject: [PATCH 09/10] STYLE!!!! --- homeassistant/components/media_player/itunes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 40321511fdc..a350de8f2d5 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -117,6 +117,7 @@ class Itunes(object): # pylint: disable=abstract-method # pylint: disable=too-many-instance-attributes + def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the itunes platform. """ @@ -221,8 +222,8 @@ class ItunesDevice(MediaPlayerDevice): self.current_title is not None: return self.client.artwork_url() else: - return 'https://cloud.githubusercontent.com/assets/260/9829355' - '/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png' + return 'https://cloud.githubusercontent.com/assets/260/9829355' \ + '/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png' @property def media_title(self): From 395dbe8804dbb334733f1b271e7abcc34de19ae8 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Sat, 12 Sep 2015 00:50:40 -0400 Subject: [PATCH 10/10] drop the try --- homeassistant/components/media_player/itunes.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index a350de8f2d5..c1a22ec78f9 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -34,10 +34,7 @@ from homeassistant.components.media_player import ( from homeassistant.const import ( STATE_IDLE, STATE_PLAYING, STATE_PAUSED) -try: - import requests -except ImportError: - requests = None +import requests _LOGGER = logging.getLogger(__name__)