Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 70 additions & 29 deletions homeassistant/components/media_player/kodi.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@
import logging
import urllib
import re
import os

import aiohttp
import voluptuous as vol

from homeassistant.config import load_yaml_config_file
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, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO,
MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN)
SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, SUPPORT_SHUFFLE_SET,
MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
MEDIA_TYPE_VIDEO, MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN)
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,
Expand Down Expand Up @@ -61,8 +63,9 @@
}

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
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_SHUFFLE_SET | \
SUPPORT_PLAY | SUPPORT_VOLUME_STEP

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
Expand All @@ -78,22 +81,25 @@
})

SERVICE_ADD_MEDIA = 'kodi_add_to_playlist'
SERVICE_SET_SHUFFLE = 'kodi_set_shuffle'
SERVICE_SET_SHUFFLE = 'kodi_set_shuffle' # this is the same as mp.shuffle_set

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just eliminate this. It only existed for a short while before core shuffle support was added.


DATA_KODI = 'kodi'

ATTR_MEDIA_TYPE = 'media_type'
ATTR_MEDIA_NAME = 'media_name'
ATTR_MEDIA_ARTIST_NAME = 'artist_name'
ATTR_MEDIA_ID = 'media_id'

MEDIA_PLAYER_SET_SHUFFLE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required('shuffle_on'): cv.boolean,
vol.Exclusive('shuffle_on', 'shuffle'): cv.boolean,
vol.Exclusive('shuffle', 'shuffle'): cv.boolean,
})

MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_MEDIA_TYPE): cv.string,
vol.Optional(ATTR_MEDIA_ID): cv.string,
vol.Optional(ATTR_MEDIA_NAME): cv.string,
vol.Optional(ATTR_MEDIA_ARTIST_NAME): cv.string,
vol.Inclusive(ATTR_MEDIA_NAME, 'media_search'): cv.string,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep these optional, and default it to ALL in the service if looking for artists? Each media type should be able to make the best decision what to do if one isn't defined.

vol.Inclusive(ATTR_MEDIA_ARTIST_NAME, 'media_search'): cv.string,
})

SERVICE_TO_METHOD = {
Expand All @@ -109,6 +115,8 @@
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Kodi platform."""
if DATA_KODI not in hass.data:
hass.data[DATA_KODI] = []

@emlove emlove May 11, 2017

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's instead make this a dict with the entity_id as the key, so we don't have to search it later.

host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
tcp_port = config.get(CONF_TCP_PORT)
Expand All @@ -130,7 +138,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
password=config.get(CONF_PASSWORD),
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somehow this was missed before, but this is a coroutine. If we make this yield from async_add_devices(..., the entity_id should be available afterwards. Use entity.entity_id as the dict key.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is not possible, since v0.40, the passed in async_add_devices method is a callback instead of a coroutine function. (https://home-assistant.io/blog/2017/03/11/repurpose-any-android-phone-as-ip-camera/)

I also did not like having to use slugify in that place, maybe it would be better to leave hass.data[DATA_KODI] as a list?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I missed that. In that case then yeah, going back to the list seems to make the most sense.

descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to add a guard clause here to check if the services are already registered. Something like

if hass.services.has_service(DOMAIN, SERVICE_ADD_MEDIA):
    return

@asyncio.coroutine
def async_service_handler(service):
Expand All @@ -141,23 +153,30 @@ def async_service_handler(service):

params = {key: value for key, value in service.data.items()
if key != 'entity_id'}

yield from getattr(entity, method['method'])(**params)
entity_ids = service.data.get('entity_id')
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 = []
if entity.should_poll:
update_coro = entity.async_update_ha_state(True)
update_tasks.append(update_coro)
for player in target_players:
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)

if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)

for service in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[service].get(
'schema', MEDIA_PLAYER_SCHEMA)
schema = SERVICE_TO_METHOD[service].get('schema')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could even make this schema = SERVICE_TO_METHOD[service]['schema']. We want to ensure the schema is defined for future changes.

hass.services.async_register(
DOMAIN, service, async_service_handler,
description=None, schema=schema)
description=descriptions.get(service), schema=schema)


def cmd(func):
Expand Down Expand Up @@ -657,12 +676,13 @@ def async_play_media(self, media_type, media_id, **kwargs):
{"item": {"file": str(media_id)}})

@asyncio.coroutine
def async_set_shuffle(self, shuffle_on):
def async_set_shuffle(self, shuffle_on=None, shuffle=None):
"""Set shuffle mode, for the first player."""
shuffle = shuffle if shuffle is not None else shuffle_on
if len(self._players) < 1:
raise RuntimeError("Error: No active player.")
yield from self.server.Player.SetShuffle(
{"playerid": self._players[0]['playerid'], "shuffle": shuffle_on})
{"playerid": self._players[0]['playerid'], "shuffle": shuffle})

@asyncio.coroutine
def async_add_media_to_playlist(
Expand All @@ -675,13 +695,14 @@ def async_add_media_to_playlist(
All the albums of an artist can be added with
media_name="ALL"
"""
import jsonrpc_base
params = {"playlistid": 0}
if media_type == "SONG":
if media_id is None:
media_id = yield from self.async_find_song(
media_name, artist_name)

yield from self.server.Playlist.Add(
{"playlistid": 0, "item": {"songid": int(media_id)}})
if media_id:
params["item"] = {"songid": int(media_id)}

elif media_type == "ALBUM":
if media_id is None:
Expand All @@ -691,12 +712,22 @@ def async_add_media_to_playlist(

media_id = yield from self.async_find_album(
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:
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 e:
result = e.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
def async_add_all_albums(self, artist_name):
"""Add all albums of an artist to default playlist (i.e. playlistid=0).
Expand Down Expand Up @@ -734,9 +765,13 @@ def async_get_albums(self, artist_id=None):
def async_find_artist(self, artist_name):
"""Find artist by name."""
artists = yield from self.async_get_artists()
out = self._find(
artist_name, [a['artist'] for a in artists['artists']])
return artists['artists'][out[0][0]]['artistid']
try:
out = self._find(
artist_name, [a['artist'] for a in artists['artists']])
return artists['artists'][out[0][0]]['artistid']
except KeyError:
_LOGGER.warning('No artists were found: %s', artist_name)
return None

@asyncio.coroutine
def async_get_songs(self, artist_id=None):
Expand Down Expand Up @@ -769,8 +804,14 @@ def async_find_album(self, album_name, artist_name=''):
artist_id = yield from self.async_find_artist(artist_name)

albums = yield from self.async_get_albums(artist_id)
out = self._find(album_name, [a['label'] for a in albums['albums']])
return albums['albums'][out[0][0]]['albumid']
try:
out = self._find(
album_name, [a['label'] for a in albums['albums']])
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
def _find(key_word, words):
Expand Down
54 changes: 54 additions & 0 deletions homeassistant/components/media_player/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,57 @@ soundtouch_remove_zone_slave:
slaves:
description: Name of slaves entities to remove from the existing zone
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'

kodi_set_shuffle: # == shuffle_set
description: Set shuffling state

fields:
entity_id:
description: Name(s) of entities to set
example: 'media_player.spotify'
# shuffle:
shuffle_on:
description: True/false for enabling/disabling shuffle
example: true

kodi_execute_addon:
description: 'Run a Kodi Addon with optional parameters. Results of the Kodi API call, if any, will be redirected in a Home Assistant event: `kodi_run_addon_result`.'

fields:
entity_id:
description: Name(s) of the Kodi entities where to run 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'