Squeezebox JSON-RPC (#5084)
* Refactor of Squeezebox connection code * Refactor of Squeezebox connection code * Typos * Make Python 3.4 friendly * Addressing comments * Improving docstring * Using discovered port * Style better * Accept new disco object * Revert "Accept new disco object" * Make it obvious that port isn't discovered yet * Flake8. ;)
This commit is contained in:
parent
469aad5fc8
commit
fd50201407
1 changed files with 120 additions and 150 deletions
|
@ -5,8 +5,11 @@ For more details about this platform, please refer to the documentation at
|
|||
https://home-assistant.io/components/media_player.squeezebox/
|
||||
"""
|
||||
import logging
|
||||
import telnetlib
|
||||
import asyncio
|
||||
import urllib.parse
|
||||
import json
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -19,10 +22,12 @@ from homeassistant.const import (
|
|||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, STATE_IDLE, STATE_OFF,
|
||||
STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_PORT = 9090
|
||||
DEFAULT_PORT = 9000
|
||||
TIMEOUT = 10
|
||||
|
||||
KNOWN_DEVICES = []
|
||||
|
||||
|
@ -38,7 +43,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup the squeezebox platform."""
|
||||
import socket
|
||||
|
||||
|
@ -47,11 +53,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
if discovery_info is not None:
|
||||
host = discovery_info[0]
|
||||
port = DEFAULT_PORT
|
||||
port = None # Port is not collected in netdisco 0.8.1
|
||||
else:
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
|
||||
# In case the port is not discovered
|
||||
if port is None:
|
||||
port = DEFAULT_PORT
|
||||
|
||||
# Get IP of host, to prevent duplication of same host (different DNS names)
|
||||
try:
|
||||
ipaddr = socket.gethostbyname(host)
|
||||
|
@ -68,13 +78,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
return False
|
||||
KNOWN_DEVICES.append(key)
|
||||
|
||||
_LOGGER.debug("Creating LMS object for %s", key)
|
||||
lms = LogitechMediaServer(host, port, username, password)
|
||||
|
||||
if not lms.init_success:
|
||||
_LOGGER.debug("Creating LMS object for %s", ipaddr)
|
||||
lms = LogitechMediaServer(hass, host, port, username, password)
|
||||
if lms is False:
|
||||
return False
|
||||
|
||||
add_devices(lms.create_players())
|
||||
players = yield from lms.create_players()
|
||||
yield from async_add_devices(players)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -82,107 +92,86 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class LogitechMediaServer(object):
|
||||
"""Representation of a Logitech media server."""
|
||||
|
||||
def __init__(self, host, port, username, password):
|
||||
def __init__(self, hass, host, port, username, password):
|
||||
"""Initialize the Logitech device."""
|
||||
self.hass = hass
|
||||
self.host = host
|
||||
self.port = port
|
||||
self._username = username
|
||||
self._password = password
|
||||
self.http_port = self._get_http_port()
|
||||
self.init_success = True if self.http_port else False
|
||||
|
||||
def _get_http_port(self):
|
||||
"""Get http port from media server, it is used to get cover art."""
|
||||
http_port = self.query('pref', 'httpport', '?')
|
||||
if not http_port:
|
||||
_LOGGER.error("Failed to connect to server %s:%s",
|
||||
self.host, self.port)
|
||||
return http_port
|
||||
|
||||
@asyncio.coroutine
|
||||
def create_players(self):
|
||||
"""Create a list of SqueezeBoxDevices connected to the LMS."""
|
||||
players = []
|
||||
count = self.query('player', 'count', '?')
|
||||
for index in range(0, int(count)):
|
||||
player_id = self.query('player', 'id', str(index), '?')
|
||||
player = SqueezeBoxDevice(self, player_id)
|
||||
players.append(player)
|
||||
return players
|
||||
"""Create a list of devices connected to LMS."""
|
||||
result = []
|
||||
data = yield from self.async_query('players', 'status')
|
||||
|
||||
def query(self, *parameters):
|
||||
"""Send request and await response from server."""
|
||||
response = self.get(' '.join(parameters))
|
||||
response = response.split(' ')[-1].strip()
|
||||
response = urllib.parse.unquote(response)
|
||||
for players in data['players_loop']:
|
||||
player = SqueezeBoxDevice(
|
||||
self, players['playerid'], players['name'])
|
||||
yield from player.async_update()
|
||||
result.append(player)
|
||||
return result
|
||||
|
||||
return response
|
||||
@asyncio.coroutine
|
||||
def async_query(self, *command, player=""):
|
||||
"""Abstract out the JSON-RPC connection."""
|
||||
response = None
|
||||
auth = None if self._username is None else aiohttp.BasicAuth(
|
||||
self._username, self._password)
|
||||
url = "http://{}:{}/jsonrpc.js".format(
|
||||
self.host, self.port)
|
||||
data = json.dumps({
|
||||
"id": "1",
|
||||
"method": "slim.request",
|
||||
"params": [player, command]
|
||||
})
|
||||
|
||||
def get_player_status(self, player):
|
||||
"""Get the status of a player."""
|
||||
# (title) : Song title
|
||||
# Requested Information
|
||||
# a (artist): Artist name 'artist'
|
||||
# d (duration): Song duration in seconds 'duration'
|
||||
# K (artwork_url): URL to remote artwork
|
||||
# l (album): Album, including the server's "(N of M)"
|
||||
tags = 'adKl'
|
||||
new_status = {}
|
||||
response = self.get('{player} status - 1 tags:{tags}\n'
|
||||
.format(player=player, tags=tags))
|
||||
_LOGGER.debug("URL: %s Data: %s", url, data)
|
||||
|
||||
if not response:
|
||||
return {}
|
||||
|
||||
response = response.split(' ')
|
||||
|
||||
for item in response:
|
||||
parts = urllib.parse.unquote(item).partition(':')
|
||||
new_status[parts[0]] = parts[2]
|
||||
return new_status
|
||||
|
||||
def get(self, command):
|
||||
"""Abstract out the telnet connection."""
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self.host, self.port)
|
||||
websession = async_get_clientsession(self.hass)
|
||||
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
||||
response = yield from websession.post(
|
||||
url,
|
||||
data=data,
|
||||
auth=auth)
|
||||
|
||||
if self._username and self._password:
|
||||
_LOGGER.debug("Logging in")
|
||||
if response.status == 200:
|
||||
data = yield from response.json()
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Query failed, response code: %s Full message: %s",
|
||||
response.status, response)
|
||||
return False
|
||||
|
||||
telnet.write('login {username} {password}\n'.format(
|
||||
username=self._username,
|
||||
password=self._password).encode('UTF-8'))
|
||||
telnet.read_until(b'\n', timeout=3)
|
||||
except (asyncio.TimeoutError,
|
||||
aiohttp.errors.ClientError,
|
||||
aiohttp.errors.ClientDisconnectedError) as error:
|
||||
_LOGGER.error("Failed communicating with LMS: %s", type(error))
|
||||
return False
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
_LOGGER.debug("About to send message: %s", command)
|
||||
message = '{}\n'.format(command)
|
||||
telnet.write(message.encode('UTF-8'))
|
||||
|
||||
response = telnet.read_until(b'\n', timeout=3)\
|
||||
.decode('UTF-8')\
|
||||
|
||||
telnet.write(b'exit\n')
|
||||
_LOGGER.debug("Response: %s", response)
|
||||
|
||||
return response
|
||||
|
||||
except (OSError, EOFError) as error:
|
||||
_LOGGER.error("Could not communicate with %s:%d: %s",
|
||||
self.host,
|
||||
self.port,
|
||||
error)
|
||||
return None
|
||||
try:
|
||||
return data['result']
|
||||
except AttributeError:
|
||||
_LOGGER.error("Received invalid response: %s", data)
|
||||
return False
|
||||
|
||||
|
||||
class SqueezeBoxDevice(MediaPlayerDevice):
|
||||
"""Representation of a SqueezeBox device."""
|
||||
|
||||
def __init__(self, lms, player_id):
|
||||
def __init__(self, lms, player_id, name):
|
||||
"""Initialize the SqueezeBox device."""
|
||||
super(SqueezeBoxDevice, self).__init__()
|
||||
self._lms = lms
|
||||
self._id = player_id
|
||||
self._name = self._lms.query(self._id, 'name', '?')
|
||||
self._status = self._lms.get_player_status(self._id)
|
||||
self._status = {}
|
||||
self._name = name
|
||||
_LOGGER.debug("Creating SqueezeBox object: %s, %s", name, player_id)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -203,9 +192,31 @@ class SqueezeBoxDevice(MediaPlayerDevice):
|
|||
return STATE_IDLE
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._status = self._lms.get_player_status(self._id)
|
||||
def async_query(self, *parameters):
|
||||
"""Send a command to the LMS.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self._lms.async_query(
|
||||
*parameters, player=self._id)
|
||||
|
||||
def query(self, *parameters):
|
||||
"""Queue up a command to send the LMS."""
|
||||
self.hass.loop.create_task(self.async_query(*parameters))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Retrieve the current state of the player."""
|
||||
tags = 'adKl'
|
||||
response = yield from self.async_query(
|
||||
"status", "-", "1", "tags:{tags}"
|
||||
.format(tags=tags))
|
||||
|
||||
try:
|
||||
self._status = response.copy()
|
||||
self._status.update(response["remoteMeta"])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
|
@ -217,7 +228,7 @@ class SqueezeBoxDevice(MediaPlayerDevice):
|
|||
def is_volume_muted(self):
|
||||
"""Return true if volume is muted."""
|
||||
if 'mixer volume' in self._status:
|
||||
return self._status['mixer volume'].startswith('-')
|
||||
return str(self._status['mixer volume']).startswith('-')
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
|
@ -254,15 +265,14 @@ class SqueezeBoxDevice(MediaPlayerDevice):
|
|||
username=self._lms._username,
|
||||
password=self._lms._password,
|
||||
server=self._lms.host,
|
||||
port=self._lms.http_port)
|
||||
port=self._lms.port)
|
||||
else:
|
||||
base_url = 'http://{server}:{port}/'.format(
|
||||
server=self._lms.host,
|
||||
port=self._lms.http_port)
|
||||
port=self._lms.port)
|
||||
|
||||
url = urllib.parse.urljoin(base_url, media_url)
|
||||
|
||||
_LOGGER.debug("Media image url: %s", url)
|
||||
return url
|
||||
|
||||
@property
|
||||
|
@ -284,7 +294,7 @@ class SqueezeBoxDevice(MediaPlayerDevice):
|
|||
def media_album_name(self):
|
||||
"""Album of current playing media."""
|
||||
if 'album' in self._status:
|
||||
return self._status['album'].rstrip()
|
||||
return self._status['album']
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
|
@ -293,64 +303,64 @@ class SqueezeBoxDevice(MediaPlayerDevice):
|
|||
|
||||
def turn_off(self):
|
||||
"""Turn off media player."""
|
||||
self._lms.query(self._id, 'power', '0')
|
||||
self.query('power', '0')
|
||||
self.update_ha_state()
|
||||
|
||||
def volume_up(self):
|
||||
"""Volume up media player."""
|
||||
self._lms.query(self._id, 'mixer', 'volume', '+5')
|
||||
self.query('mixer', 'volume', '+5')
|
||||
self.update_ha_state()
|
||||
|
||||
def volume_down(self):
|
||||
"""Volume down media player."""
|
||||
self._lms.query(self._id, 'mixer', 'volume', '-5')
|
||||
self.query('mixer', 'volume', '-5')
|
||||
self.update_ha_state()
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set volume level, range 0..1."""
|
||||
volume_percent = str(int(volume*100))
|
||||
self._lms.query(self._id, 'mixer', 'volume', volume_percent)
|
||||
self.query('mixer', 'volume', volume_percent)
|
||||
self.update_ha_state()
|
||||
|
||||
def mute_volume(self, mute):
|
||||
"""Mute (true) or unmute (false) media player."""
|
||||
mute_numeric = '1' if mute else '0'
|
||||
self._lms.query(self._id, 'mixer', 'muting', mute_numeric)
|
||||
self.query('mixer', 'muting', mute_numeric)
|
||||
self.update_ha_state()
|
||||
|
||||
def media_play_pause(self):
|
||||
"""Send pause command to media player."""
|
||||
self._lms.query(self._id, 'pause')
|
||||
self.query('pause')
|
||||
self.update_ha_state()
|
||||
|
||||
def media_play(self):
|
||||
"""Send play command to media player."""
|
||||
self._lms.query(self._id, 'play')
|
||||
self.query('play')
|
||||
self.update_ha_state()
|
||||
|
||||
def media_pause(self):
|
||||
"""Send pause command to media player."""
|
||||
self._lms.query(self._id, 'pause', '1')
|
||||
self.query('pause', '1')
|
||||
self.update_ha_state()
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send next track command."""
|
||||
self._lms.query(self._id, 'playlist', 'index', '+1')
|
||||
self.query('playlist', 'index', '+1')
|
||||
self.update_ha_state()
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send next track command."""
|
||||
self._lms.query(self._id, 'playlist', 'index', '-1')
|
||||
self.query('playlist', 'index', '-1')
|
||||
self.update_ha_state()
|
||||
|
||||
def media_seek(self, position):
|
||||
"""Send seek command."""
|
||||
self._lms.query(self._id, 'time', position)
|
||||
self.query('time', position)
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn the media player on."""
|
||||
self._lms.query(self._id, 'power', '1')
|
||||
self.query('power', '1')
|
||||
self.update_ha_state()
|
||||
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
|
@ -365,51 +375,11 @@ class SqueezeBoxDevice(MediaPlayerDevice):
|
|||
self._play_uri(media_id)
|
||||
|
||||
def _play_uri(self, media_id):
|
||||
"""
|
||||
Replace the current play list with the uri.
|
||||
|
||||
Telnet Command Structure:
|
||||
<playerid> playlist play <item> <title> <fadeInSecs>
|
||||
|
||||
The "playlist play" command puts the specified song URL,
|
||||
playlist or directory contents into the current playlist
|
||||
and plays starting at the first item. Any songs previously
|
||||
in the playlist are discarded. An optional title value may be
|
||||
passed to set a title. This can be useful for remote URLs.
|
||||
The "fadeInSecs" parameter may be passed to specify fade-in period.
|
||||
|
||||
Examples:
|
||||
Request: "04:20:00:12:23:45 playlist play
|
||||
/music/abba/01_Voulez_Vous.mp3<LF>"
|
||||
Response: "04:20:00:12:23:45 playlist play
|
||||
/music/abba/01_Voulez_Vous.mp3<LF>"
|
||||
|
||||
"""
|
||||
self._lms.query(self._id, 'playlist', 'play', media_id)
|
||||
"""Replace the current play list with the uri."""
|
||||
self.query('playlist', 'play', media_id)
|
||||
self.update_ha_state()
|
||||
|
||||
def _add_uri_to_playlist(self, media_id):
|
||||
"""
|
||||
Add a items to the existing playlist.
|
||||
|
||||
Telnet Command Structure:
|
||||
<playerid> playlist add <item>
|
||||
|
||||
The "playlist add" command adds the specified song URL, playlist or
|
||||
directory contents to the end of the current playlist. Songs
|
||||
currently playing or already on the playlist are not affected.
|
||||
|
||||
Examples:
|
||||
Request: "04:20:00:12:23:45 playlist add
|
||||
/music/abba/01_Voulez_Vous.mp3<LF>"
|
||||
Response: "04:20:00:12:23:45 playlist add
|
||||
/music/abba/01_Voulez_Vous.mp3<LF>"
|
||||
|
||||
Request: "04:20:00:12:23:45 playlist add
|
||||
/playlists/abba.m3u<LF>"
|
||||
Response: "04:20:00:12:23:45 playlist add
|
||||
/playlists/abba.m3u<LF>"
|
||||
|
||||
"""
|
||||
self._lms.query(self._id, 'playlist', 'add', media_id)
|
||||
"""Add a items to the existing playlist."""
|
||||
self.query('playlist', 'add', media_id)
|
||||
self.update_ha_state()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue