diff --git a/README.md b/README.md index 7c3a43b..1a4a43a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,19 @@ ## Release +v.0.9 (2016-11-20) + + -- added missing 'track_album' property + -- added missing 'track_album' to Sonos-Broker commandline + -- added "get_playlist_position" command and "playlist_position" property to Sonos Broker and + Sonos Broker commandline tool + -- added "get_playlist_total_tracks" command and "playlist_total_tracks" property to Sonos Broker and + Sonos Broker commandline tool + -- added missing 'track_album_art' property to Sonos Broker commandline tool + -- bug: 'playlist_position' was not handled correctly + -- changed maximum snippet length to 15 seconds + -- bugfixed: Sonos Broker user-specific server port was ignored + -- updated documentation + v.0.8.2 (2016-11-14) -- fixed bug in GoogleTTS @@ -316,7 +330,8 @@ In almost any cases, you'll get the appropriate response in the following JSON f "mute": "0", "pause": 0, "play": 0, - "playlist_position": 0, + "playlist_position": "1", + "playlist_total_tracks": "10", "playmode": "normal", "radio_show": "", "radio_station": "", @@ -326,6 +341,7 @@ In almost any cases, you'll get the appropriate response in the following JSON f "status": true, "stop": 1, "streamtype": "music", + "track_album": "Feuerwehrmann Sam 02", "track_album_art": "http://192.168.0.4:1400/getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a3xCk8npVehdV55KuPdjrmZ%3fsid%3d9%26flags%3d32", "track_artist": "Feuerwehrmann Sam & Clemens Gerhard", "track_duration": "0:10:15", @@ -396,10 +412,13 @@ Click on the links below to get a detailed command descriptions and their usage. ###### [set_playmode](#s_playmode) ###### [get_track_position](#g_track_position) ###### [set_track_position](#s_track_position) +###### [get_track_album](#g_track_album) ###### [get_track_title](#g_track_title) ###### [get_track_artist](#g_track_artist) ###### [get_track_album_art](#g_track_album_art) ###### [get_track_uri](#g_track_uri) +###### [get_playlist_position](#g_playlist_position) +###### [get_playlist_total_tracks](#g_playlist_total_tracks) ###### [get_radio_station](#g_radio_station) ###### [get_radio_show](#g_radio_show) ###### [join](#s_join) @@ -1496,6 +1515,36 @@ No special parameter needed. The response is only sent if the new value is different from the old value. +#### get_track_album + Returns the album title of the currently played track. + In most cases, you don't have to execute this command, because all subscribed clients will be notified automatically + about 'track_album'-status changes. + +| parameter | required / optional | valid values | description | +| :-------- | :------------------ | :----------- | :---------- | +| uid | required | | The UID of the Sonos speaker. | + +######Example + JSON format: + { + 'command': 'get_track_album', + 'parameter': { + 'uid': 'rincon_b8e93730d19801410' + } + } + +######HTTP Response + HTTP 200 OK or Exception with HTTP status 400 and the specific error message. + +######UDP Response sent to subscribed clients: + JSON format: + { + ... + "track_album": "Delta Machine", + "uid": "rincon_b8e93730d19801410", + ... + } + #### get_track_title Returns the title of the currently played track. In most cases, you don't have to execute this command, because all subscribed clients will be notified automatically @@ -1618,6 +1667,73 @@ No special parameter needed. All URIs can be passed to the play_uri and play_snippet functions. +---- +#### get_playlist_position + Returns the position of the currently played track in the playlist. + In most cases, you don't have to execute this command, because all subscribed clients will be notified automatically + about 'playlist_position'-status changes. + +| parameter | required / optional | valid values | description | +| :-------- | :------------------ | :----------- | :---------- | +| uid | required | string | The UID of the Sonos speaker. | + +######Example + JSON format: + { + 'command': 'get_playlist_position', + 'parameter': { + 'uid': 'rincon_b8e91111d11111400' + } + } + +######HTTP Response + HTTP 200 OK + or + Exception with HTTP status 400 and the specific error message. + +###### UDP Response sent to subscribed clients: + JSON format: + { + ... + "uid": "rincon_b8e91111d11111400", + "playlist_position": "3" + ... + } + +---- +#### get_playlist_total_tracks + Returns the total number of tracks in the current playlist. + In most cases, you don't have to execute this command, because all subscribed clients will be notified automatically + about 'playlist_total_tracks'-status changes. + +| parameter | required / optional | valid values | description | +| :-------- | :------------------ | :----------- | :---------- | +| uid | required | string | The UID of the Sonos speaker. | + +######Example + JSON format: + { + 'command': 'get_playlist_total_tracks', + 'parameter': { + 'uid': 'rincon_b8e91111d11111400' + } + } + +######HTTP Response + HTTP 200 OK + or + Exception with HTTP status 400 and the specific error message. + +###### UDP Response sent to subscribed clients: + JSON format: + { + ... + "uid": "rincon_b8e91111d11111400", + "playlist_total_tracks": "13" + ... + } + +---- #### get_radio_station Returns the title of the currently played radio station. In most cases, you don't have to execute this command, because all subscribed clients will be notified automatically @@ -2048,7 +2164,8 @@ No special parameter needed. "mute": 0, "pause": 1, "play": 0, - "playlist_position": 0, + "playlist_position": "1", + "playlist_total_tracks": "10", "playmode": "normal", "radio_show": "", "radio_station": "", diff --git a/plugin.sonos/README.md b/plugin.sonos/README.md index 8cce73c..5d155e3 100644 --- a/plugin.sonos/README.md +++ b/plugin.sonos/README.md @@ -3,6 +3,12 @@ Smarthome.py framework (https://github.com/mknx/smarthome). ##Release +v0.9 (2016-11-20) + + -- added missing 'track_album' property + -- add new property 'playlist_total_tracks' + -- change expected Sonos Broker version to 0.9 + v0.8.2 (2016-11-14) -- change expected Sonos Broker version to 0.8.2 @@ -25,25 +31,10 @@ v1.8 (2016-11-11) -- 'clear_queue' command added. The command clears the current queue. -- version check against Sonos Broker to identify an out-dated plugin or Broker -v1.7 (2016-01-03) - - -- bug: discover function call now working - -- command "balance" added; documentation updated - -v1.6 (2015-12-23) - - -- function 'discover' added to perform a manual scan for new Sonos speaker - -v1.5 (2015-10-30) - - -- property 'display_version' added - -- property 'model_number' added - -- property 'household_id' added (a unique identifier for all players in a household) - ## Requirements: - sonos_broker server v0.8 + sonos_broker server v0.8.3 (https://github.com/pfischi/shSonos) SmarthomeNG @@ -161,6 +152,10 @@ Edit file with this sample of mine: [[track_title]] type = str sonos_recv = track_title + + [[track_album]] + type = str + sonos_recv = track_album [[track_duration]] type = str @@ -189,6 +184,10 @@ Edit file with this sample of mine: type = num sonos_recv = playlist_position visu_acl = rw + + [[playlist_total_tracks]] + type = num + sonos_recv = playlist_total_tracks [[streamtype]] type = str diff --git a/plugin.sonos/__init__.py b/plugin.sonos/__init__.py index a29df96..8084fb0 100755 --- a/plugin.sonos/__init__.py +++ b/plugin.sonos/__init__.py @@ -31,7 +31,7 @@ import struct import requests -EXPECTED_BROKER_VERSION = "0.8.2" +EXPECTED_BROKER_VERSION = "0.9" logger = logging.getLogger('') sonos_speaker = {} @@ -503,7 +503,7 @@ def refresh_media_library(self, display_option='none'): return self._send_cmd(SonosCommand.refresh_media_library(display_option)) def version(self): - return "v0.8.2\t2016-11-14" + return "v0.9\t2016-11-20" def discover(self): return self._send_cmd(SonosCommand.discover()) @@ -525,6 +525,7 @@ def __init__(self): self.hardware_version = [] self.mac_address = [] self.playlist_position = [] + self.playlist_total_tracks = [] self.volume = [] self.mute = [] self.led = [] @@ -537,6 +538,7 @@ def __init__(self): self.track_duration = [] self.track_position = [] self.track_album_art = [] + self.track_album = [] self.track_uri = [] self.radio_station = [] self.radio_show = [] diff --git a/plugin.sonos/examples/sonos.conf b/plugin.sonos/examples/sonos.conf index 68f422e..53e6289 100644 --- a/plugin.sonos/examples/sonos.conf +++ b/plugin.sonos/examples/sonos.conf @@ -89,6 +89,10 @@ sonos_send = previous visu_acl = rw + [[track_album]] + type = str + sonos_recv = track_album + [[track_title]] type = str sonos_recv = track_title @@ -121,6 +125,10 @@ sonos_recv = playlist_position visu_acl = rw + [[playlist_total_tracks]] + type = num + sonos_recv = playlist_total_tracks + [[streamtype]] type = str sonos_recv = streamtype diff --git a/server.sonos/dist/sonos-broker-0.8.2.tar.gz b/server.sonos/dist/sonos-broker-0.8.2.tar.gz deleted file mode 100644 index 1e03a27..0000000 Binary files a/server.sonos/dist/sonos-broker-0.8.2.tar.gz and /dev/null differ diff --git a/server.sonos/dist/sonos-broker-0.9.tar.gz b/server.sonos/dist/sonos-broker-0.9.tar.gz new file mode 100644 index 0000000..51b8b73 Binary files /dev/null and b/server.sonos/dist/sonos-broker-0.9.tar.gz differ diff --git a/server.sonos/lib_sonos/definitions.py b/server.sonos/lib_sonos/definitions.py index 07abb69..5ed4b67 100644 --- a/server.sonos/lib_sonos/definitions.py +++ b/server.sonos/lib_sonos/definitions.py @@ -23,8 +23,8 @@ # regular expressions to find sonos meta info through udp stream ip_pattern = '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$' -VERSION_BUILDSTRING = "v0.8.2 (2016-11-14)" -VERSION = "0.8.2" +VERSION_BUILDSTRING = "v0.9 (2016-11-20)" +VERSION = "0.9" DEFAULT_HOST = '0.0.0.0' diff --git a/server.sonos/lib_sonos/sonos_commands.py b/server.sonos/lib_sonos/sonos_commands.py index e94a6c5..f7e1ff6 100644 --- a/server.sonos/lib_sonos/sonos_commands.py +++ b/server.sonos/lib_sonos/sonos_commands.py @@ -754,6 +754,62 @@ def run(self): return self._status, self._response +### PLAYLIST POSITION ################################################################################################## + +class GetPlaylistPosition(JsonCommandBase): + def __init__(self, parameter): + super().__init__(parameter) + + def run(self): + try: + logger.debug('COMMAND {classname} -- attributes: {attributes}'.format(classname=self.__class__.__name__, + attributes=utils.dump_attributes( + self))) + if self.uid not in sonos_speaker.sonos_speakers: + raise Exception('No speaker found with uid \'{uid}\'!'.format(uid=self.uid)) + + sonos_speaker.sonos_speakers[self.uid].dirty_property('playlist_position') + sonos_speaker.sonos_speakers[self.uid].send() + self._status = True + except requests.ConnectionError: + self._response = 'Unable to process command. Speaker with uid \'{uid}\'seems to be offline.'. \ + format(uid=self.uid) + except AttributeError as err: + self._response = JsonCommandBase.missing_param_error(err) + except Exception as err: + self._response = err + finally: + return self._status, self._response + + +### PLAYLIST TOTAL TRACKS ############################################################################################## + +class GetPlaylistTotalTracks(JsonCommandBase): + def __init__(self, parameter): + super().__init__(parameter) + + def run(self): + try: + logger.debug('COMMAND {classname} -- attributes: {attributes}'.format(classname=self.__class__.__name__, + attributes=utils.dump_attributes( + self))) + if self.uid not in sonos_speaker.sonos_speakers: + raise Exception('No speaker found with uid \'{uid}\'!'.format(uid=self.uid)) + + sonos_speaker.sonos_speakers[self.uid].dirty_property('playlist_total_tracks') + sonos_speaker.sonos_speakers[self.uid].send() + self._status = True + except requests.ConnectionError: + self._response = 'Unable to process command. Speaker with uid \'{uid}\'seems to be offline.'. \ + format(uid=self.uid) + except AttributeError as err: + self._response = JsonCommandBase.missing_param_error(err) + except Exception as err: + self._response = err + finally: + return self._status, self._response + + ### PLAY ############################################################################################################### class GetPlay(JsonCommandBase): @@ -1073,6 +1129,32 @@ def run(self): finally: return self._status, self._response +### TRACK ALBUM ######################################################################################################## + +class GetTrackAlbum(JsonCommandBase): + def __init__(self, parameter): + super().__init__(parameter) + + def run(self): + try: + logger.debug('COMMAND {classname} -- attributes: {attributes}'.format(classname=self.__class__.__name__, + attributes=utils.dump_attributes( + self))) + if self.uid not in sonos_speaker.sonos_speakers: + raise Exception('No speaker found with uid \'{uid}\'!'.format(uid=self.uid)) + + sonos_speaker.sonos_speakers[self.uid].dirty_property('track_album') + sonos_speaker.sonos_speakers[self.uid].send() + self._status = True + except requests.ConnectionError: + self._response = 'Unable to process command. Speaker with uid \'{uid}\'seems to be offline.'. \ + format(uid=self.uid) + except AttributeError as err: + self._response = JsonCommandBase.missing_param_error(err) + except Exception as err: + self._response = err + finally: + return self._status, self._response ### TRACK ALBUM COVER ################################################################################################## @@ -1102,7 +1184,7 @@ def run(self): return self._status, self._response -### TRACK TITLE ######################################################################################################## +### TRACK URI ########################################################################################################## class GetTrackUri(JsonCommandBase): def __init__(self, parameter): diff --git a/server.sonos/lib_sonos/sonos_service.py b/server.sonos/lib_sonos/sonos_service.py index a8bc30b..1ed99ee 100644 --- a/server.sonos/lib_sonos/sonos_service.py +++ b/server.sonos/lib_sonos/sonos_service.py @@ -312,6 +312,12 @@ def handle_AVTransport_event(self, speaker, variables): if 'current_track_uri' in variables: speaker.track_uri = variables['current_track_uri'] + if 'current_track' in variables: + speaker.playlist_position = variables['current_track'] + + if 'number_of_tracks' in variables: + speaker.playlist_total_tracks = variables['number_of_tracks'] + if 'current_playmode' in variables: speaker.playmode = variables['current_playmode'].lower() diff --git a/server.sonos/lib_sonos/sonos_speaker.py b/server.sonos/lib_sonos/sonos_speaker.py index 1ed5e96..7dcfb69 100644 --- a/server.sonos/lib_sonos/sonos_speaker.py +++ b/server.sonos/lib_sonos/sonos_speaker.py @@ -44,6 +44,7 @@ def __del__(self): def __init__(self, soco): info = soco.get_speaker_info(timeout=5) + self._fade_in = False self._balance = 0 self._saved_music_item = None @@ -55,6 +56,7 @@ def __init__(self, soco): self._alarms = '' self._mute = 0 self._track_uri = '' + self._track_album = '' self._track_duration = "00:00:00" self._track_position = "00:00:00" self._streamtype = '' @@ -69,6 +71,7 @@ def __init__(self, soco): self._led = 1 self._max_volume = -1 self._playlist_position = 0 + self._playlist_total_tracks = 0 self._model = '' self._status = True self._metadata = '' @@ -113,7 +116,6 @@ def soco(self): """ return self._soco - ### MODEL ########################################################################################################## @property @@ -558,7 +560,7 @@ def get_trackposition(self, force_refresh=False): if not self.is_coordinator: logger.debug("forwarding track_position getter to coordinator with uid {uid}". format(uid=self.zone_coordinator.uid)) - return self.zone_coordinator.get_trackposition(force_refresh=True) + return self.zone_coordinator.get_trackposition(force_refresh=force_refresh) if force_refresh: track_info = self.soco.get_current_track_info() @@ -600,6 +602,7 @@ def playlist_position(self): logger.debug("forwarding playlist_position getter to coordinator with uid {uid}". format(uid=self.zone_coordinator.uid)) return self.zone_coordinator.playlist_position + return self._playlist_position @playlist_position.setter @@ -614,6 +617,27 @@ def playlist_position(self, value): for speaker in self._zone_members: speaker.dirty_property('playlist_position') + ### PLAYLIST TOTAL NUMBER TRACKS ################################################################################### + + @property + def playlist_total_tracks(self): + if not self.is_coordinator: + logger.debug("forwarding playlist_position getter to coordinator with uid {uid}". + format(uid=self.zone_coordinator.uid)) + return self.zone_coordinator.playlist_total_tracks + return self._playlist_total_tracks + + @playlist_total_tracks.setter + def playlist_total_tracks(self, value): + if self._playlist_total_tracks == value: + return + self._playlist_total_tracks = value + self.dirty_property('playlist_total_tracks') + # dirty properties for all zone members, if coordinator + if self.is_coordinator: + for speaker in self._zone_members: + speaker.dirty_property('playlist_total_tracks') + ### STREAMTYPE ##################################################################################################### @property @@ -747,6 +771,29 @@ def set_pause(self, value, trigger_action=False): for speaker in self._zone_members: speaker.dirty_property('pause', 'play', 'stop') + + ### TRACK ALBUM #################################################################################################### + + @property + def track_album(self): + if not self.is_coordinator: + logger.debug("forwarding track_album getter to coordinator with uid {uid}". + format(uid=self.zone_coordinator.uid)) + return self.zone_coordinator.track_album + return self._track_album + + @track_album.setter + def track_album(self, value): + if self._track_album == value: + return + self._track_album = value + self.dirty_property('track_album') + + # dirty properties for all zone members, if coordinator + if self.is_coordinator: + for speaker in self._zone_members: + speaker.dirty_property('track_album') + ### RADIO STATION ################################################################################################## @property @@ -1095,6 +1142,7 @@ def dirty_music_metadata(self): 'track_artist', 'track_uri', 'track_duration', + 'track_album', 'stop', 'play', 'pause', @@ -1102,6 +1150,7 @@ def dirty_music_metadata(self): 'radio_station', 'radio_show', 'playlist_position', + 'playlist_total_tracks', 'streamtype', 'playmode', 'zone_icon', @@ -1136,7 +1185,7 @@ def dirty_all(self): 'alarms', 'is_coordinator', 'wifi_state', - 'balance' + 'balance', ) @property @@ -1178,6 +1227,7 @@ def status(self, value): self._track_duration = "00:00:00" self._track_position = "00:00:00" self._playlist_position = 0 + self._playlist_total_tracks = 0 self._track_uri = '' self._track_album_art = '' self._radio_show = '' @@ -1257,8 +1307,8 @@ def play_snippet(self, uri, volume=-1, group_command=False, fade_in=False): logger.debug('Estimated snippet length: {seconds}'.format(seconds=seconds)) # maximum snippet length is 60 sec - if seconds > 60: - seconds = 60 + if seconds > 10: + seconds = 10 if seconds < 3: seconds = 3 diff --git a/server.sonos/sonos_cmd b/server.sonos/sonos_cmd index ae76ccb..8fe44f6 100755 --- a/server.sonos/sonos_cmd +++ b/server.sonos/sonos_cmd @@ -562,6 +562,38 @@ class SonosSpeakerCmd(cmd.Cmd): return print("playmode: {}".format(self.playmode)) + def help_playlist_total_tracks(self): + print("playlist_total_tracks: Gets the total number of tracks in the current playlist.") + print("playlist_total_tracks [get]: Forces the Broker to retrieve the total number of tracks in the current " + "playlist.") + + def do_playlist_total_tracks(self, line): + line = line.lower() + if not line: + pass + elif line == "get": + self.commands.get_playlist_total_tracks(self.uid) + else: + print("unknown argument") + return + print("playlist_total_tracks: {}".format(self.playlist_total_tracks)) + + + def help_playlist_position(self): + print("playlist_position: Gets the current playlist position.") + print("playlist_position [get]: Forces the Broker to retrieve the current playlist position.") + + def do_playlist_position(self, line): + line = line.lower() + if not line: + pass + elif line == "get": + self.commands.get_playlist_position(self.uid) + else: + print("unknown argument") + return + print("playlist_position: {}".format(self.playlist_position)) + def help_track_position(self): print("track_position: Gets the current track position.") print("track_position [get]: Forces the Broker to retrieve the current track position.") @@ -661,6 +693,21 @@ class SonosSpeakerCmd(cmd.Cmd): return print("track_url: {}".format(self.track_uri)) + def help_track_album(self): + print("track_album: Gets the current track album title.") + print("track_album [get]: Forces the Broker to retrieve the current album title.") + + def do_track_album(self, line): + line = line.lower() + if not line: + pass + elif line == "get": + self.commands.get_track_album(self.uid) + else: + print("unknown argument") + return + print("track_album: {}".format(self.track_album)) + def help_track_title(self): print("track_title: Gets the current track title.") print("track_title [get]: Forces the Broker to retrieve the current track title.") @@ -691,6 +738,21 @@ class SonosSpeakerCmd(cmd.Cmd): return print("track_artist: {}".format(self.track_artist)) + def help_track_album_art(self): + print("track_album_art: Gets the current track album art/cover.") + print("track_album_art [get]: Forces the Broker to retrieve the current track album art/cover.") + + def do_track_album_art(self, line): + line = line.lower() + if not line: + pass + elif line == "get": + self.commands.get_track_album_art(self.uid) + else: + print("unknown argument") + return + print("track_album_art: {}".format(self.track_album_art)) + def help_radio_station(self): print("radio_station: Gets the current radio station.") print("radio_station [get]: Forces the Broker to retrieve the current radio station.") @@ -1388,6 +1450,36 @@ class Commands(): } ) + def get_track_album(self, uid): + return self.send( + { + 'command': 'get_track_album', + 'parameter': { + 'uid': uid.lower() + } + } + ) + + def get_playlist_position(self, uid): + return self.send( + { + 'command': 'get_playlist_position', + 'parameter': { + 'uid': uid.lower() + } + } + ) + + def get_playlist_total_tracks(self, uid): + return self.send( + { + 'command': 'get_playlist_total_tracks', + 'parameter': { + 'uid': uid.lower() + } + } + ) + def get_track_artist(self, uid): return self.send( { @@ -1398,6 +1490,16 @@ class Commands(): } ) + def get_track_album_art(self, uid): + return self.send( + { + 'command': 'get_track_album_art', + 'parameter': { + 'uid': uid.lower() + } + } + ) + def get_radio_station(self, uid): return self.send( { @@ -1614,5 +1716,6 @@ if __name__ == '__main__': client_ip = get_lan_ip() - broker_cmd = SonosBrokerCmd(client_hostname=client_ip, client_port=client_port, server_hostname=server_ip) + broker_cmd = SonosBrokerCmd(client_hostname=client_ip, client_port=client_port, server_hostname=server_ip, + server_port=server_port) broker_cmd.cmdloop()