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
10 changes: 10 additions & 0 deletions homeassistant/components/media_player/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,13 @@ kodi_call_method:
method:
description: Name of the Kodi JSONRPC API method to be called.
example: 'VideoLibrary.GetRecentlyAddedEpisodes'

squeezebox_call_method:
description: 'Call a Squeezebox JSON/RPC API method.'
fields:
entity_id:
description: Name(s) of the Squeexebox entities where to run the API method.
example: 'media_player.squeezebox_radio'
method:
description: Squeezebox JSON/RPC method call, in the form of an array of positional parameters. See 'Command Line Interface' official help page from Logitech for details.

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.

Separate the method arguments out into their own optional field on the schema.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree that would be nicer (easier to use), but the API supports hundreds of commands, requiring a different number of parameters.
The parameter p2 of one query can have a completely different meaning as the paramater p2 of another one.
So we can't really name the parameters. We could create param_0, param_1 but... up to how many? Most commands have up to 4 but the count can go up quickly, especially for search commands which accept various criteria.

Extract of the documentation:

The format of the commands, queries and server replies is as follows:
[<playerid>] <p0> <p1> ... <pN> <LF>
where:
<playerid> is the unique identifier for the player, usually (but not guaranteed to be)
the MAC address of the player. Some commands are global to the Squeezebox Server
and do not require a <playerid>. For commands requiring it, a random player will be
selected by the Squeezebox Server if the <playerid> is omitted, and returned in the
server reply. <playerid> may be obtained by using the "player id" or "players" queries.

<p0> through <pN> are positional parameters. Pass a "?" to obtain a value for that
parameter in the Squeezebox Server response (i.e. send a query). Details of the
parameters vary for each command as described below.

Each parameter needs to be encoded using percent-style escaping, the same method
as is used in URLs; for example, "The Clash?" would be encoded as "The%20Clash%3F".
This also applies to <playerid>. In the examples below, the escaping is not show for
readability (except %20 for space).

Some examples:

info total artists ?
Returns the number of artists in the DB. Has to be passed as ['info', 'total', 'artists', '?']

playlist loadalbum <genre> <artist> <album>
Puts songs matching the specified genre artist and album criteria on the playlist. Songs previously in the playlist are discarded.
For example: ['playlist', 'loadalbum', 'Rock', 'Abba', '*']

genres <start> <itemsPerResponse> <taggedParameters>
This is an example of command accepting tagged parameters. An example would be
['genres', '0', '10', 'search:rock', 'artist_id:5', 'album_id:6', 'year:2003']
This would restrict the search to the genres proposed by artist with ID 5, available on the album with ID 6 and with tracks in year 2003.

As you can see in just 3 examples, parameter p1 can be static strings 'total', 'loadalbum', or a number (index of first result).
I think it is easier for somebody used to the Squeezebox API to just give the array of parameters to the service as is.
Let me know if you have another idea!

If you don't have a squeezebox server running, you can find a copy of the documentation here:
https://github.com/elParaguayo/LMS-CLI-Documentation/blob/master/LMS-CLI.md

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, sorry, that was a confusing comment. I just meant separate this out into two fields. method, which can just be a required string, and arguments, which is an optional array of any arguments.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK I get it now.
I split the unique method field into two:

  • command which is a mandatory string and will be the first parameter passed to the API. I use the word command to be consistent with the API documentation. I did not use the word 'method' because it would be conflicting with the main API method which is always 'slim.request'
  • parameters which is an optional array of extra parameters added to the command.

example: '["playlist", "loadtracks", "track.titlesearch=you''re welcome"]'

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 avoid the more complicated quote escaping example here, and pick an easier title. Knowing how to escape quotes in yaml isn't necessary to understand this service. 😄

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair enough, I changed the title with last commit!

87 changes: 85 additions & 2 deletions homeassistant/components/media_player/squeezebox.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@
import asyncio
import urllib.parse
import json
import os
import aiohttp
import async_timeout

import voluptuous as vol

from homeassistant.config import load_yaml_config_file
from homeassistant.components.media_player import (
ATTR_MEDIA_ENQUEUE, SUPPORT_PLAY_MEDIA,
MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, PLATFORM_SCHEMA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_PLAY, MediaPlayerDevice)
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_PLAY, MediaPlayerDevice,
MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_SHUFFLE_SET, SUPPORT_CLEAR_PLAYLIST)
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, STATE_IDLE, STATE_OFF,
STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, CONF_PORT)
Expand All @@ -33,7 +36,7 @@
SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_SEEK | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | \
SUPPORT_PLAY
SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | SUPPORT_CLEAR_PLAYLIST

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
Expand All @@ -42,12 +45,32 @@
vol.Optional(CONF_USERNAME): cv.string,
})

SERVICE_CALL_METHOD = 'squeezebox_call_method'

DATA_SQUEEZEBOX = 'squeexebox'

ATTR_METHOD = 'method'

SQUEEZEBOX_CALL_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_METHOD):
vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]),
})

SERVICE_TO_METHOD = {
SERVICE_CALL_METHOD: {
'method': 'async_call_method',
'schema': SQUEEZEBOX_CALL_METHOD_SCHEMA},
}


@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the squeezebox platform."""
import socket

if DATA_SQUEEZEBOX not in hass.data:
hass.data[DATA_SQUEEZEBOX] = []

username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)

Expand All @@ -74,8 +97,44 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
lms = LogitechMediaServer(hass, host, port, username, password)

players = yield from lms.create_players()

hass.data[DATA_SQUEEZEBOX].extend(players)
async_add_devices(players)

@asyncio.coroutine
def async_service_handler(service):
"""Map services to methods on MediaPlayerDevice."""
method = SERVICE_TO_METHOD.get(service.service)
if not method:
return

params = {key: value for key, value in service.data.items()
if key != 'entity_id'}
entity_ids = service.data.get('entity_id')
if entity_ids:
target_players = [player for player in hass.data[DATA_SQUEEZEBOX]
if player.entity_id in entity_ids]
else:
target_players = hass.data[DATA_SQUEEZEBOX]

update_tasks = []
for player in target_players:
yield from getattr(player, method['method'])(**params)
update_tasks.append(player.async_update_ha_state(True))

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

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

for service in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[service]['schema']
hass.services.async_register(
DOMAIN, service, async_service_handler,
description=descriptions.get(service), schema=schema)

return True


Expand Down Expand Up @@ -305,6 +364,12 @@ def media_album_name(self):
if 'album' in self._status:
return self._status['album']

@property
def shuffle(self):
"""Boolean if shuffle is enabled."""
if 'playlist_shuffle' in self._status:
return self._status['playlist_shuffle'] == 1

@property
def supported_features(self):
"""Flag media player features that are supported."""
Expand Down Expand Up @@ -415,3 +480,21 @@ def _play_uri(self, media_id):
def _add_uri_to_playlist(self, media_id):
"""Add a items to the existing playlist."""
return self.async_query('playlist', 'add', media_id)

def async_set_shuffle(self, shuffle):
"""Enable/disable shuffle mode."""
return self.async_query('playlist', 'shuffle', int(shuffle))

def async_clear_playlist(self):
"""Send the media player the command for clear playlist."""
return self.async_query('playlist', 'clear')

def async_call_method(self, method):
"""
Call Squeezebox JSON/RPC method.

Parameter 'method' should be the list of positional parameters (p0, p1,
pN) to be passed to JSON/RPC server.
"""
return self.async_query(
*[urllib.parse.quote(element, safe=':=') for element in method])