diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 0a88336b5a3..abab433eb38 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -4,9 +4,11 @@ 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 ( @@ -16,9 +18,10 @@ from homeassistant.components.media_player import ( from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, CONF_PORT, CONF_USERNAME, CONF_PASSWORD) +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['jsonrpc-requests==0.3'] +REQUIREMENTS = ['jsonrpc-async==0.1'] _LOGGER = logging.getLogger(__name__) @@ -26,6 +29,7 @@ CONF_TURN_OFF_ACTION = 'turn_off_action' DEFAULT_NAME = 'Kodi' DEFAULT_PORT = 8080 +DEFAULT_TIMEOUT = 5 TURN_OFF_ACTION = [None, 'quit', 'hibernate', 'suspend', 'reboot', 'shutdown'] @@ -43,7 +47,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_entities, discovery_info=None): +@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) @@ -55,29 +61,34 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "definitions here: " "https://home-assistant.io/components/media_player.kodi/") - add_entities([ - KodiDevice( - config.get(CONF_NAME), - host=host, port=port, - username=config.get(CONF_USERNAME), - password=config.get(CONF_PASSWORD), - turn_off_action=config.get(CONF_TURN_OFF_ACTION)), - ]) + entity = KodiDevice( + hass, + name=config.get(CONF_NAME), + host=host, port=port, + 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, name, host, port, username=None, password=None, + def __init__(self, hass, name, host, port, username=None, password=None, turn_off_action=None): """Initialize the Kodi device.""" - import jsonrpc_requests + import jsonrpc_async + self.hass = hass self._name = name - kwargs = {'timeout': 5} + kwargs = { + 'timeout': DEFAULT_TIMEOUT, + 'session': async_get_clientsession(hass), + } if username is not None: - kwargs['auth'] = (username, password) + kwargs['auth'] = aiohttp.BasicAuth(username, password) image_auth_string = "{}:{}@".format(username, password) else: image_auth_string = "" @@ -86,26 +97,26 @@ class KodiDevice(MediaPlayerDevice): self._image_url = 'http://{}{}:{}/image'.format( image_auth_string, host, port) - self._server = jsonrpc_requests.Server(self._http_url, **kwargs) + 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 - self.update() @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_requests + import jsonrpc_async try: - return self._server.Player.GetActivePlayers() - except jsonrpc_requests.jsonrpc.TransportError: + 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) @@ -125,28 +136,30 @@ class KodiDevice(MediaPlayerDevice): else: return STATE_PLAYING - def update(self): + @asyncio.coroutine + def async_update(self): """Retrieve latest state.""" - self._players = self._get_players() + 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 = self._server.Player.GetProperties( + self._properties = yield from self._server.Player.GetProperties( player_id, ['time', 'totaltime', 'speed', 'live'] ) - self._item = self._server.Player.GetItem( + self._item = (yield from self._server.Player.GetItem( player_id, ['title', 'file', 'uniqueid', 'thumbnail', 'artist'] - )['item'] + ))['item'] - self._app_properties = self._server.Application.GetProperties( - ['volume', 'muted'] - ) + self._app_properties = \ + yield from self._server.Application.GetProperties( + ['volume', 'muted'] + ) else: self._properties = None self._item = None @@ -218,94 +231,118 @@ class KodiDevice(MediaPlayerDevice): return supported_media_commands - def turn_off(self): + @asyncio.coroutine + def async_turn_off(self): """Execute turn_off_action to turn off media player.""" if self._turn_off_action == 'quit': - self._server.Application.Quit() + yield from self._server.Application.Quit() elif self._turn_off_action == 'hibernate': - self._server.System.Hibernate() + yield from self._server.System.Hibernate() elif self._turn_off_action == 'suspend': - self._server.System.Suspend() + yield from self._server.System.Suspend() elif self._turn_off_action == 'reboot': - self._server.System.Reboot() + yield from self._server.System.Reboot() elif self._turn_off_action == 'shutdown': - self._server.System.Shutdown() + yield from self._server.System.Shutdown() else: _LOGGER.warning('turn_off requested but turn_off_action is none') - self.update_ha_state() - - def volume_up(self): + @asyncio.coroutine + def async_volume_up(self): """Volume up the media player.""" - assert self._server.Input.ExecuteAction('volumeup') == 'OK' - self.update_ha_state() + assert ( + yield from self._server.Input.ExecuteAction('volumeup')) == 'OK' - def volume_down(self): + @asyncio.coroutine + def async_volume_down(self): """Volume down the media player.""" - assert self._server.Input.ExecuteAction('volumedown') == 'OK' - self.update_ha_state() + assert ( + yield from self._server.Input.ExecuteAction('volumedown')) == 'OK' - def set_volume_level(self, volume): - """Set volume level, range 0..1.""" - self._server.Application.SetVolume(int(volume * 100)) - self.update_ha_state() + def async_set_volume_level(self, volume): + """Set volume level, range 0..1. - def mute_volume(self, mute): - """Mute (true) or unmute (false) media player.""" - self._server.Application.SetMute(mute) - self.update_ha_state() + This method must be run in the event loop and returns a coroutine. + """ + return self._server.Application.SetVolume(int(volume * 100)) - def _set_play_state(self, state): + 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 = self._get_players() + players = yield from self._get_players() if len(players) != 0: - self._server.Player.PlayPause(players[0]['playerid'], state) + yield from self._server.Player.PlayPause( + players[0]['playerid'], state) - self.update_ha_state() + def async_media_play_pause(self): + """Pause media on media player. - def media_play_pause(self): - """Pause media on media player.""" - self._set_play_state('toggle') + This method must be run in the event loop and returns a coroutine. + """ + return self.async_set_play_state('toggle') - def media_play(self): - """Play media.""" - self._set_play_state(True) + def async_media_play(self): + """Play media. - def media_pause(self): - """Pause the media player.""" - self._set_play_state(False) + This method must be run in the event loop and returns a coroutine. + """ + return self.async_set_play_state(True) - def media_stop(self): + 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 = self._get_players() + players = yield from self._get_players() if len(players) != 0: - self._server.Player.Stop(players[0]['playerid']) + yield from self._server.Player.Stop(players[0]['playerid']) + @asyncio.coroutine def _goto(self, direction): """Helper method used for previous/next track.""" - players = self._get_players() + players = yield from self._get_players() if len(players) != 0: - self._server.Player.GoTo(players[0]['playerid'], direction) + 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) - self.update_ha_state() + yield from self._server.Player.GoTo( + players[0]['playerid'], direction) - def media_next_track(self): - """Send next track command.""" - self._goto('next') + def async_media_next_track(self): + """Send next track command. - def media_previous_track(self): - """Send next track command.""" - # first seek to position 0, Kodi seems to go to the beginning - # of the current track current track is not at the beginning - self.media_seek(0) - self._goto('previous') + This method must be run in the event loop and returns a coroutine. + """ + return self._goto('next') - def media_seek(self, position): + 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 = self._get_players() + players = yield from self._get_players() time = {} @@ -321,13 +358,16 @@ class KodiDevice(MediaPlayerDevice): time['hours'] = int(position) if len(players) != 0: - self._server.Player.Seek(players[0]['playerid'], time) + yield from self._server.Player.Seek(players[0]['playerid'], time) - self.update_ha_state() + def async_play_media(self, media_type, media_id, **kwargs): + """Send the play_media command to the media player. - def 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": - self._server.Player.Open({"item": {"channelid": int(media_id)}}) + return self._server.Player.Open( + {"item": {"channelid": int(media_id)}}) else: - self._server.Player.Open({"item": {"file": str(media_id)}}) + return self._server.Player.Open( + {"item": {"file": str(media_id)}}) diff --git a/requirements_all.txt b/requirements_all.txt index dd43d13fd4f..9892551f9f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -272,6 +272,8 @@ influxdb==3.0.0 insteon_hub==0.4.5 # homeassistant.components.media_player.kodi +jsonrpc-async==0.1 + # homeassistant.components.notify.kodi jsonrpc-requests==0.3