Fix Kodi specific services registry and add descriptions (#7551)

* Fix Kodi specific services, add descriptions, add more handled exceptions

 - Fixes issue #7528
 - Add descriptions for Kodi specific services in services.yaml.
 - Error handling in Kodi API errors.
 - Make compatible the existent specific service `media_player.kodi_set_shuffle` with the general `media_player.shuffle_set` service (both use the same method but with different named parameter, I think the Kodi specific service should be eliminated, since it is not)

* fix line too long

* removed new services (for another PR); removed `kodi_set_shuffle` service

* requested changes

 - Removed `kodi_set_shuffle` service.
 - Optional `media_name` and `artist_name` parameters. `media_name` defaults to 'ALL'.
 - Guard clause to check if the services are already registered.
This commit is contained in:
Eugenio Panadero 2017-05-13 05:48:57 +02:00 committed by Paulus Schoutsen
parent ad15844cf4
commit 4cdf0b4969
2 changed files with 89 additions and 34 deletions

View File

@ -9,16 +9,18 @@ from functools import wraps
import logging import logging
import urllib import urllib
import re import re
import os
import aiohttp import aiohttp
import voluptuous as vol import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, MediaPlayerDevice, SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, SUPPORT_SHUFFLE_SET,
PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN) MEDIA_TYPE_VIDEO, MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN)
from homeassistant.const import ( from homeassistant.const import (
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME,
CONF_PORT, CONF_SSL, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD,
@ -62,7 +64,8 @@ MEDIA_TYPES = {
SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_VOLUME_STEP SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_SHUFFLE_SET | \
SUPPORT_PLAY | SUPPORT_VOLUME_STEP
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
@ -78,17 +81,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
SERVICE_ADD_MEDIA = 'kodi_add_to_playlist' SERVICE_ADD_MEDIA = 'kodi_add_to_playlist'
SERVICE_SET_SHUFFLE = 'kodi_set_shuffle'
DATA_KODI = 'kodi'
ATTR_MEDIA_TYPE = 'media_type' ATTR_MEDIA_TYPE = 'media_type'
ATTR_MEDIA_NAME = 'media_name' ATTR_MEDIA_NAME = 'media_name'
ATTR_MEDIA_ARTIST_NAME = 'artist_name' ATTR_MEDIA_ARTIST_NAME = 'artist_name'
ATTR_MEDIA_ID = 'media_id' ATTR_MEDIA_ID = 'media_id'
MEDIA_PLAYER_SET_SHUFFLE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required('shuffle_on'): cv.boolean,
})
MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_MEDIA_TYPE): cv.string, vol.Required(ATTR_MEDIA_TYPE): cv.string,
vol.Optional(ATTR_MEDIA_ID): cv.string, vol.Optional(ATTR_MEDIA_ID): cv.string,
@ -100,15 +100,14 @@ SERVICE_TO_METHOD = {
SERVICE_ADD_MEDIA: { SERVICE_ADD_MEDIA: {
'method': 'async_add_media_to_playlist', 'method': 'async_add_media_to_playlist',
'schema': MEDIA_PLAYER_ADD_MEDIA_SCHEMA}, 'schema': MEDIA_PLAYER_ADD_MEDIA_SCHEMA},
SERVICE_SET_SHUFFLE: {
'method': 'async_set_shuffle',
'schema': MEDIA_PLAYER_SET_SHUFFLE_SCHEMA},
} }
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Kodi platform.""" """Set up the Kodi platform."""
if DATA_KODI not in hass.data:
hass.data[DATA_KODI] = []
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
port = config.get(CONF_PORT) port = config.get(CONF_PORT)
tcp_port = config.get(CONF_TCP_PORT) tcp_port = config.get(CONF_TCP_PORT)
@ -130,6 +129,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
password=config.get(CONF_PASSWORD), 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), websocket=websocket)
hass.data[DATA_KODI].append(entity)
async_add_devices([entity], update_before_add=True) async_add_devices([entity], update_before_add=True)
@asyncio.coroutine @asyncio.coroutine
@ -141,23 +141,37 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
params = {key: value for key, value in service.data.items() params = {key: value for key, value in service.data.items()
if key != 'entity_id'} if key != 'entity_id'}
entity_ids = service.data.get('entity_id')
yield from getattr(entity, method['method'])(**params) if entity_ids:
target_players = [player for player in hass.data[DATA_KODI]
if player.entity_id in entity_ids]
else:
target_players = hass.data[DATA_KODI]
update_tasks = [] update_tasks = []
if entity.should_poll: for player in target_players:
update_coro = entity.async_update_ha_state(True) yield from getattr(player, method['method'])(**params)
for player in target_players:
if player.should_poll:
update_coro = player.async_update_ha_state(True)
update_tasks.append(update_coro) update_tasks.append(update_coro)
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) yield from asyncio.wait(update_tasks, loop=hass.loop)
if hass.services.has_service(DOMAIN, SERVICE_ADD_MEDIA):
return
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
for service in SERVICE_TO_METHOD: for service in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[service].get( schema = SERVICE_TO_METHOD[service]['schema']
'schema', MEDIA_PLAYER_SCHEMA)
hass.services.async_register( hass.services.async_register(
DOMAIN, service, async_service_handler, DOMAIN, service, async_service_handler,
description=None, schema=schema) description=descriptions.get(service), schema=schema)
def cmd(func): def cmd(func):
@ -657,16 +671,16 @@ class KodiDevice(MediaPlayerDevice):
{"item": {"file": str(media_id)}}) {"item": {"file": str(media_id)}})
@asyncio.coroutine @asyncio.coroutine
def async_set_shuffle(self, shuffle_on): def async_set_shuffle(self, shuffle):
"""Set shuffle mode, for the first player.""" """Set shuffle mode, for the first player."""
if len(self._players) < 1: if len(self._players) < 1:
raise RuntimeError("Error: No active player.") raise RuntimeError("Error: No active player.")
yield from self.server.Player.SetShuffle( yield from self.server.Player.SetShuffle(
{"playerid": self._players[0]['playerid'], "shuffle": shuffle_on}) {"playerid": self._players[0]['playerid'], "shuffle": shuffle})
@asyncio.coroutine @asyncio.coroutine
def async_add_media_to_playlist( def async_add_media_to_playlist(
self, media_type, media_id=None, media_name='', artist_name=''): self, media_type, media_id=None, media_name='ALL', artist_name=''):
"""Add a media to default playlist (i.e. playlistid=0). """Add a media to default playlist (i.e. playlistid=0).
First the media type must be selected, then First the media type must be selected, then
@ -675,13 +689,14 @@ class KodiDevice(MediaPlayerDevice):
All the albums of an artist can be added with All the albums of an artist can be added with
media_name="ALL" media_name="ALL"
""" """
import jsonrpc_base
params = {"playlistid": 0}
if media_type == "SONG": if media_type == "SONG":
if media_id is None: if media_id is None:
media_id = yield from self.async_find_song( media_id = yield from self.async_find_song(
media_name, artist_name) media_name, artist_name)
if media_id:
yield from self.server.Playlist.Add( params["item"] = {"songid": int(media_id)}
{"playlistid": 0, "item": {"songid": int(media_id)}})
elif media_type == "ALBUM": elif media_type == "ALBUM":
if media_id is None: if media_id is None:
@ -691,12 +706,22 @@ class KodiDevice(MediaPlayerDevice):
media_id = yield from self.async_find_album( media_id = yield from self.async_find_album(
media_name, artist_name) media_name, artist_name)
if media_id:
params["item"] = {"albumid": int(media_id)}
yield from self.server.Playlist.Add(
{"playlistid": 0, "item": {"albumid": int(media_id)}})
else: else:
raise RuntimeError("Unrecognized media type.") raise RuntimeError("Unrecognized media type.")
if media_id is not None:
try:
yield from self.server.Playlist.Add(params)
except jsonrpc_base.jsonrpc.ProtocolError as exc:
result = exc.args[2]['error']
_LOGGER.error('Run API method %s.Playlist.Add(%s) error: %s',
self.entity_id, media_type, result)
else:
_LOGGER.warning('No media detected for Playlist.Add')
@asyncio.coroutine @asyncio.coroutine
def async_add_all_albums(self, artist_name): def async_add_all_albums(self, artist_name):
"""Add all albums of an artist to default playlist (i.e. playlistid=0). """Add all albums of an artist to default playlist (i.e. playlistid=0).
@ -734,9 +759,13 @@ class KodiDevice(MediaPlayerDevice):
def async_find_artist(self, artist_name): def async_find_artist(self, artist_name):
"""Find artist by name.""" """Find artist by name."""
artists = yield from self.async_get_artists() artists = yield from self.async_get_artists()
try:
out = self._find( out = self._find(
artist_name, [a['artist'] for a in artists['artists']]) artist_name, [a['artist'] for a in artists['artists']])
return artists['artists'][out[0][0]]['artistid'] return artists['artists'][out[0][0]]['artistid']
except KeyError:
_LOGGER.warning('No artists were found: %s', artist_name)
return None
@asyncio.coroutine @asyncio.coroutine
def async_get_songs(self, artist_id=None): def async_get_songs(self, artist_id=None):
@ -769,8 +798,14 @@ class KodiDevice(MediaPlayerDevice):
artist_id = yield from self.async_find_artist(artist_name) artist_id = yield from self.async_find_artist(artist_name)
albums = yield from self.async_get_albums(artist_id) albums = yield from self.async_get_albums(artist_id)
out = self._find(album_name, [a['label'] for a in albums['albums']]) try:
out = self._find(
album_name, [a['label'] for a in albums['albums']])
return albums['albums'][out[0][0]]['albumid'] return albums['albums'][out[0][0]]['albumid']
except KeyError:
_LOGGER.warning('No albums were found with artist: %s, album: %s',
artist_name, album_name)
return None
@staticmethod @staticmethod
def _find(key_word, words): def _find(key_word, words):

View File

@ -269,3 +269,23 @@ soundtouch_remove_zone_slave:
slaves: slaves:
description: Name of slaves entities to remove from the existing zone description: Name of slaves entities to remove from the existing zone
example: 'media_player.soundtouch_bedroom' example: 'media_player.soundtouch_bedroom'
kodi_add_to_playlist:
description: Add music to the default playlist (i.e. playlistid=0).
fields:
entity_id:
description: Name(s) of the Kodi entities where to add the media.
example: 'media_player.living_room_kodi'
media_type:
description: Media type identifier. It must be one of SONG or ALBUM.
example: ALBUM
media_id:
description: Unique Id of the media entry to add (`songid` or albumid`). If not defined, `media_name` and `artist_name` are needed to search the Kodi music library.
example: 123456
media_name:
description: Optional media name for filtering media. Can be 'ALL' when `media_type` is 'ALBUM' and `artist_name` is specified, to add all songs from one artist.
example: 'Highway to Hell'
artist_name:
description: Optional artist name for filtering media.
example: 'AC/DC'