From db2cbf33c3172c889fcc70e95efb39bdd8d7e0f1 Mon Sep 17 00:00:00 2001
From: Per Sandstrom <Per.J.Sandstrom@gmail.com>
Date: Wed, 5 Aug 2015 13:49:45 +0200
Subject: [PATCH] Added support for multiple players

---
 .../components/media_player/squeezebox.py     | 221 ++++++++++--------
 1 file changed, 129 insertions(+), 92 deletions(-)

diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py
index 70eab958000..fa05e304c29 100644
--- a/homeassistant/components/media_player/squeezebox.py
+++ b/homeassistant/components/media_player/squeezebox.py
@@ -10,34 +10,24 @@ To use SqueezeBox add something like this to your configuration:
 
 media_player:
   platform: squeezebox
-  name: SqueezeBox
-  server: 192.168.1.21
-  player: Player1
+  host: 192.168.1.21
   port: 9090
-  user: user
+  username: user
   password: password
 
 Variables:
 
-name
-*Optional
-The name of the device
-
-server
+host
 *Required
-The address of the Logitech Media Server
-
-player
-*Required
-The unique name of the player
+The host name or address of the Logitech Media Server
 
 port
 *Optional
 Telnet port to Logitech Media Server, default 9090
 
-user
+usermame
 *Optional
-User, if password protection is enabled
+Username, if password protection is enabled
 
 password
 *Optional
@@ -51,8 +41,10 @@ import urllib.parse
 from homeassistant.components.media_player import (
     MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET,
     SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
-    MEDIA_TYPE_MUSIC)
+    MEDIA_TYPE_MUSIC, DOMAIN)
+
 from homeassistant.const import (
+    CONF_HOST, CONF_USERNAME, CONF_PASSWORD,
     STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_UNKNOWN)
 
 _LOGGER = logging.getLogger(__name__)
@@ -64,15 +56,104 @@ SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
 # pylint: disable=unused-argument
 def setup_platform(hass, config, add_devices, discovery_info=None):
     """ Sets up the squeezebox platform. """
