From fded45c03133a784366fce2c8305ad096263619d Mon Sep 17 00:00:00 2001 From: LukeSpad <64772822+LukeSpad@users.noreply.github.com> Date: Fri, 17 Dec 2021 18:45:05 +0000 Subject: [PATCH] Add files via upload --- Resources/ASBridge.py | 27 ++++++++++++++++++++------- Resources/CONSTANTS.py | 26 ++++++++++++++++++-------- Resources/MLCLI_CLIENT.py | 13 ++++++++++++- Resources/MLCONFIG.py | 21 +++++++++++++++------ 4 files changed, 65 insertions(+), 22 deletions(-) diff --git a/Resources/ASBridge.py b/Resources/ASBridge.py index e1337fd..4a2c406 100644 --- a/Resources/ASBridge.py +++ b/Resources/ASBridge.py @@ -1,13 +1,14 @@ #!/usr/bin/python import os +import unicodedata from Foundation import NSAppleScript from ScriptingBridge import SBApplication ''' Module defining a MusicController class for Apple Music, enables: basic transport control of the player, query of player status, - playing existing playlists, and - running external appleScripts for more complex control + playing existing playlists, + running external appleScripts for more complex control, and reporting messages in the notification centre''' PLAYSTATE = dict([ @@ -26,9 +27,14 @@ class MusicController(object): # Player information def get_current_track_info(self): - name = str(self.app.currentTrack().name()) - album = str(self.app.currentTrack().album()) - artist = str(self.app.currentTrack().artist()) + name = self.app.currentTrack().name() + album = self.app.currentTrack().album() + artist = self.app.currentTrack().artist() + if name: + # Deal with tracks with non-ascii characters such as accents + name = unicodedata.normalize('NFD', name).encode('ascii', 'ignore') + album = unicodedata.normalize('NFD', album).encode('ascii', 'ignore') + artist = unicodedata.normalize('NFD', artist).encode('ascii', 'ignore') return [name, album, artist] def get_current_play_state(self): @@ -48,6 +54,7 @@ class MusicController(object): elif PLAYSTATE.get(self.app.playerState()) == 'Pause': self.app.playpause() elif PLAYSTATE.get(self.app.playerState()) == 'Stop': + self. app.setValue_forKey_('true', 'shuffleEnabled') playlist = self.app.sources().objectWithName_("Library") playlist.playOnce_(None) @@ -79,6 +86,12 @@ class MusicController(object): # ######################################################################################## # More complex playback control functions + def shuffle(self): + if self.app.shuffleEnabled(): + self.app.setValue_forKey_('false', 'shuffleEnabled') + else: + self.app.setValue_forKey_('true', 'shuffleEnabled') + def set_current_track_position(self, time, mode='Relative'): if mode == 'Relative': # Set playback position in seconds relative to current position @@ -102,10 +115,10 @@ class MusicController(object): s.executeAndReturnError_(None) @staticmethod - def notify(message): + def notify(message, subtitle): # Post message in notification center path = os.path.dirname(os.path.abspath(__file__)) script = 'tell application "' + path + '/Notify.app" to notify("BeoGateway", "' + \ - message + '")' + message + '", "' + subtitle + '")' s = NSAppleScript.alloc().initWithSource_(script) s.executeAndReturnError_(None) diff --git a/Resources/CONSTANTS.py b/Resources/CONSTANTS.py index 4d5fd51..dd1ed16 100644 --- a/Resources/CONSTANTS.py +++ b/Resources/CONSTANTS.py @@ -1,7 +1,14 @@ # Constants for B&O telegram protocols # ######################################################################################## # Config data (set on initialisation) -gateway = dict([("Current Audio Source", ''), ("Current Video Source", '')]) +gateway = dict( + [ + ("Current Audio Source", 'Unknown'), + ("Current Video Source", 'Unknown'), + ("Now Playing", 'Unknown'), + ("N.MUSIC Renderers", []) + ] +) rooms = [] devices = [] available_sources = [] @@ -13,7 +20,7 @@ source_type_dict = dict( [ ("Video Sources", ("TV", "V.AUX/DTV2", "MEDIA", "V.TAPE/V.MEM/DVD2", "DVD", "CAMERA", "SAT/DTV", "PC", "WEB", "DOORCAM", "PHOTO", "USB2", "WEBMEDIA", "AV.IN", - "HOMEMEDIA", "DNLA", "RECORDINGS", "CAMERA", "USB", "DNLA-DMR", "YOUTUBE", + "HOMEMEDIA", "DVB_RADIO", "DNLA", "RECORDINGS", "CAMERA", "USB", "DNLA-DMR", "YOUTUBE", "HOME.APP", "HDMI_1", "HDMI_2", "HDMI_3", "HDMI_4", "HDMI_5", "HDMI_6", "HDMI_7", "HDMI_8", "MATRIX_1", "MATRIX_2", "MATRIX_3", "MATRIX_4", "MATRIX_5", "MATRIX_6", "MATRIX_7", "MATRIX_8", "MATRIX_9", "MATRIX_10", "MATRIX_11", @@ -262,9 +269,9 @@ ml_command_type_dict = dict( (0x3C, "TIMER"), (0x40, "CLOCK"), (0x44, "TRACK_INFO"), - # LOCKmANAGER_COMMAND: Lock to Determine what device issues source commands - # reference: https://tidsskrift.dk/daimipb/article/download/7043/6004/0 (0x45, "GOTO_SOURCE"), + # LOCKMANAGER_COMMAND: Lock to Determine what device issues source commands + # reference: https://tidsskrift.dk/daimipb/article/download/7043/6004/0 (0x5C, "LOCK_MANAGER_COMMAND"), (0x6C, "DISTRIBUTION_REQUEST"), (0x82, "TRACK_INFO_LONG"), @@ -301,7 +308,8 @@ ml_command_type_dict = dict( # On power up all devices send out a request key telegram. If # no lock manager is allocated the devices send out a key_lost telegram. The Video Master (or Power # Master in older implementations) then asserts a NEW_LOCKmANAGER telegram and assumes responsibility - # for LOCKmANAGER_COMMAND telegrams until a key transfer occurs. + # for LOCKMANAGER_COMMAND telegrams until a key transfer occurs. + # reference: https://tidsskrift.dk/daimipb/article/download/7043/6004/0 (0x12, "KEY_LOST"), # ? # Unknown command with payload of length 1. # bit 0: unknown @@ -344,9 +352,11 @@ ml_device_dict = dict( (0x83, "ALL LINK DEVICES"), (0x80, "ALL"), (0xF0, "MLGW"), + (0x21, "BLC NL/ML"), # Power Master exists in older (pre 1996?) ML implementations. Later revisions enforced the Video Master # as lock key manager for the system and the concept was phased out. If your system is older than 2000 # you may see this device type on the network. + # reference: https://tidsskrift.dk/daimipb/article/download/7043/6004/0 (0xFF, "POWER MASTER"), # ? ] ) @@ -433,7 +443,7 @@ destselectordict = dict( [ (0x00, "Video Source"), (0x01, "Audio Source"), - (0x05, "V.TAPE/V.MEM"), + (0x05, "Secondary Video Source (V.TAPE/V.MEM)"), (0x0F, "All Products"), (0x1B, "MLGW"), ] @@ -451,9 +461,9 @@ mlgw_soundstatusdict = dict([(0x00, "Not muted"), (0x01, "Muted")]) mlgw_speakermodedict = dict( [ (0x01, "Center channel"), - (0x02, "2ch stereo"), + (0x02, "2 channel stereo"), (0x03, "Front surround"), - (0x04, "4ch stereo"), + (0x04, "4 channel stereo"), (0x05, "Full surround"), (0xFD, ""), # Dummy for 'Listen for all modes' ] diff --git a/Resources/MLCLI_CLIENT.py b/Resources/MLCLI_CLIENT.py index 188cb30..e45f47c 100644 --- a/Resources/MLCLI_CLIENT.py +++ b/Resources/MLCLI_CLIENT.py @@ -193,7 +193,10 @@ class MLCLIClient(asynchat.async_chat): # Decode header message = OrderedDict() self._get_device_info(message, telegram) - message["from_device"] = self._get_device_name(self._dictsanitize(CONST.ml_device_dict, telegram[1])) + if 'Device' not in message: + # If ML telegram has been matched to a Masterlink node in the devices list then the 'from_device' + # key is redundant - it will always be identical to the 'Device' key + message["from_device"] = self._dictsanitize(CONST.ml_device_dict, telegram[1]) message["from_source"] = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[5]) message["to_device"] = self._get_device_name(self._dictsanitize(CONST.ml_device_dict, telegram[0])) message["to_source"] = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[4]) @@ -252,6 +255,10 @@ class MLCLIClient(asynchat.async_chat): message["State_Update"]["sourceID"] = telegram[10] message["State_Update"]["source_type"] = self._get_type(CONST.ml_selectedsource_type_dict, telegram[10]) message["State_Update"]["command"] = self._dictsanitize(CONST.beo4_commanddict, telegram[11]) + if message["State_Update"]["command"] == 'Go/Play': + message["State_Update"]["state"] = 'Play' + elif message["State_Update"]["command"] in ['Standby', 'Stop', 'Wind', 'Rewind']: + message["State_Update"]["state"] = message["State_Update"]["command"] # audio track info long if message.get("payload_type") == "TRACK_INFO_LONG": @@ -304,6 +311,8 @@ class MLCLIClient(asynchat.async_chat): message["State_Update"]["source"] = source message["State_Update"]["sourceID"] = telegram[11] message["State_Update"]["source_type"] = self._get_type(CONST.ml_selectedsource_type_dict, telegram[11]) + # This device is playing + message["State_Update"]["state"] = 'Play' else: message["State_Update"]["subtype"] = "Undefined: " + self._hexbyte(telegram[9]) @@ -319,6 +328,8 @@ class MLCLIClient(asynchat.async_chat): message["State_Update"]["source"] = source message["State_Update"]["sourceID"] = telegram[11] self._get_channel_track(telegram, message) + # Device sending goto source command is playing + message["State_Update"]["state"] = 'Play' # remote request if message.get("payload_type") == "MLGW_REMOTE_BEO4": diff --git a/Resources/MLCONFIG.py b/Resources/MLCONFIG.py index 5b5b654..ca43adf 100644 --- a/Resources/MLCONFIG.py +++ b/Resources/MLCONFIG.py @@ -103,8 +103,11 @@ class MLConfig: for selectCmd in source["selectCmds"]: if gateway_type == 'blgw': # get source information from the BLGW config file - source_id = str(source['sourceId'].split(':')[0]) - source_id = self._srcdictsanitize(CONST.blgw_srcdict, source_id).upper() + if str(source['sourceId']) == '': + source_id = self._srcdictsanitize(CONST.beo4_commanddict, source['selectID']).upper() + else: + source_id = str(source['sourceId'].split(':')[0]) + source_id = self._srcdictsanitize(CONST.blgw_srcdict, source_id).upper() device['Sources'][str(source["name"])]['source'] = source_id device['Sources'][str(source["name"])]['uniqueID'] = str(source['sourceId']) else: @@ -142,7 +145,7 @@ class MLConfig: self.log.info('\tFound ' + str(len(CONST.available_sources)) + ' Available Sources [Name, Type]:') for i in range(len(CONST.available_sources)): self.log.info('\t\t' + str(list(CONST.available_sources[i]))) - self.log.info('Done!\n') + self.log.info('\tDone!\n') self.log.debug(json.dumps(CONST.gateway, indent=4)) self.log.debug(json.dumps(CONST.rooms, indent=4)) @@ -169,6 +172,7 @@ class MLConfig: mlcli.last_message['State_Update']['command'] == "Light Timeout": device['ML_ID'] = mlcli.last_message.get('to_device') + device['Serial_num'] = 'NA' self.log.info("\tMasterLink ID of product " + device.get('Device') + " is " + device.get('ML_ID') + ".\n") test = False @@ -177,9 +181,14 @@ class MLConfig: else: # If this is a NetLink product then it has a serial number and no ML_ID - device['ML_ID'] = 'NA' - self.log.info("\tNetworkLink ID of product " + device.get('Device') + " is " + - device.get('Serial_num') + ". No MasterLink ID assigned.\n") + if device['Device'] == 'BeoMaster 7000': + device['ML_ID'] = "AUDIO MASTER" + self.log.info("\tNetworkLink ID of product " + device.get('Device') + " is " + + device.get('Serial_num') + ". MasterLink ID manually assigned to AUDIO MASTER.\n") + else: + device['ML_ID'] = 'NA' + self.log.info("\tNetworkLink ID of product " + device.get('Device') + " is " + + device.get('Serial_num') + ". No MasterLink ID assigned.\n") self.log.debug(json.dumps(device, indent=4))