diff --git a/README.md b/README.md index 05fe01340bc..a50962e659f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It offers the following functionality through built-in components: * Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), and [DD-WRT](http://www.dd-wrt.com/site/index)) * Track and control [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors - * Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast) and [Music Player Daemon](http://www.musicpd.org/) + * Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/) and XBMC/Kodi (http://kodi.tv/) * 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/), and [Modbus](http://www.modbus.org/) * Track running system services and monitoring your system stats (Memory, disk usage, and more) * Control low-cost 433 MHz remote control wall-socket devices (https://github.com/r10r/rcswitch-pi) and other switches that can be turned on/off with shell commands diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 90c1f126117..cf35506d009 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -1,23 +1,70 @@ """ -homeassistant.components.media_player.demo +homeassistant.components.media_player.kodi ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Demo implementation of the media player. +Provides an interface to the XBMC/Kodi JSON-RPC API +Configuration: + +To use Kodi add something like this to your configuration: + +media_player: + platform: kodi + name: Kodi + url: http://192.168.0.123/jsonrpc + user: kodi + password: my_secure_password + +Variables: + +name +*Optional +The name of the device + +url +*Required +The URL of the XBMC/Kodi JSON-RPC API. Example: http://192.168.0.123/jsonrpc + +user +*Optional +The XBMC/Kodi HTTP username + +password +*Optional +The XBMC/Kodi HTTP password """ + +import urllib +import logging + from homeassistant.components.media_player import ( - MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE, - ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_TITLE, ATTR_MEDIA_DURATION, - ATTR_MEDIA_VOLUME, MEDIA_STATE_PLAYING, MEDIA_STATE_STOPPED, - ATTR_MEDIA_IS_VOLUME_MUTED, MEDIA_STATE_PAUSED) -from homeassistant.const import ATTR_ENTITY_PICTURE -import jsonrpc_requests + MediaPlayerDevice, 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 jsonrpc_requests +except ImportError: + jsonrpc_requests = None + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the kodi platform. """ + if jsonrpc_requests is None: + _LOGGER.exception( + "Unable to import jsonrpc_requests. " + "Did you maybe not install the 'jsonrpc-requests' pip module?") + + return False + add_devices([ KodiDevice( config.get('name', 'Kodi'), @@ -28,18 +75,29 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ]) +def _get_image_url(kodi_url): + """ Helper function that parses the thumbnail URLs used by Kodi """ + url_components = urllib.parse.urlparse(kodi_url) + + if url_components.scheme == 'image': + return urllib.parse.unquote(url_components.netloc) + + class KodiDevice(MediaPlayerDevice): - """ TODO. """ + """ Represents a XBMC/Kodi device. """ + + # pylint: disable=too-many-public-methods def __init__(self, name, url, auth=None): self._name = name self._url = url self._server = jsonrpc_requests.Server(url, auth=auth) + self._players = None + self._properties = None + self._item = None + self._app_properties = None - @property - def should_poll(self): - """ TODO. """ - return True + self.update() @property def name(self): @@ -47,69 +105,108 @@ class KodiDevice(MediaPlayerDevice): return self._name def _get_players(self): - return self._server.Player.GetActivePlayers() + """ Returns the active player objects or None """ + try: + return self._server.Player.GetActivePlayers() + except (OSError, ConnectionError): + # OSError may be raised for "No route to host" + return None @property def state(self): """ Returns the state of the device. """ - players = self._get_players() + if self._players is None: + return STATE_OFF - if len(players) == 0: - return STATE_NO_APP + if len(self._players) == 0: + return STATE_IDLE - return players[0]['type'] + if self._properties['speed'] == 0: + return STATE_PAUSED + else: + return STATE_PLAYING + + def update(self): + """ Retrieve latest state. """ + self._players = 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( + player_id, + ['time', 'totaltime', 'speed'] + ) + + self._item = self._server.Player.GetItem( + player_id, + ['title', 'file', 'uniqueid', 'thumbnail', 'artist'] + )['item'] + + self._app_properties = self._server.Application.GetProperties( + ['volume', 'muted'] + ) @property - def state_attributes(self): - """ Returns the state attributes. """ + 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 - players = self._get_players() + @property + def is_volume_muted(self): + """ Boolean if volume is currently muted. """ + if self._app_properties is not None: + return self._app_properties['muted'] - if len(players) == 0: - return { - ATTR_MEDIA_STATE: MEDIA_STATE_STOPPED - } + @property + def media_content_id(self): + """ Content ID of current playing media. """ + if self._item is not None: + return self._item['uniqueid'] - player_id = players[0]['playerid'] + @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'] - assert isinstance(player_id, int) + @property + def media_duration(self): + """ Duration of current playing media in seconds. """ + if self._properties is not None: + total_time = self._properties['totaltime'] - item = self._server.Player.GetItem( - player_id, - ['title', 'file', 'uniqueid', 'thumbnail', 'artist'])[ - 'item'] - - properties = self._server.Player.GetProperties( - player_id, - ['time', 'totaltime', 'speed']) - - app_properties = self._server.Application.GetProperties( - ['volume', 'muted']) - - # find a string we can use as a title - title = item.get('title', - item.get('label', - item.get('file', 'unknown'))) - - total_time = properties['totaltime'] - - return { - ATTR_MEDIA_CONTENT_ID: item['uniqueid'], - ATTR_MEDIA_TITLE: title, - ATTR_MEDIA_DURATION: + return ( total_time['hours'] * 3600 + total_time['minutes'] * 60 + - total_time['seconds'], - ATTR_MEDIA_VOLUME: - app_properties['volume'] / 100.0, - ATTR_MEDIA_IS_VOLUME_MUTED: - app_properties['muted'], - ATTR_ENTITY_PICTURE: - item['thumbnail'], - ATTR_MEDIA_STATE: - MEDIA_STATE_PAUSED if properties['speed'] == 0 - else MEDIA_STATE_PLAYING, - } + total_time['seconds']) + + @property + def media_image_url(self): + """ Image url of current playing media. """ + if self._item is not None: + return _get_image_url(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_media_commands(self): + """ Flags of media commands that are supported. """ + return SUPPORT_KODI def turn_off(self): """ turn_off media player. """ @@ -126,17 +223,18 @@ class KodiDevice(MediaPlayerDevice): assert self._server.Input.ExecuteAction('volumedown') == 'OK' self.update_ha_state() - def volume_set(self, volume): - """ TODO. """ + def set_volume_level(self, volume): + """ set volume level, range 0..1. """ self._server.Application.SetVolume(int(volume * 100)) self.update_ha_state() - def volume_mute(self, mute): + def mute_volume(self, mute): """ mute (true) or unmute (false) media player. """ self._server.Application.SetMute(mute) self.update_ha_state() def _set_play_state(self, state): + """ Helper method for play/pause/toggle """ players = self._get_players() if len(players) != 0: @@ -156,16 +254,52 @@ class KodiDevice(MediaPlayerDevice): """ media_pause media player. """ self._set_play_state(False) - def _goto(self, to): + def _goto(self, direction): + """ Helper method used for previous/next track """ players = self._get_players() if len(players) != 0: - self._server.Player.GoTo(players[0]['playerid'], to) + self._server.Player.GoTo(players[0]['playerid'], direction) self.update_ha_state() def media_next_track(self): + """ Send next track command. """ self._goto('next') - def media_prev_track(self): - self._goto('previous') \ No newline at end of file + 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') + + def media_seek(self, position): + """ Send seek command. """ + players = 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: + self._server.Player.Seek(players[0]['playerid'], time) + + self.update_ha_state() + + def turn_on(self): + """ turn the media player on. """ + raise NotImplementedError() + + def play_youtube(self, media_id): + """ Plays a YouTube media. """ + raise NotImplementedError()