""" 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 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 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, MEDIA_TYPE_PLAYLIST, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY_MEDIA, ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_MEDIA_COMMANDS) from homeassistant.const import ( STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_ON) import requests _LOGGER = logging.getLogger(__name__) SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ SUPPORT_PLAY_MEDIA SUPPORT_AIRPLAY = SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_TURN_OFF DOMAIN = 'itunes' class Itunes(object): """ itunes-api client. """ def __init__(self, host, port): self.host = host self.port = port @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': response = requests.get(url) elif method == "POST": response = requests.put(url, params) elif method == "PUT": response = requests.put(url, params) elif method == "DELETE": response = requests.delete(url) 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 play_playlist(self, playlist_id_or_name): """ Sets a playlist to be current and returns the current state. """ response = self._request('GET', '/playlists') playlists = response.get('playlists', []) found_playlists = \ [playlist for playlist in playlists if (playlist_id_or_name in [playlist["name"], playlist["id"]])] if len(found_playlists) > 0: playlist = found_playlists[0] path = '/playlists/' + playlist['id'] + '/play' return self._request('PUT', path) def artwork_url(self): """ 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, device_id): """ Returns an AirPlay device. """ return self._request('GET', '/airplay_devices/' + device_id) 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' path = '/airplay_devices/' + device_id + '/' + command return self._request('PUT', path) def set_volume_airplay_device(self, device_id, level): """ Sets volume, returns current state of device, id,level 0-100. """ path = '/airplay_devices/' + device_id + '/volume' return self._request('PUT', path, {'level': level}) # 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. """ add_devices([ ItunesDevice( config.get('name', 'iTunes'), config.get('host'), config.get('port'), add_devices ) ]) class ItunesDevice(MediaPlayerDevice): """ Represents a iTunes-API instance. """ # pylint: disable=too-many-public-methods 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) 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.airplay_devices = {} self.update() 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) 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 is 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.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. """ return self.muted @property def volume_level(self): """ Volume level of the media player (0..1). """ 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): """ Content type of current playing media. """ return MEDIA_TYPE_MUSIC @property def media_image_url(self): """ Image url of current playing media. """ 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' @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 media_playlist(self): """ Title of the currently playing playlist. """ return self.current_playlist @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. """ response = self.client.set_volume(int(volume * 100)) self.update_state(response) def mute_volume(self, mute): """ mute (true) or unmute (false) media player. """ response = self.client.set_muted(mute) self.update_state(response) def media_play(self): """ media_play media player. """ response = self.client.play() self.update_state(response) def media_pause(self): """ media_pause media player. """ response = self.client.pause() self.update_state(response) def media_next_track(self): """ media_next media player. """ response = self.client.next() self.update_state(response) def media_previous_track(self): """ media_previous media player. """ response = self.client.previous() self.update_state(response) def play_media(self, media_type, media_id): """ play_media media player. """ if media_type == MEDIA_TYPE_PLAYLIST: response = self.client.play_playlist(media_id) 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, device_id, client): self._id = device_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 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) if 'name' in state_hash: name = state_hash.get('name', '') self.device_name = (name + ' AirTunes Speaker').strip() 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)