From 50b23e19695c5369a9d990452d85e3cd5dfece69 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 17:27:00 -0400 Subject: [PATCH 1/3] adds airplay speakers as media_players --- .../components/media_player/itunes.py | 168 +++++++++++++++++- 1 file changed, 164 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index c1a22ec78f9..7bb58e23a51 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -30,9 +30,10 @@ 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) + SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_MEDIA_COMMANDS) from homeassistant.const import ( - STATE_IDLE, STATE_PLAYING, STATE_PAUSED) + STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_ON) import requests @@ -41,6 +42,10 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK +SUPPORT_AIRPLAY = SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_TURN_OFF + +DOMAIN = 'itunes' + class Itunes(object): """ itunes-api client. """ @@ -110,6 +115,24 @@ class Itunes(object): """ Returns a URL of the current track's album art. """ return self._base_url + '/artwork' + def airplay_devices(self): + """ Returns a list of AirPlay devices. """ + return self._request('GET', '/airplay_devices') + + def airplay_device(self, id): + """ Returns an AirPlay device. """ + return self._request('GET', '/airplay_devices/' + id) + + def toggle_airplay_device(self, id, toggle): + """ Toggles airplay device on or off, id, toggle True or False. """ + command = 'on' if toggle else 'off' + return self._request('PUT', '/airplay_devices/' + id + '/' + command) + + def set_volume_airplay_device(self, id, level): + """ Sets volume, returns current state of device, id,level 0-100. """ + path = '/airplay_devices/' + id + '/volume' + return self._request('PUT', path, {'level': level}) + # pylint: disable=unused-argument # pylint: disable=abstract-method # pylint: disable=too-many-instance-attributes @@ -122,7 +145,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ItunesDevice( config.get('name', 'iTunes'), config.get('host'), - config.get('port') + config.get('port'), + add_devices ) ]) @@ -132,10 +156,11 @@ class ItunesDevice(MediaPlayerDevice): # pylint: disable=too-many-public-methods - def __init__(self, name, host, port): + def __init__(self, name, host, port, add_devices): self._name = name self._host = host self._port = port + self._add_devices = add_devices self.client = Itunes(self._host, self._port) @@ -149,6 +174,8 @@ class ItunesDevice(MediaPlayerDevice): self.player_state = None + self.airplay_devices = {} + self.update() def update_state(self, state_hash): @@ -191,6 +218,28 @@ class ItunesDevice(MediaPlayerDevice): now_playing = self.client.now_playing() self.update_state(now_playing) + found_devices = self.client.airplay_devices() + found_devices = found_devices.get('airplay_devices', []) + + new_devices = [] + + for device_data in found_devices: + device_id = device_data.get('id') + + if self.airplay_devices.get(device_id): + # update it + airplay_device = self.airplay_devices.get(device_id) + airplay_device.update_state(device_data) + else: + # add it + airplay_device = AirPlayDevice(device_id, self.client) + airplay_device.update_state(device_data) + self.airplay_devices[device_id] = airplay_device + new_devices.append(airplay_device) + + if new_devices: + self._add_devices(new_devices) + @property def is_volume_muted(self): """ Boolean if volume is currently muted. """ @@ -271,3 +320,114 @@ class ItunesDevice(MediaPlayerDevice): """ media_previous media player. """ response = self.client.previous() self.update_state(response) + + +class AirPlayDevice(MediaPlayerDevice): + """ Represents an AirPlay device via an iTunes-API instance. """ + + # pylint: disable=too-many-public-methods + + def __init__(self, id, client): + self._id = id + + self.client = client + + self.device_name = "AirPlay" + self.kind = None + self.active = False + self.selected = False + self.volume = 0 + self.supports_audio = False + self.supports_video = False + + def update_state(self, state_hash): + if 'player_state' in state_hash: + self.player_state = state_hash.get('player_state', None) + + if 'name' in state_hash: + self.device_name = state_hash.get('name', 'AirPlay') + + if 'kind' in state_hash: + self.kind = state_hash.get('kind', None) + + if 'active' in state_hash: + self.active = state_hash.get('active', None) + + if 'selected' in state_hash: + self.selected = state_hash.get('selected', None) + + if 'sound_volume' in state_hash: + self.volume = state_hash.get('sound_volume', 0) + + if 'supports_audio' in state_hash: + self.supports_audio = state_hash.get('supports_audio', None) + + if 'supports_video' in state_hash: + self.supports_video = state_hash.get('supports_video', None) + + @property + def name(self): + """ Returns the name of the device. """ + return self.device_name + + @property + def state(self): + """ Returns the state of the device. """ + + if self.selected is True: + return STATE_ON + else: + return STATE_OFF + + def update(self): + """ Retrieve latest state. """ + + @property + def volume_level(self): + return float(self.volume)/100.0 + + @property + def media_content_type(self): + return MEDIA_TYPE_MUSIC + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return SUPPORT_AIRPLAY + + @property + def device_state_attributes(self): + """ Return the state attributes. """ + state_attr = {} + state_attr[ATTR_SUPPORTED_MEDIA_COMMANDS] = SUPPORT_AIRPLAY + + if self.state == STATE_OFF: + state_attr[ATTR_ENTITY_PICTURE] = \ + ('https://cloud.githubusercontent.com/assets/260/9833073' + '/6eb5c906-5958-11e5-9b4a-472cdf36be16.png') + else: + state_attr[ATTR_ENTITY_PICTURE] = \ + ('https://cloud.githubusercontent.com/assets/260/9833072' + '/6eb13cce-5958-11e5-996f-e2aaefbc9a24.png') + + return state_attr + + def set_volume_level(self, volume): + """ set volume level, range 0..1. """ + volume = int(volume * 100) + response = self.client.set_volume_airplay_device(self._id, volume) + self.update_state(response) + + def turn_on(self): + """ Select AirPlay. """ + self.update_state({"selected": True}) + self.update_ha_state() + response = self.client.toggle_airplay_device(self._id, True) + self.update_state(response) + + def turn_off(self): + """ Deselect AirPlay. """ + self.update_state({"selected": False}) + self.update_ha_state() + response = self.client.toggle_airplay_device(self._id, False) + self.update_state(response) From fcbeddeb57cfc867574df932a39886019b49e929 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 17:34:57 -0400 Subject: [PATCH 2/3] describe airplay part --- homeassistant/components/media_player/itunes.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 7bb58e23a51..eddffd843df 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -3,6 +3,13 @@ homeassistant.components.media_player.itunes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Provides an interface to iTunes-API (https://github.com/maddox/itunes-api) +The iTunes media player will allow you to control your iTunes instance. You +can play/pause/next/previous/mute, adjust volume, etc. + +In addition to controlling iTunes, your available AirPlay endpoints will be +added as media players as well. You can then individually address them append +turn them on, turn them off, or adjust their volume. + Configuration: To use iTunes you will need to add something like the following to From e8c3eaab335d2ce148d48dedbaa737c011bc75c5 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 17:39:43 -0400 Subject: [PATCH 3/3] style tweaks --- .../components/media_player/itunes.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index eddffd843df..f63b6c933fe 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -126,18 +126,19 @@ class Itunes(object): """ Returns a list of AirPlay devices. """ return self._request('GET', '/airplay_devices') - def airplay_device(self, id): + def airplay_device(self, device_id): """ Returns an AirPlay device. """ - return self._request('GET', '/airplay_devices/' + id) + return self._request('GET', '/airplay_devices/' + device_id) - def toggle_airplay_device(self, id, toggle): + def toggle_airplay_device(self, device_id, toggle): """ Toggles airplay device on or off, id, toggle True or False. """ command = 'on' if toggle else 'off' - return self._request('PUT', '/airplay_devices/' + id + '/' + command) + path = '/airplay_devices/' + device_id + '/' + command + return self._request('PUT', path) - def set_volume_airplay_device(self, id, level): + def set_volume_airplay_device(self, device_id, level): """ Sets volume, returns current state of device, id,level 0-100. """ - path = '/airplay_devices/' + id + '/volume' + path = '/airplay_devices/' + device_id + '/volume' return self._request('PUT', path, {'level': level}) # pylint: disable=unused-argument @@ -334,8 +335,8 @@ class AirPlayDevice(MediaPlayerDevice): # pylint: disable=too-many-public-methods - def __init__(self, id, client): - self._id = id + def __init__(self, device_id, client): + self._id = device_id self.client = client @@ -346,8 +347,11 @@ class AirPlayDevice(MediaPlayerDevice): self.volume = 0 self.supports_audio = False self.supports_video = False + self.player_state = None def update_state(self, state_hash): + """ Update all the state properties with the passed in dictionary. """ + if 'player_state' in state_hash: self.player_state = state_hash.get('player_state', None)