From a59644c58d03e94215c5377dd38d6a8cacf989f7 Mon Sep 17 00:00:00 2001 From: azogue Date: Mon, 15 May 2017 11:05:19 +0200 Subject: [PATCH 1/5] Kodi specific services to call Kodi API methods - new service: `kodi_execute_addon` to run a Kodi Addon with optional parameters. Results of the Kodi API call, if any, are redirected in a Home Assistant event: `kodi_execute_addon_result`. - new service: `kodi_run_method` to run a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call are redirected in a Home Assistant event: `kodi_run_method_result`. - Add descriptions in services.yaml. - Add `timeout` parameter to yaml config (needed to make slow queries to the JSONRPC API, default timeout is set to 5s). - Trigger events with the results of the Kodi API calls, with: ``` event_data = { 'result': api_call_results, 'result_ok': boolean, 'input': api_call_parameters, 'entity_id': 'media_player.kodi'} ``` --- homeassistant/components/media_player/kodi.py | 93 ++++++++++++++++++- .../components/media_player/services.yaml | 22 +++++ 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 18c01c396acdbb..1863fb6075cb8f 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -5,6 +5,7 @@ https://home-assistant.io/components/media_player.kodi/ """ import asyncio +from collections import OrderedDict from functools import wraps import logging import urllib @@ -24,7 +25,7 @@ from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD, - EVENT_HOMEASSISTANT_STOP) + CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -34,6 +35,9 @@ _LOGGER = logging.getLogger(__name__) +EVENT_KODI_RUN_METHOD_RESULT = 'kodi_run_method_result' +EVENT_KODI_EXEC_ADDON_RESULT = 'kodi_execute_addon_result' + CONF_TCP_PORT = 'tcp_port' CONF_TURN_OFF_ACTION = 'turn_off_action' CONF_ENABLE_WEBSOCKET = 'enable_websocket' @@ -74,6 +78,7 @@ vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port, vol.Optional(CONF_PROXY_SSL, default=DEFAULT_PROXY_SSL): cv.boolean, vol.Optional(CONF_TURN_OFF_ACTION, default=None): vol.In(TURN_OFF_ACTION), + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Inclusive(CONF_USERNAME, 'auth'): cv.string, vol.Inclusive(CONF_PASSWORD, 'auth'): cv.string, vol.Optional(CONF_ENABLE_WEBSOCKET, default=DEFAULT_ENABLE_WEBSOCKET): @@ -81,6 +86,8 @@ }) SERVICE_ADD_MEDIA = 'kodi_add_to_playlist' +SERVICE_RUN_METHOD = 'kodi_run_method' +SERVICE_EXEC_ADDON = 'kodi_execute_addon' DATA_KODI = 'kodi' @@ -88,6 +95,8 @@ ATTR_MEDIA_NAME = 'media_name' ATTR_MEDIA_ARTIST_NAME = 'artist_name' ATTR_MEDIA_ID = 'media_id' +ATTR_METHOD = 'method' +ATTR_ADDONID = 'addonid' MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ vol.Required(ATTR_MEDIA_TYPE): cv.string, @@ -95,11 +104,23 @@ vol.Optional(ATTR_MEDIA_NAME): cv.string, vol.Optional(ATTR_MEDIA_ARTIST_NAME): cv.string, }) +MEDIA_PLAYER_RUN_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ + vol.Required(ATTR_METHOD): cv.string, +}, extra=vol.ALLOW_EXTRA) +MEDIA_PLAYER_EXEC_ADDON_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ + vol.Required(ATTR_ADDONID): cv.string, +}, extra=vol.ALLOW_EXTRA) SERVICE_TO_METHOD = { SERVICE_ADD_MEDIA: { 'method': 'async_add_media_to_playlist', 'schema': MEDIA_PLAYER_ADD_MEDIA_SCHEMA}, + SERVICE_RUN_METHOD: { + 'method': 'async_run_method', + 'schema': MEDIA_PLAYER_RUN_METHOD_SCHEMA}, + SERVICE_EXEC_ADDON: { + 'method': 'async_exec_addon', + 'schema': MEDIA_PLAYER_EXEC_ADDON_SCHEMA}, } @@ -127,7 +148,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): host=host, port=port, tcp_port=tcp_port, encryption=encryption, username=config.get(CONF_USERNAME), password=config.get(CONF_PASSWORD), - turn_off_action=config.get(CONF_TURN_OFF_ACTION), websocket=websocket) + turn_off_action=config.get(CONF_TURN_OFF_ACTION), + timeout=config.get(CONF_TIMEOUT), websocket=websocket) hass.data[DATA_KODI].append(entity) async_add_devices([entity], update_before_add=True) @@ -194,12 +216,20 @@ def wrapper(obj, *args, **kwargs): return wrapper +def _ordereddict_to_dict(params): + """Recursive method to clean kwargs before calling the Kodi API.""" + for key, value in params.items(): + if isinstance(value, OrderedDict): + params[key] = _ordereddict_to_dict(value) + return dict(params) + + class KodiDevice(MediaPlayerDevice): """Representation of a XBMC/Kodi device.""" def __init__(self, hass, name, host, port, tcp_port, encryption=False, username=None, password=None, turn_off_action=None, - websocket=True): + timeout=DEFAULT_TIMEOUT, websocket=True): """Initialize the Kodi device.""" import jsonrpc_async import jsonrpc_websocket @@ -207,7 +237,7 @@ def __init__(self, hass, name, host, port, tcp_port, encryption=False, self._name = name kwargs = { - 'timeout': DEFAULT_TIMEOUT, + 'timeout': timeout, 'session': async_get_clientsession(hass), } @@ -678,6 +708,61 @@ def async_set_shuffle(self, shuffle): yield from self.server.Player.SetShuffle( {"playerid": self._players[0]['playerid'], "shuffle": shuffle}) + @asyncio.coroutine + def async_run_method(self, method, **kwargs): + """Run Kodi JSONRPC API method with params.""" + import jsonrpc_base + _LOGGER.debug('Run API method "%s", kwargs=%s', method, kwargs) + params, result_ok = None, False + try: + if kwargs: + params = _ordereddict_to_dict(kwargs) + result = yield from getattr(self.server, method)(params) + else: + result = yield from getattr(self.server, method)() + result_ok = True + except jsonrpc_base.jsonrpc.ProtocolError as exc: + result = exc.args[2]['error'] + _LOGGER.error('Run API method %s.%s(%s) error: %s', + self.entity_id, method, params, result) + + if isinstance(result, dict): + event_data = {'entity_id': self.entity_id, + 'result': result, + 'result_ok': result_ok, + 'input': {'method': method, 'params': params}} + _LOGGER.debug('EVENT kodi_run_method_result: %s', event_data) + self.hass.bus.async_fire(EVENT_KODI_RUN_METHOD_RESULT, + event_data=event_data) + return result + + @asyncio.coroutine + def async_exec_addon(self, addonid, **kwargs): + """Execute Kodi addon with optional params.""" + import jsonrpc_base + _LOGGER.debug('Kodi execute addon "%s", kwargs=%s', addonid, kwargs) + params = {"addonid": addonid} + result_ok = False + if kwargs: + params.update(_ordereddict_to_dict(kwargs)) + try: + result = yield from self.server.Addons.ExecuteAddon(params) + result_ok = True + except jsonrpc_base.jsonrpc.ProtocolError as exc: + result = exc.args[2]['error'] + _LOGGER.error('Execute addon %s.%s(%s) error: %s', + self.entity_id, addonid, params, result) + + if isinstance(result, dict): + event_data = {'entity_id': self.entity_id, + 'result': result, + 'result_ok': result_ok, + 'input': params} + _LOGGER.debug('EVENT kodi_execute_addon_result: %s', event_data) + self.hass.bus.async_fire(EVENT_KODI_EXEC_ADDON_RESULT, + event_data=event_data) + return result + @asyncio.coroutine def async_add_media_to_playlist( self, media_type, media_id=None, media_name='ALL', artist_name=''): diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 4d5f85c05eb825..1415ef115399e2 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -289,3 +289,25 @@ kodi_add_to_playlist: artist_name: description: Optional artist name for filtering media. example: 'AC/DC' + +kodi_execute_addon: + description: 'Execute a Kodi addon with optional parameters. Results of the Kodi API call, if any, will be redirected in a Home Assistant event: `kodi_execute_addon_result`.' + + fields: + entity_id: + description: Name(s) of the Kodi entities where to execute the (pre-installed) Kodi Addon. + example: 'media_player.living_room_kodi' + addonid: + description: Name of the Kodi addon. + example: 'script.json-cec' + +kodi_run_method: + description: 'Run a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call will be redirected in a Home Assistant event: `kodi_run_method_result`.' + + fields: + entity_id: + description: Name(s) of the Kodi entities where to run the API method. + example: 'media_player.living_room_kodi' + method: + description: Name of the Kodi JSONRPC API method to be called. + example: 'VideoLibrary.GetRecentlyAddedEpisodes' From 7b510a4c5a0c0d4fe64b7c1eb5b26cd8eff18366 Mon Sep 17 00:00:00 2001 From: azogue Date: Mon, 15 May 2017 17:25:01 +0200 Subject: [PATCH 2/5] no need to clean OrderedDicts; no need for the `kodi_execute_addon` service --- homeassistant/components/media_player/kodi.py | 53 ++----------------- 1 file changed, 4 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 1863fb6075cb8f..7894b5748fe962 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -36,7 +36,6 @@ _LOGGER = logging.getLogger(__name__) EVENT_KODI_RUN_METHOD_RESULT = 'kodi_run_method_result' -EVENT_KODI_EXEC_ADDON_RESULT = 'kodi_execute_addon_result' CONF_TCP_PORT = 'tcp_port' CONF_TURN_OFF_ACTION = 'turn_off_action' @@ -87,7 +86,6 @@ SERVICE_ADD_MEDIA = 'kodi_add_to_playlist' SERVICE_RUN_METHOD = 'kodi_run_method' -SERVICE_EXEC_ADDON = 'kodi_execute_addon' DATA_KODI = 'kodi' @@ -96,7 +94,6 @@ ATTR_MEDIA_ARTIST_NAME = 'artist_name' ATTR_MEDIA_ID = 'media_id' ATTR_METHOD = 'method' -ATTR_ADDONID = 'addonid' MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ vol.Required(ATTR_MEDIA_TYPE): cv.string, @@ -107,9 +104,6 @@ MEDIA_PLAYER_RUN_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ vol.Required(ATTR_METHOD): cv.string, }, extra=vol.ALLOW_EXTRA) -MEDIA_PLAYER_EXEC_ADDON_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ - vol.Required(ATTR_ADDONID): cv.string, -}, extra=vol.ALLOW_EXTRA) SERVICE_TO_METHOD = { SERVICE_ADD_MEDIA: { @@ -118,9 +112,6 @@ SERVICE_RUN_METHOD: { 'method': 'async_run_method', 'schema': MEDIA_PLAYER_RUN_METHOD_SCHEMA}, - SERVICE_EXEC_ADDON: { - 'method': 'async_exec_addon', - 'schema': MEDIA_PLAYER_EXEC_ADDON_SCHEMA}, } @@ -216,14 +207,6 @@ def wrapper(obj, *args, **kwargs): return wrapper -def _ordereddict_to_dict(params): - """Recursive method to clean kwargs before calling the Kodi API.""" - for key, value in params.items(): - if isinstance(value, OrderedDict): - params[key] = _ordereddict_to_dict(value) - return dict(params) - - class KodiDevice(MediaPlayerDevice): """Representation of a XBMC/Kodi device.""" @@ -713,56 +696,28 @@ def async_run_method(self, method, **kwargs): """Run Kodi JSONRPC API method with params.""" import jsonrpc_base _LOGGER.debug('Run API method "%s", kwargs=%s', method, kwargs) - params, result_ok = None, False + result_ok = False try: if kwargs: - params = _ordereddict_to_dict(kwargs) - result = yield from getattr(self.server, method)(params) + result = yield from getattr(self.server, method)(kwargs) else: result = yield from getattr(self.server, method)() result_ok = True except jsonrpc_base.jsonrpc.ProtocolError as exc: result = exc.args[2]['error'] _LOGGER.error('Run API method %s.%s(%s) error: %s', - self.entity_id, method, params, result) + self.entity_id, method, kwargs, result) if isinstance(result, dict): event_data = {'entity_id': self.entity_id, 'result': result, 'result_ok': result_ok, - 'input': {'method': method, 'params': params}} + 'input': {'method': method, 'params': kwargs}} _LOGGER.debug('EVENT kodi_run_method_result: %s', event_data) self.hass.bus.async_fire(EVENT_KODI_RUN_METHOD_RESULT, event_data=event_data) return result - @asyncio.coroutine - def async_exec_addon(self, addonid, **kwargs): - """Execute Kodi addon with optional params.""" - import jsonrpc_base - _LOGGER.debug('Kodi execute addon "%s", kwargs=%s', addonid, kwargs) - params = {"addonid": addonid} - result_ok = False - if kwargs: - params.update(_ordereddict_to_dict(kwargs)) - try: - result = yield from self.server.Addons.ExecuteAddon(params) - result_ok = True - except jsonrpc_base.jsonrpc.ProtocolError as exc: - result = exc.args[2]['error'] - _LOGGER.error('Execute addon %s.%s(%s) error: %s', - self.entity_id, addonid, params, result) - - if isinstance(result, dict): - event_data = {'entity_id': self.entity_id, - 'result': result, - 'result_ok': result_ok, - 'input': params} - _LOGGER.debug('EVENT kodi_execute_addon_result: %s', event_data) - self.hass.bus.async_fire(EVENT_KODI_EXEC_ADDON_RESULT, - event_data=event_data) - return result - @asyncio.coroutine def async_add_media_to_playlist( self, media_type, media_id=None, media_name='ALL', artist_name=''): From 28746736870a08d0f15619687dd46e270b58e2c0 Mon Sep 17 00:00:00 2001 From: azogue Date: Mon, 15 May 2017 17:25:38 +0200 Subject: [PATCH 3/5] no need for the `kodi_execute_addon` service --- homeassistant/components/media_player/services.yaml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 1415ef115399e2..c46caaa1a8cc2e 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -290,17 +290,6 @@ kodi_add_to_playlist: description: Optional artist name for filtering media. example: 'AC/DC' -kodi_execute_addon: - description: 'Execute a Kodi addon with optional parameters. Results of the Kodi API call, if any, will be redirected in a Home Assistant event: `kodi_execute_addon_result`.' - - fields: - entity_id: - description: Name(s) of the Kodi entities where to execute the (pre-installed) Kodi Addon. - example: 'media_player.living_room_kodi' - addonid: - description: Name of the Kodi addon. - example: 'script.json-cec' - kodi_run_method: description: 'Run a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call will be redirected in a Home Assistant event: `kodi_run_method_result`.' From a763b3d0e9ef4fa42aea7965ef59b64d2f2d5333 Mon Sep 17 00:00:00 2001 From: azogue Date: Mon, 15 May 2017 17:31:59 +0200 Subject: [PATCH 4/5] unused import --- homeassistant/components/media_player/kodi.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 7894b5748fe962..0bd88a2578c49b 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -5,7 +5,6 @@ https://home-assistant.io/components/media_player.kodi/ """ import asyncio -from collections import OrderedDict from functools import wraps import logging import urllib From 2d0e486aa346658e9dda8c641ee83b36a1640ddd Mon Sep 17 00:00:00 2001 From: azogue Date: Mon, 15 May 2017 18:36:49 +0200 Subject: [PATCH 5/5] naming changes --- homeassistant/components/media_player/kodi.py | 23 ++++++++----------- .../components/media_player/services.yaml | 4 ++-- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 0bd88a2578c49b..9861887df89de8 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -34,7 +34,7 @@ _LOGGER = logging.getLogger(__name__) -EVENT_KODI_RUN_METHOD_RESULT = 'kodi_run_method_result' +EVENT_KODI_CALL_METHOD_RESULT = 'kodi_call_method_result' CONF_TCP_PORT = 'tcp_port' CONF_TURN_OFF_ACTION = 'turn_off_action' @@ -84,7 +84,7 @@ }) SERVICE_ADD_MEDIA = 'kodi_add_to_playlist' -SERVICE_RUN_METHOD = 'kodi_run_method' +SERVICE_CALL_METHOD = 'kodi_call_method' DATA_KODI = 'kodi' @@ -100,7 +100,7 @@ vol.Optional(ATTR_MEDIA_NAME): cv.string, vol.Optional(ATTR_MEDIA_ARTIST_NAME): cv.string, }) -MEDIA_PLAYER_RUN_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ +MEDIA_PLAYER_CALL_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ vol.Required(ATTR_METHOD): cv.string, }, extra=vol.ALLOW_EXTRA) @@ -108,9 +108,9 @@ SERVICE_ADD_MEDIA: { 'method': 'async_add_media_to_playlist', 'schema': MEDIA_PLAYER_ADD_MEDIA_SCHEMA}, - SERVICE_RUN_METHOD: { - 'method': 'async_run_method', - 'schema': MEDIA_PLAYER_RUN_METHOD_SCHEMA}, + SERVICE_CALL_METHOD: { + 'method': 'async_call_method', + 'schema': MEDIA_PLAYER_CALL_METHOD_SCHEMA}, } @@ -691,16 +691,13 @@ def async_set_shuffle(self, shuffle): {"playerid": self._players[0]['playerid'], "shuffle": shuffle}) @asyncio.coroutine - def async_run_method(self, method, **kwargs): + def async_call_method(self, method, **kwargs): """Run Kodi JSONRPC API method with params.""" import jsonrpc_base _LOGGER.debug('Run API method "%s", kwargs=%s', method, kwargs) result_ok = False try: - if kwargs: - result = yield from getattr(self.server, method)(kwargs) - else: - result = yield from getattr(self.server, method)() + result = yield from getattr(self.server, method)(**kwargs) result_ok = True except jsonrpc_base.jsonrpc.ProtocolError as exc: result = exc.args[2]['error'] @@ -712,8 +709,8 @@ def async_run_method(self, method, **kwargs): 'result': result, 'result_ok': result_ok, 'input': {'method': method, 'params': kwargs}} - _LOGGER.debug('EVENT kodi_run_method_result: %s', event_data) - self.hass.bus.async_fire(EVENT_KODI_RUN_METHOD_RESULT, + _LOGGER.debug('EVENT kodi_call_method_result: %s', event_data) + self.hass.bus.async_fire(EVENT_KODI_CALL_METHOD_RESULT, event_data=event_data) return result diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index c46caaa1a8cc2e..00ce0987fd9060 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -290,8 +290,8 @@ kodi_add_to_playlist: description: Optional artist name for filtering media. example: 'AC/DC' -kodi_run_method: - description: 'Run a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call will be redirected in a Home Assistant event: `kodi_run_method_result`.' +kodi_call_method: + description: 'Call a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call will be redirected in a Home Assistant event: `kodi_call_method_result`.' fields: entity_id: