core/homeassistant/components/media_player/squeezebox.py

306 lines
9.9 KiB
Python
Raw Normal View History

2015-08-04 15:22:56 +00:00
"""
2016-03-08 09:34:33 +00:00
Support for interfacing to the Logitech SqueezeBox API.
2015-08-04 15:22:56 +00:00
2015-10-23 16:10:32 +00:00
For more details about this platform, please refer to the documentation at
2015-11-09 12:12:18 +00:00
https://home-assistant.io/components/media_player.squeezebox/
2015-08-04 15:22:56 +00:00
"""
import logging
import telnetlib
import urllib.parse
from homeassistant.components.media_player import (
2016-02-19 05:27:50 +00:00
DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
2015-08-04 15:22:56 +00:00
from homeassistant.const import (
2016-02-19 05:27:50 +00:00
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, STATE_IDLE, STATE_OFF,
STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN)
2015-08-04 15:22:56 +00:00
_LOGGER = logging.getLogger(__name__)
2016-01-13 05:53:27 +00:00
SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_SEEK | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
2015-08-04 15:22:56 +00:00
KNOWN_DEVICES = []
2015-08-04 15:22:56 +00:00
def setup_platform(hass, config, add_devices, discovery_info=None):
2016-03-08 09:34:33 +00:00
"""Setup the squeezebox platform."""
if discovery_info is not None:
host = discovery_info[0]
port = 9090
else:
host = config.get(CONF_HOST)
port = int(config.get('port', 9090))
if not host:
2015-08-05 11:49:45 +00:00
_LOGGER.error(
2015-08-05 18:02:39 +00:00
"Missing required configuration items in %s: %s",
2015-08-05 18:09:20 +00:00
DOMAIN,
CONF_HOST)
2015-08-05 11:49:45 +00:00
return False
2015-08-04 15:22:56 +00:00
# Only add a media server once
if host in KNOWN_DEVICES:
return False
KNOWN_DEVICES.append(host)
2015-08-05 11:49:45 +00:00
lms = LogitechMediaServer(
host, port,
2015-08-05 11:49:45 +00:00
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD))
2015-08-04 15:22:56 +00:00
2015-08-05 11:49:45 +00:00
if not lms.init_success:
return False
2015-08-04 15:22:56 +00:00
2015-08-05 11:49:45 +00:00
add_devices(lms.create_players())
2015-08-04 15:22:56 +00:00
2015-08-05 11:49:45 +00:00
return True
2015-08-04 15:22:56 +00:00
2015-08-05 11:49:45 +00:00
class LogitechMediaServer(object):
2016-03-08 09:34:33 +00:00
"""Representation of a Logitech media server."""
2015-08-05 18:09:20 +00:00
2015-08-05 11:49:45 +00:00
def __init__(self, host, port, username, password):
2016-03-08 09:34:33 +00:00
"""Initialize the Logitech device."""
2015-08-05 11:49:45 +00:00
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):
2016-03-08 09:34:33 +00:00
"""Get http port from media server, it is used to get cover art."""
2015-08-05 11:49:45 +00:00
http_port = None
try:
http_port = self.query('pref', 'httpport', '?')
if not http_port:
_LOGGER.error(
2015-08-05 18:02:39 +00:00
"Unable to read data from server %s:%s",
2015-08-05 18:09:20 +00:00
self.host,
self.port)
2015-08-05 11:49:45 +00:00
return
return http_port
except ConnectionError as ex:
_LOGGER.error(
2015-08-05 18:02:39 +00:00
"Failed to connect to server %s:%s - %s",
2015-08-05 18:09:20 +00:00
self.host,
self.port,
ex)
2015-08-05 11:49:45 +00:00
return
def create_players(self):
2016-03-08 09:34:33 +00:00
"""Create a list of SqueezeBoxDevices connected to the LMS."""
2015-08-05 11:49:45 +00:00
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
def query(self, *parameters):
2016-03-08 09:34:33 +00:00
"""Send request and await response from server."""
2015-08-05 11:49:45 +00:00
telnet = telnetlib.Telnet(self.host, self.port)
if self._username and self._password:
telnet.write('login {username} {password}\n'.format(
username=self._username,
2015-08-04 18:08:48 +00:00
password=self._password).encode('UTF-8'))
telnet.read_until(b'\n', timeout=3)
2015-08-04 15:22:56 +00:00
message = '{}\n'.format(' '.join(parameters))
telnet.write(message.encode('UTF-8'))
response = telnet.read_until(b'\n', timeout=3)\
.decode('UTF-8')\
.split(' ')[-1]\
.strip()
telnet.write(b'exit\n')
return urllib.parse.unquote(response)
2015-08-05 11:49:45 +00:00
def get_player_status(self, player):
2016-03-08 09:34:33 +00:00
"""Get ithe status of a player."""
2015-08-04 15:22:56 +00:00
# (title) : Song title
# Requested Information
# a (artist): Artist name 'artist'
# d (duration): Song duration in seconds 'duration'
# K (artwork_url): URL to remote artwork
tags = 'adK'
new_status = {}
2015-08-05 11:49:45 +00:00
telnet = telnetlib.Telnet(self.host, self.port)
2015-08-04 15:22:56 +00:00
telnet.write('{player} status - 1 tags:{tags}\n'.format(
2015-08-05 11:49:45 +00:00
player=player,
2015-08-04 15:22:56 +00:00
tags=tags
).encode('UTF-8'))
response = telnet.read_until(b'\n', timeout=3)\
.decode('UTF-8')\
.split(' ')
telnet.write(b'exit\n')
for item in response:
parts = urllib.parse.unquote(item).partition(':')
new_status[parts[0]] = parts[2]
2015-08-05 11:49:45 +00:00
return new_status
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-public-methods
class SqueezeBoxDevice(MediaPlayerDevice):
2016-03-08 09:34:33 +00:00
"""Representation of a SqueezeBox device."""
2015-08-05 11:49:45 +00:00
2016-02-02 08:31:36 +00:00
# pylint: disable=too-many-arguments, abstract-method
2015-08-05 11:49:45 +00:00
def __init__(self, lms, player_id):
2016-03-08 09:34:33 +00:00
"""Initialize the SqeezeBox device."""
2015-08-05 11:49:45 +00:00
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)
@property
def name(self):
2016-03-08 09:34:33 +00:00
"""Return the name of the device."""
2015-08-05 11:49:45 +00:00
return self._name
@property
def state(self):
2016-03-08 09:34:33 +00:00
"""Return the state of the device."""
2015-08-05 11:49:45 +00:00
if 'power' in self._status and self._status['power'] == '0':
return STATE_OFF
if 'mode' in self._status:
if self._status['mode'] == 'pause':
return STATE_PAUSED
if self._status['mode'] == 'play':
return STATE_PLAYING
if self._status['mode'] == 'stop':
return STATE_IDLE
return STATE_UNKNOWN
def update(self):
2016-03-08 09:34:33 +00:00
"""Retrieve latest state."""
self._status = self._lms.get_player_status(self._id)
2015-08-04 15:22:56 +00:00
@property
def volume_level(self):
2016-03-08 09:34:33 +00:00
"""Volume level of the media player (0..1)."""
2015-08-04 17:35:53 +00:00
if 'mixer volume' in self._status:
return int(float(self._status['mixer volume'])) / 100.0
2015-08-04 15:22:56 +00:00
@property
def is_volume_muted(self):
2016-03-08 09:34:33 +00:00
"""Return true if volume is muted."""
2015-08-04 17:35:53 +00:00
if 'mixer volume' in self._status:
2015-08-05 20:25:03 +00:00
return self._status['mixer volume'].startswith('-')
2015-08-04 15:22:56 +00:00
@property
def media_content_id(self):
2016-03-08 09:34:33 +00:00
"""Content ID of current playing media."""
2015-08-04 17:35:53 +00:00
if 'current_title' in self._status:
2015-08-04 15:22:56 +00:00
return self._status['current_title']
@property
def media_content_type(self):
2016-03-08 09:34:33 +00:00
"""Content type of current playing media."""
2015-08-04 15:22:56 +00:00
return MEDIA_TYPE_MUSIC
@property
def media_duration(self):
2016-03-08 09:34:33 +00:00
"""Duration of current playing media in seconds."""
2015-08-04 17:35:53 +00:00
if 'duration' in self._status:
2015-08-04 15:22:56 +00:00
return int(float(self._status['duration']))
@property
def media_image_url(self):
2016-03-08 09:34:33 +00:00
"""Image url of current playing media."""
2015-08-04 17:35:53 +00:00
if 'artwork_url' in self._status:
media_url = self._status['artwork_url']
2016-01-23 17:08:54 +00:00
elif 'id' in self._status:
media_url = ('/music/{track_id}/cover.jpg').format(
2016-01-23 17:14:03 +00:00
track_id=self._status['id'])
else:
media_url = ('/music/current/cover.jpg?player={player}').format(
2016-01-23 17:55:43 +00:00
player=self._id)
base_url = 'http://{server}:{port}/'.format(
server=self._lms.host,
port=self._lms.http_port)
return urllib.parse.urljoin(base_url, media_url)
2015-08-04 15:22:56 +00:00
@property
def media_title(self):
2016-03-08 09:34:33 +00:00
"""Title of current playing media."""
2015-08-04 17:35:53 +00:00
if 'artist' in self._status and 'title' in self._status:
2015-08-04 15:22:56 +00:00
return '{artist} - {title}'.format(
artist=self._status['artist'],
title=self._status['title']
)
2015-08-04 17:35:53 +00:00
if 'current_title' in self._status:
2015-08-04 15:22:56 +00:00
return self._status['current_title']
@property
def supported_media_commands(self):
2016-03-08 09:34:33 +00:00
"""Flag of media commands that are supported."""
2015-08-04 15:22:56 +00:00
return SUPPORT_SQUEEZEBOX
def turn_off(self):
2016-03-08 09:34:33 +00:00
"""Turn off media player."""
2015-08-05 11:49:45 +00:00
self._lms.query(self._id, 'power', '0')
2015-08-04 15:22:56 +00:00
self.update_ha_state()
def volume_up(self):
2016-03-08 09:34:33 +00:00
"""Volume up media player."""
2015-08-05 11:49:45 +00:00
self._lms.query(self._id, 'mixer', 'volume', '+5')
2015-08-04 15:22:56 +00:00
self.update_ha_state()
def volume_down(self):
2016-03-08 09:34:33 +00:00
"""Volume down media player."""
2015-08-05 11:49:45 +00:00
self._lms.query(self._id, 'mixer', 'volume', '-5')
2015-08-04 15:22:56 +00:00
self.update_ha_state()
def set_volume_level(self, volume):
2016-03-08 09:34:33 +00:00
"""Set volume level, range 0..1."""
2015-08-04 15:22:56 +00:00
volume_percent = str(int(volume*100))
2015-08-05 11:49:45 +00:00
self._lms.query(self._id, 'mixer', 'volume', volume_percent)
2015-08-04 15:22:56 +00:00
self.update_ha_state()
def mute_volume(self, mute):
2016-03-08 09:34:33 +00:00
"""Mute (true) or unmute (false) media player."""
2015-08-04 15:22:56 +00:00
mute_numeric = '1' if mute else '0'
2015-08-05 11:49:45 +00:00
self._lms.query(self._id, 'mixer', 'muting', mute_numeric)
2015-08-04 15:22:56 +00:00
self.update_ha_state()
def media_play_pause(self):
2016-03-08 09:34:33 +00:00
"""Send pause command to media player."""
2015-08-05 11:49:45 +00:00
self._lms.query(self._id, 'pause')
2015-08-04 15:22:56 +00:00
self.update_ha_state()
def media_play(self):
2016-03-08 09:34:33 +00:00
"""Send play command to media player."""
2015-08-05 11:49:45 +00:00
self._lms.query(self._id, 'play')
2015-08-04 15:22:56 +00:00
self.update_ha_state()
def media_pause(self):
2016-03-08 09:34:33 +00:00
"""Send pause command to media player."""
2015-10-18 19:05:30 +00:00
self._lms.query(self._id, 'pause', '1')
2015-08-04 15:22:56 +00:00
self.update_ha_state()
def media_next_track(self):
2016-03-08 09:34:33 +00:00
"""Send next track command."""
2015-08-05 11:49:45 +00:00
self._lms.query(self._id, 'playlist', 'index', '+1')
2015-08-04 15:22:56 +00:00
self.update_ha_state()
def media_previous_track(self):
2016-03-08 09:34:33 +00:00
"""Send next track command."""
2015-08-05 11:49:45 +00:00
self._lms.query(self._id, 'playlist', 'index', '-1')
2015-08-04 15:22:56 +00:00
self.update_ha_state()
def media_seek(self, position):
2016-03-08 09:34:33 +00:00
"""Send seek command."""
2015-08-05 11:49:45 +00:00
self._lms.query(self._id, 'time', position)
2015-08-04 15:22:56 +00:00
self.update_ha_state()
def turn_on(self):
2016-03-08 09:34:33 +00:00
"""Turn the media player on."""
2015-08-05 11:49:45 +00:00
self._lms.query(self._id, 'power', '1')
2015-08-04 15:22:56 +00:00
self.update_ha_state()