-    add_devices([
-        SqueezeBoxDevice(
-            config.get('name', 'SqueezeBox'),
-            config.get('server'),
-            config.get('player'),
-            config.get('port', '9090'),
-            config.get('user', None),
-            config.get('password', None)
-        )])
+    if not config.get(CONF_HOST):
+        _LOGGER.error(
+            "Missing required configuration items in {}: {}".format(
+                DOMAIN, CONF_HOST))
+        return False
+
+    lms = LogitechMediaServer(
+        config.get(CONF_HOST),
+        config.get('port', '9090'),
+        config.get(CONF_USERNAME),
+        config.get(CONF_PASSWORD))
+
+    if not lms.init_success:
+        return False
+
+    add_devices(lms.create_players())
+
+    return True
+
+
+class LogitechMediaServer(object):
+    def __init__(self, host, port, username, password):
+        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 = None
+        try:
+            http_port = self.query('pref', 'httpport', '?')
+            if not http_port:
+                _LOGGER.error(
+                    "Unable to read data from server {}:{}".format(
+                        self.host,
+                        self.port))
+                return
+            return http_port
+        except ConnectionError as ex:
+            _LOGGER.error(
+                "Failed to connect to server {}:{} - {}".format(
+                    self.host,
+                    self.port,
+                    ex))
+            return
+
+    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
+
+    def query(self, *parameters):
+        """ Send request and await response from server  """
+        telnet = telnetlib.Telnet(self.host, self.port)
+        if self._username and self._password:
+            telnet.write('login {username} {password}\n'.format(
+                username=self._username,
+                password=self._password).encode('UTF-8'))
+            telnet.read_until(b'\n', timeout=3)
+        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)
+
+    def get_player_status(self, player):
+        """ Get ithe 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
+        tags = 'adK'
+        new_status = {}
+        telnet = telnetlib.Telnet(self.host, self.port)
+        telnet.write('{player} status - 1 tags:{tags}\n'.format(
+            player=player,
+            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]
+        return new_status
 
 
 # pylint: disable=too-many-instance-attributes
@@ -81,17 +162,12 @@ class SqueezeBoxDevice(MediaPlayerDevice):
     """ Represents a SqueezeBox device. """
 
     # pylint: disable=too-many-arguments
-    def __init__(self, name, server, player, port, user, password):
+    def __init__(self, lms, player_id):
         super(SqueezeBoxDevice, self).__init__()
-        self._name = name
-        self._server = server
-        self._player = player
-        self._port = port
-        self._user = user
-        self._password = password
-        self._status = {}
-        self.update()
-        self._http_port = self._query('pref', 'httpport', '?')
+        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):
@@ -113,47 +189,8 @@ class SqueezeBoxDevice(MediaPlayerDevice):
         return STATE_UNKNOWN
 
     def update(self):
-        self._get_status()
-
-    def _query(self, *parameters):
-        """ Send request and await response from server  """
-        telnet = telnetlib.Telnet(self._server, self._port)
-        if self._user and self._password:
-            telnet.write('login {user} {password}\n'.format(
-                user=self._user,
-                password=self._password).encode('UTF-8'))
-            telnet.read_until(b'\n', timeout=3)
-        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)
-
-    def _get_status(self):
-        """ request status and parse result """
-        #   (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 = {}
-        telnet = telnetlib.Telnet(self._server, self._port)
-        telnet.write('{player} status - 1 tags:{tags}\n'.format(
-            player=self._player,
-            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]
-        self._status = new_status
+        """ Retrieve latest state. """
+        self._status = self._lms.get_player_status(self._name)
 
     @property
     def volume_level(self):
@@ -190,9 +227,9 @@ class SqueezeBoxDevice(MediaPlayerDevice):
             return self._status['artwork_url']
         return 'http://{server}:{port}/music/current/cover.jpg?player={player}'\
             .format(
-                server=self._server,
-                port=self._http_port,
-                player=self._player)
+                server=self._lms.host,
+                port=self._lms.http_port,
+                player=self._id)
 
     @property
     def media_title(self):
@@ -212,64 +249,64 @@ class SqueezeBoxDevice(MediaPlayerDevice):
 
     def turn_off(self):
         """ turn_off media player. """
-        self._query(self._player, 'power', '0')
+        self._lms.query(self._id, 'power', '0')
         self.update_ha_state()
 
     def volume_up(self):
         """ volume_up media player. """
-        self._query(self._player, 'mixer', 'volume', '+5')
+        self._lms.query(self._id, 'mixer', 'volume', '+5')
         self.update_ha_state()
 
     def volume_down(self):
         """ volume_down media player. """
-        self._query(self._player, 'mixer', 'volume', '-5')
+        self._lms.query(self._id, '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._query(self._player, 'mixer', 'volume', volume_percent)
+        self._lms.query(self._id, '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._query(self._player, 'mixer', 'muting', mute_numeric)
+        self._lms.query(self._id, 'mixer', 'muting', mute_numeric)
         self.update_ha_state()
 
     def media_play_pause(self):
         """ media_play_pause media player. """
-        self._query(self._player, 'pause')
+        self._lms.query(self._id, 'pause')
         self.update_ha_state()
 
     def media_play(self):
         """ media_play media player. """
-        self._query(self._player, 'play')
+        self._lms.query(self._id, 'play')
         self.update_ha_state()
 
     def media_pause(self):
         """ media_pause media player. """
-        self._query(self._player, 'pause', '0')
+        self._lms.query(self._id, 'pause', '0')
         self.update_ha_state()
 
     def media_next_track(self):
         """ Send next track command. """
-        self._query(self._player, 'playlist', 'index', '+1')
+        self._lms.query(self._id, 'playlist', 'index', '+1')
         self.update_ha_state()
 
     def media_previous_track(self):
         """ Send next track command. """
-        self._query(self._player, 'playlist', 'index', '-1')
+        self._lms.query(self._id, 'playlist', 'index', '-1')
         self.update_ha_state()
 
     def media_seek(self, position):
         """ Send seek command. """
-        self._query(self._player, 'time', position)
+        self._lms.query(self._id, 'time', position)
         self.update_ha_state()
 
     def turn_on(self):
         """ turn the media player on. """
-        self._query(self._player, 'power', '1')
+        self._lms.query(self._id, 'power', '1')
         self.update_ha_state()
 
     def play_youtube(self, media_id):