""" Support for interfacing with the XBMC/Kodi JSON-RPC API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.kodi/ """ import asyncio import logging import urllib import aiohttp import voluptuous as vol from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL, CONF_USERNAME, CONF_PASSWORD) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['jsonrpc-async==0.2'] _LOGGER = logging.getLogger(__name__) CONF_TURN_OFF_ACTION = 'turn_off_action' DEFAULT_NAME = 'Kodi' DEFAULT_PORT = 8080 DEFAULT_TIMEOUT = 5 DEFAULT_SSL = False TURN_OFF_ACTION = [None, 'quit', 'hibernate', 'suspend', 'reboot', 'shutdown'] SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_VOLUME_STEP PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, vol.Optional(CONF_TURN_OFF_ACTION, default=None): vol.In(TURN_OFF_ACTION), vol.Inclusive(CONF_USERNAME, 'auth'): cv.string, vol.Inclusive(CONF_PASSWORD, 'auth'): cv.string, }) @asyncio.coroutine def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Setup the Kodi platform.""" host = config.get(CONF_HOST) port = config.get(CONF_PORT) use_encryption = config.get(CONF_SSL) if host.startswith('http://') or host.startswith('https://'): host = host.lstrip('http://').lstrip('https://') _LOGGER.warning( "Kodi host name should no longer conatin http:// See updated " "definitions here: " "https://home-assistant.io/components/media_player.kodi/") entity = KodiDevice( hass, name=config.get(CONF_NAME), host=host, port=port, encryption=use_encryption, username=config.get(CONF_USERNAME), password=config.get(CONF_PASSWORD), turn_off_action=config.get(CONF_TURN_OFF_ACTION)) yield from async_add_entities([entity], update_before_add=True) class KodiDevice(MediaPlayerDevice): """Representation of a XBMC/Kodi device.""" def __init__(self, hass, name, host, port, encryption=False, username=None, password=None, turn_off_action=None): """Initialize the Kodi device.""" import jsonrpc_async self.hass = hass self._name = name kwargs = { 'timeout': DEFAULT_TIMEOUT, 'session': async_get_clientsession(hass), } if username is not None: kwargs['auth'] = aiohttp.BasicAuth(username, password) image_auth_string = "{}:{}@".format(username, password) else: image_auth_string = "" protocol = 'https' if encryption else 'http' self._http_url = '{}://{}:{}/jsonrpc'.format(protocol, host, port) self._image_url = '{}://{}{}:{}/image'.format( protocol, image_auth_string, host, port) self._server = jsonrpc_async.Server(self._http_url, **kwargs) self._turn_off_action = turn_off_action self._players = list() self._properties = None self._item = None self._app_properties = None @property def name(self): """Return the name of the device.""" return self._name @asyncio.coroutine def _get_players(self): """Return the active player objects or None.""" import jsonrpc_async try: return (yield from self._server.Player.GetActivePlayers()) except jsonrpc_async.jsonrpc.TransportError: if self._players is not None: _LOGGER.info('Unable to fetch kodi data') _LOGGER.debug('Unable to fetch kodi data', exc_info=True) return None @property def state(self): """Return the state of the device.""" if self._players is None: return STATE_OFF if len(self._players) == 0: return STATE_IDLE if self._properties['speed'] == 0 and not self._properties['live']: return STATE_PAUSED else: return STATE_PLAYING @asyncio.coroutine def async_update(self): """Retrieve latest state.""" self._players = yield from self._get_players() if self._players is not None and len(self._players) > 0: player_id = self._players[0]['playerid'] assert isinstance(player_id, int) self._properties = yield from self._server.Player.GetProperties( player_id, ['time', 'totaltime', 'speed', 'live'] ) self._item = (yield from self._server.Player.GetItem( player_id, ['title', 'file', 'uniqueid', 'thumbnail', 'artist'] ))['item'] self._app_properties = \ yield from self._server.Application.GetProperties( ['volume', 'muted'] ) else: self._properties = None self._item = None self._app_properties = None @property def volume_level(self): """Volume level of the media player (0..1).""" if self._app_properties is not None: return self._app_properties['volume'] / 100.0 @property def is_volume_muted(self): """Boolean if volume is currently muted.""" if self._app_properties is not None: return self._app_properties['muted'] @property def media_content_id(self): """Content ID of current playing media.""" if self._item is not None: return self._item.get('uniqueid', None) @property def media_content_type(self): """Content type of current playing media.""" if self._players is not None and len(self._players) > 0: return self._players[0]['type'] @property def media_duration(self): """Duration of current playing media in seconds.""" if self._properties is not None and not self._properties['live']: total_time = self._properties['totaltime'] return ( total_time['hours'] * 3600 + total_time['minutes'] * 60 + total_time['seconds']) @property def media_image_url(self): """Image url of current playing media.""" if self._item is None: return None url_components = urllib.parse.urlparse(self._item['thumbnail']) if url_components.scheme == 'image': return '{}/{}'.format( self._image_url, urllib.parse.quote_plus(self._item['thumbnail'])) @property def media_title(self): """Title of current playing media.""" # find a string we can use as a title if self._item is not None: return self._item.get( 'title', self._item.get('label', self._item.get('file', 'unknown'))) @property def supported_features(self): """Flag media player features that are supported.""" supported_features = SUPPORT_KODI if self._turn_off_action in TURN_OFF_ACTION: supported_features |= SUPPORT_TURN_OFF return supported_features @asyncio.coroutine def async_turn_off(self): """Execute turn_off_action to turn off media player.""" if self._turn_off_action == 'quit': yield from self._server.Application.Quit() elif self._turn_off_action == 'hibernate': yield from self._server.System.Hibernate() elif self._turn_off_action == 'suspend': yield from self._server.System.Suspend() elif self._turn_off_action == 'reboot': yield from self._server.System.Reboot() elif self._turn_off_action == 'shutdown': yield from self._server.System.Shutdown() else: _LOGGER.warning('turn_off requested but turn_off_action is none') @asyncio.coroutine def async_volume_up(self): """Volume up the media player.""" assert ( yield from self._server.Input.ExecuteAction('volumeup')) == 'OK' @asyncio.coroutine def async_volume_down(self): """Volume down the media player.""" assert ( yield from self._server.Input.ExecuteAction('volumedown')) == 'OK' def async_set_volume_level(self, volume): """Set volume level, range 0..1. This method must be run in the event loop and returns a coroutine. """ return self._server.Application.SetVolume(int(volume * 100)) def async_mute_volume(self, mute): """Mute (true) or unmute (false) media player. This method must be run in the event loop and returns a coroutine. """ return self._server.Application.SetMute(mute) @asyncio.coroutine def async_set_play_state(self, state): """Helper method for play/pause/toggle.""" players = yield from self._get_players() if len(players) != 0: yield from self._server.Player.PlayPause( players[0]['playerid'], state) def async_media_play_pause(self): """Pause media on media player. This method must be run in the event loop and returns a coroutine. """ return self.async_set_play_state('toggle') def async_media_play(self): """Play media. This method must be run in the event loop and returns a coroutine. """ return self.async_set_play_state(True) def async_media_pause(self): """Pause the media player. This method must be run in the event loop and returns a coroutine. """ return self.async_set_play_state(False) @asyncio.coroutine def async_media_stop(self): """Stop the media player.""" players = yield from self._get_players() if len(players) != 0: yield from self._server.Player.Stop(players[0]['playerid']) @asyncio.coroutine def _goto(self, direction): """Helper method used for previous/next track.""" players = yield from self._get_players() if len(players) != 0: if direction == 'previous': # first seek to position 0. Kodi goes to the beginning of the # current track if the current track is not at the beginning. yield from self._server.Player.Seek(players[0]['playerid'], 0) yield from self._server.Player.GoTo( players[0]['playerid'], direction) def async_media_next_track(self): """Send next track command. This method must be run in the event loop and returns a coroutine. """ return self._goto('next') def async_media_previous_track(self): """Send next track command. This method must be run in the event loop and returns a coroutine. """ return self._goto('previous') @asyncio.coroutine def async_media_seek(self, position): """Send seek command.""" players = yield from self._get_players() time = {} time['milliseconds'] = int((position % 1) * 1000) position = int(position) time['seconds'] = int(position % 60) position /= 60 time['minutes'] = int(position % 60) position /= 60 time['hours'] = int(position) if len(players) != 0: yield from self._server.Player.Seek(players[0]['playerid'], time) def async_play_media(self, media_type, media_id, **kwargs): """Send the play_media command to the media player. This method must be run in the event loop and returns a coroutine. """ if media_type == "CHANNEL": return self._server.Player.Open( {"item": {"channelid": int(media_id)}}) else: return self._server.Player.Open( {"item": {"file": str(media_id)}})