From 338287f47723fa9d13bc792e8565d6b9408d9a0e Mon Sep 17 00:00:00 2001 From: LukeSpad <64772822+LukeSpad@users.noreply.github.com> Date: Thu, 25 Nov 2021 19:18:52 +0000 Subject: [PATCH] Add files via upload --- Resources/BLHIP_CLIENT.py | 74 ++++++---- Resources/CONSTANTS.py | 94 ++++++------- Resources/MLCLI_CLIENT.py | 280 ++++++++++++++++++++++++-------------- Resources/MLCONFIG.py | 90 ++++++------ Resources/MLGW_CLIENT.py | 241 +++++++++++++++++--------------- Resources/MLtn_CLIENT.py | 65 ++++----- Resources/__init__.py | 1 + 7 files changed, 490 insertions(+), 355 deletions(-) create mode 100644 Resources/__init__.py diff --git a/Resources/BLHIP_CLIENT.py b/Resources/BLHIP_CLIENT.py index f19077c..e2e670e 100644 --- a/Resources/BLHIP_CLIENT.py +++ b/Resources/BLHIP_CLIENT.py @@ -6,7 +6,8 @@ import json import urllib from collections import OrderedDict -import Resources.CONSTANTS as const +import Resources.CONSTANTS as CONST + class BLHIPClient(asynchat.async_chat): """Client to interact with a Beolink Gateway via the Home Integration Protocol @@ -32,7 +33,7 @@ class BLHIPClient(asynchat.async_chat): self.last_received_at = time.time() self.last_message = {} - #Optional callback function + # Optional callback function if cb: self.messageCallBack = cb else: @@ -87,35 +88,40 @@ class BLHIPClient(asynchat.async_chat): return if len(telegram) > 4: - state = telegram[4].replace('?','&') + state = telegram[4].replace('?', '&') state = state.split('&')[1:] message = OrderedDict() - message['Zone'] = telegram[0][2:].upper() - message['Room'] = telegram[1].upper() - message['Type'] = telegram[2].upper() + message['Zone'] = telegram[0][2:].upper() + message['Room'] = telegram[1].upper() + message['Type'] = telegram[2].upper() message['Device'] = telegram[3] message['State_Update'] = OrderedDict() for s in state: if s.split('=')[0] == "nowPlayingDetails": - playDetails = s.split('=') - if len(playDetails[1]) >0: - playDetails = playDetails[1].split('; ') + play_details = s.split('=') + if len(play_details[1]) > 0: + play_details = play_details[1].split('; ') message['State_Update']["nowPlayingDetails"] = OrderedDict() - for p in playDetails: - if p.split(': ')[0] in ['track number','channel number']: + for p in play_details: + if p.split(': ')[0] in ['track number', 'channel number']: message['State_Update']["nowPlayingDetails"]['channel_track'] = p.split(': ')[1] else: message['State_Update']["nowPlayingDetails"][p.split(': ')[0]] = p.split(': ')[1] elif s.split('=')[0] == "sourceUniqueId": src = s.split('=')[1].split(':')[0].upper() - message['State_Update']['source'] = self._srcdictsanitize(const._blgw_srcdict, src) + message['State_Update']['source'] = self._srcdictsanitize(CONST.blgw_srcdict, src) message['State_Update'][s.split('=')[0]] = s.split('=')[1] else: message['State_Update'][s.split('=')[0]] = s.split('=')[1] + # call function to find channel details if type = Legacy + if 'nowPlayingDetails' in message['State_Update'] \ + and message['State_Update']['nowPlayingDetails']['type'] == 'Legacy': + self._get_channel_track(message) + if message.get('Type') == 'BUTTON': if message['State_Update'].get('STATE') == '0': message['State_Update']['Status'] = 'Off' @@ -131,7 +137,7 @@ class BLHIPClient(asynchat.async_chat): self._report(header, state, message) def _report(self, header, payload, message): - #Report messages, excluding regular clock pings from gateway + # Report messages, excluding regular clock pings from gateway self.last_message = message if message.get('Device').upper() != 'CLOCK': self.log.debug(self.name + "\n" + str(json.dumps(message, indent=4))) @@ -141,25 +147,24 @@ class BLHIPClient(asynchat.async_chat): def client_connect(self): self.log.info('Connecting to host at %s, port %i', self._host, self._port) self.set_terminator(b'\r\n') - #Create the socket + # Create the socket try: - socket.setdefaulttimeout(3) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) except socket.error, e: self.log.info("Error creating socket: %s" % e) self.handle_close() - #Now connect + # Now connect try: self.connect((self._host, self._port)) except socket.gaierror, e: self.log.info("\tError with address %s:%i - %s" % (self._host, self._port, e)) self.handle_close() - except socket.error, e: - self.log.info("\tError opening connection to %s:%i - %s" % (self._host, self._port, e)) - self.handle_close() except socket.timeout, e: self.log.info("\tSocket connection to %s:%i timed out- %s" % (self._host, self._port, e)) self.handle_close() + except socket.error, e: + self.log.info("\tError opening connection to %s:%i - %s" % (self._host, self._port, e)) + self.handle_close() else: self.is_connected = True self.log.info("\tConnected to B&O Gateway") @@ -175,25 +180,25 @@ class BLHIPClient(asynchat.async_chat): self.is_connected = False self.close() - def send_cmd(self,telegram): + def send_cmd(self, telegram): try: self.push(telegram.encode("ascii") + "\r\n") - except socket.error, e: - self.log.info("Error sending data: %s" % e) - self.handle_close() except socket.timeout, e: self.log.info("\tSocket connection to %s:%i timed out- %s" % (self._host, self._port, e)) self.handle_close() + except socket.error, e: + self.log.info("Error sending data: %s" % e) + self.handle_close() else: self.last_sent = telegram self.last_sent_at = time.time() self.log.info(self.name + " >>-SENT--> : " + telegram) time.sleep(0.2) - def query(self, zone='*',room='*',dev_type='*',device='*'): + def query(self, zone='*', room='*', dev_type='*', device='*'): query = "q " + zone + "/" + room + "/" + dev_type + '/' + device - #Convert to human readable string + # Convert to human readable string if zone == '*': zone = ' in all zones.' else: @@ -214,7 +219,7 @@ class BLHIPClient(asynchat.async_chat): self.log.info(self.name + ": sending state update request for" + device + dev_type + room + zone) self.send_cmd(query) - def statefiler(self, zone='*',room='*',dev_type='*',device='*'): + def statefiler(self, zone='*', room='*', dev_type='*', device='*'): s_filter = "f " + zone + "/" + room + "/" + dev_type + '/' + device self.send_cmd(s_filter) @@ -226,9 +231,22 @@ class BLHIPClient(asynchat.async_chat): def ping(self): self.query('Main', 'global', 'SYSTEM', 'BeoLink') - def _srcdictsanitize(self, d, s): + # Utility Functions + @staticmethod + def _srcdictsanitize(d, s): result = d.get(s) - if result == None: + if result is None: result = s return str(result) + @staticmethod + def _get_channel_track(message): + # Check device list for channel name information + if CONST.devices: + for device in CONST.devices: + if device['Device'] == message['Device']: + if 'channels' in device['Sources'][message["State_Update"]["source"]]: + for channel in device['Sources'][message["State_Update"]["source"]]['channels']: + if channel['number'] == int(message["State_Update"]['nowPlayingDetails']["channel_track"]): + message["State_Update"]["nowPlaying"] = channel['name'] + break diff --git a/Resources/CONSTANTS.py b/Resources/CONSTANTS.py index 2392432..48c601b 100644 --- a/Resources/CONSTANTS.py +++ b/Resources/CONSTANTS.py @@ -1,13 +1,13 @@ # Constants for B&O telegram protocols # ######################################################################################## -### Config data (set on initialisation) +# Config data (set on initialisation) gateway = dict() rooms = [] devices = [] available_sources = [] # ######################################################################################## -### Beo4 Commands +# Beo4 Commands beo4_commanddict = dict( [ # Source selection: @@ -124,10 +124,10 @@ beo4_commanddict = dict( ) BEO4_CMDS = {v.upper(): k for k, v in beo4_commanddict.items()} -### BeoRemote One Commands +# BeoRemote One Commands beoremoteone_commanddict = dict( [ - #Source, (Cmd, Unit) + # Source, (Cmd, Unit) ("TV", (0x80, 0)), ("RADIO", (0x81, 0)), ("TUNEIN", (0x81, 1)), @@ -189,13 +189,13 @@ beoremoteone_commanddict = dict( ("PERSONAL_8", (0xD1, 7)), ("TV.ON", (0xD2, 0)), ("MUSIC.ON", (0xD3, 0)), - ("PATTERNPLAY",(0xD3, 1)), + ("PATTERNPLAY", (0xD3, 1)), ] ) # ######################################################################################## # Source Activity -_sourceactivitydict = dict( +sourceactivitydict = dict( [ (0x00, "Unknown"), (0x01, "Stop"), @@ -214,7 +214,7 @@ _sourceactivitydict = dict( # ######################################################################################## # ##### MasterLink (not MLGW) Protocol packet constants -_ml_telegram_type_dict = dict( +ml_telegram_type_dict = dict( [ (0x0A, "COMMAND"), (0x0B, "REQUEST"), @@ -224,7 +224,7 @@ _ml_telegram_type_dict = dict( ] ) -_ml_command_type_dict = dict( +ml_command_type_dict = dict( [ (0x04, "MASTER_PRESENT"), # REQUEST_DISTRIBUTED_SOURCE: seen when a device asks what source is being distributed @@ -242,7 +242,7 @@ _ml_command_type_dict = dict( (0x3C, "TIMER"), (0x40, "CLOCK"), (0x44, "TRACK_INFO"), - # LOCK_MANAGER_COMMAND: Lock to Determine what device issues source commands + # LOCKmANAGER_COMMAND: Lock to Determine what device issues source commands # reference: https://tidsskrift.dk/daimipb/article/download/7043/6004/0 (0x45, "GOTO_SOURCE"), (0x5C, "LOCK_MANAGER_COMMAND"), @@ -280,13 +280,13 @@ _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_LOCK_MANAGER telegram and assumes responsibility - # for LOCK_MANAGER_COMMAND telegrams until a key transfer occurs. - (0x12, "KEY_LOST"), #? + # Master in older implementations) then asserts a NEW_LOCKmANAGER telegram and assumes responsibility + # for LOCKmANAGER_COMMAND telegrams until a key transfer occurs. + (0x12, "KEY_LOST"), # ? # Unknown command with payload of length 1. # bit 0: unknown # bit 1: unknown - (0xA0, "NEW_LOCK_MANAGER"), #? + (0xA0, "NEW_LOCKMANAGER"), # ? # Unknown command with payload of length 2 # bit 0: unknown # bit 1: unknown @@ -294,7 +294,7 @@ _ml_command_type_dict = dict( ] ) -_ml_command_type_request_key_subtype_dict = dict( +ml_command_type_request_key_subtype_dict = dict( [ (0x01, "Request Key"), (0x02, "Transfer Key"), @@ -305,7 +305,7 @@ _ml_command_type_request_key_subtype_dict = dict( ] ) -_ml_activity_dict = dict( +ml_activity_dict = dict( [ (0x01, "Request Source"), (0x02, "Request Source"), @@ -314,24 +314,24 @@ _ml_activity_dict = dict( ] ) -_ml_device_dict = dict( +ml_device_dict = dict( [ - (0xC0, "VIDEO_MASTER"), - (0xC1, "AUDIO_MASTER"), - (0xC2, "SOURCE_CENTER"), - (0x81, "ALL_AUDIO_LINK_DEVICES"), - (0x82, "ALL_VIDEO_LINK_DEVICES"), - (0x83, "ALL_LINK_DEVICES"), + (0xC0, "VIDEO MASTER"), + (0xC1, "AUDIO MASTER"), + (0xC2, "SOURCE CENTER/SLAVE DEVICE"), + (0x81, "ALL AUDIO LINK DEVICES"), + (0x82, "ALL VIDEO LINK DEVICES"), + (0x83, "ALL LINK DEVICES"), (0x80, "ALL"), (0xF0, "MLGW"), # 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. - (0xFF, "POWER_MASTER"), #? + (0xFF, "POWER MASTER"), # ? ] ) -_ml_pictureformatdict = dict( +ml_pictureformatdict = dict( [ (0x00, "Not known"), (0x01, "Known by decoder"), @@ -344,7 +344,7 @@ _ml_pictureformatdict = dict( ] ) -_ml_selectedsourcedict = dict( +ml_selectedsourcedict = dict( [ (0x00, "NONE"), (0x0B, "TV"), @@ -367,9 +367,9 @@ _ml_selectedsourcedict = dict( ] ) -_ml_trackinfo_subtype_dict = dict([(0x05, "Current Source"),(0x07, "Change Source"),]) +ml_trackinfo_subtype_dict = dict([(0x05, "Current Source"), (0x07, "Change Source"), ]) -_ml_selectedsource_type_dict = dict( +ml_selectedsource_type_dict = dict( [ ("VIDEO", (0x0B, 0x1F)), ("VIDEO_PAUSABLE", (0x15, 0x16, 0x29, 0x33)), @@ -382,7 +382,7 @@ _ml_selectedsource_type_dict = dict( # ######################################################################################## # ##### MLGW Protocol packet constants -_mlgw_payloadtypedict = dict( +mlgw_payloadtypedict = dict( [ (0x01, "Beo4 Command"), (0x02, "Source Status"), @@ -405,9 +405,9 @@ _mlgw_payloadtypedict = dict( (0x40, "Location based event"), ] ) -MLGW_PL = {v.upper(): k for k, v in _mlgw_payloadtypedict.items()} +MLGW_PL = {v.upper(): k for k, v in mlgw_payloadtypedict.items()} -_destselectordict = dict( +destselectordict = dict( [ (0x00, "Video Source"), (0x01, "Audio Source"), @@ -416,41 +416,41 @@ _destselectordict = dict( (0x1B, "MLGW"), ] ) -CMDS_DEST = {v.upper(): k for k, v in _destselectordict.items()} +CMDS_DEST = {v.upper(): k for k, v in destselectordict.items()} -_mlgw_secsourcedict = dict([(0x00, "V.TAPE/V.MEM"),(0x01, "V.TAPE2/DVD2/V.MEM2"),]) -_mlgw_linkdict = dict([(0x00, "Local/Default Source"),(0x01, "Remote Source/Option 4 Product"),]) +mlgw_secsourcedict = dict([(0x00, "V.TAPE/V.MEM"), (0x01, "V.TAPE2/DVD2/V.MEM2")]) +mlgw_linkdict = dict([(0x00, "Local/Default Source"), (0x01, "Remote Source/Option 4 Product")]) -_mlgw_virtualactiondict = dict([(0x01, "PRESS"), (0x02, "HOLD"), (0x03, "RELEASE")]) +mlgw_virtualactiondict = dict([(0x01, "PRESS"), (0x02, "HOLD"), (0x03, "RELEASE")]) -### for '0x03: Picture and Sound Status' -_mlgw_soundstatusdict = dict([(0x00, "Not muted"), (0x01, "Muted")]) +# for '0x03: Picture and Sound Status' +mlgw_soundstatusdict = dict([(0x00, "Not muted"), (0x01, "Muted")]) -_mlgw_speakermodedict = dict( +mlgw_speakermodedict = dict( [ (0x01, "Center channel"), (0x02, "2ch stereo"), (0x03, "Front surround"), (0x04, "4ch stereo"), (0x05, "Full surround"), - (0xFD, ""), # Dummy for 'Listen for all modes' + (0xFD, ""), # Dummy for 'Listen for all modes' ] ) -_mlgw_screenmutedict = dict([(0x00, "not muted"), (0x01, "muted")]) -_mlgw_screenactivedict = dict([(0x00, "not active"), (0x01, "active")]) -_mlgw_cinemamodedict = dict([(0x00, "Cinemamode=off"), (0x01, "Cinemamode=on")]) -_mlgw_stereoindicatordict = dict([(0x00, "Mono"), (0x01, "Stereo")]) +mlgw_screenmutedict = dict([(0x00, "not muted"), (0x01, "muted")]) +mlgw_screenactivedict = dict([(0x00, "not active"), (0x01, "active")]) +mlgw_cinemamodedict = dict([(0x00, "Cinema mode off"), (0x01, "Cinema mode on")]) +mlgw_stereoindicatordict = dict([(0x00, "Mono"), (0x01, "Stereo")]) -### for '0x04: Light and Control command' -_mlgw_lctypedict = dict([(0x01, "LIGHT"), (0x02, "CONTROL")]) +# for '0x04: Light and Control command' +mlgw_lctypedict = dict([(0x01, "LIGHT"), (0x02, "CONTROL")]) -### for '0x31: Login Status -_mlgw_loginstatusdict = dict([(0x00, "OK"), (0x01, "FAIL")]) +# for '0x31: Login Status +mlgw_loginstatusdict = dict([(0x00, "OK"), (0x01, "FAIL")]) # ######################################################################################## # ##### BeoLink Gateway Protocol packet constants -_blgw_srcdict = dict( +blgw_srcdict = dict( [ ("TV", "TV"), ("DVD", "DVD"), diff --git a/Resources/MLCLI_CLIENT.py b/Resources/MLCLI_CLIENT.py index c5a776c..5bfb1db 100644 --- a/Resources/MLCLI_CLIENT.py +++ b/Resources/MLCLI_CLIENT.py @@ -5,7 +5,8 @@ import time import json from collections import OrderedDict -import Resources.CONSTANTS as const +import Resources.CONSTANTS as CONST + class MLCLIClient(asynchat.async_chat): """Client to monitor raw packet traffic on the Masterlink network via the undocumented command line interface @@ -57,7 +58,7 @@ class MLCLIClient(asynchat.async_chat): telegram = self._received_data self._received_data = "" - #clear login process lines before processing telegrams + # Clear login process lines before processing telegrams if self._i <= self._header_lines: self._i += 1 if self._i == self._header_lines - 1: @@ -65,32 +66,43 @@ class MLCLIClient(asynchat.async_chat): if telegram[0:4] != "MLGW": self.isBLGW = True - #Process telegrams and return json data in human readable format + # Process telegrams and return json data in human readable format if self._i > self._header_lines: - items = telegram.split()[1:] - if len(items): - telegram=bytearray() - for item in items: - try: - telegram.append(int(item[:-1],base=16)) - except: - #abort if invalid character found - break + if "---- Logging" in telegram: + # Pong telegram + header = telegram + payload = [] + message = OrderedDict([('payload_type', 'Pong'), ('CONNECTION', 'Online')]) + self.is_connected = True + if self.messageCallBack: + self.messageCallBack(self.name, header, str(list(payload)), message) + else: + # ML protocol message detected + items = telegram.split()[1:] + if len(items): + telegram = bytearray() + for item in items: + try: + telegram.append(int(item[:-1], base=16)) + except: + # abort if invalid character found + self.log.debug('Invalid character ' + str(item) + ' found in telegram: ' + + ''.join(items) + '\nAborting!') + break - #Decode any telegram with a valid 9 byte header, excluding typy 0x14 (regular clock sync pings) - if len(telegram) >= 9 and telegram[7] != 0x14: - #Header: To_Device/From_Device/1/Type/To_Source/From_Source/0/Payload_Type/Length - header = telegram[:9] - payload = telegram[9:] - message = self._decode(telegram) - self._report(header, payload, message) + # Decode any telegram with a valid 9 byte header, excluding typy 0x14 (regular clock sync pings) + if len(telegram) >= 9 and telegram[7] != 0x14: + # Header: To_Device/From_Device/1/Type/To_Source/From_Source/0/Payload_Type/Length + header = telegram[:9] + payload = telegram[9:] + message = self._decode(telegram) + self._report(header, payload, message) def client_connect(self): self.log.info('Connecting to host at %s, port %i', self._host, self._port) self.set_terminator(b'\r\n') # Create the socket try: - socket.setdefaulttimeout(3) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) except socket.error, e: self.log.info("Error creating socket: %s" % e) @@ -101,12 +113,12 @@ class MLCLIClient(asynchat.async_chat): except socket.gaierror, e: self.log.info("\tError with address %s:%i - %s" % (self._host, self._port, e)) self.handle_close() - except socket.error, e: - self.log.info("\tError opening connection to %s:%i - %s" % (self._host, self._port, e)) - self.handle_close() except socket.timeout, e: self.log.info("\tSocket connection to %s:%i timed out- %s" % (self._host, self._port, e)) self.handle_close() + except socket.error, e: + self.log.info("\tError opening connection to %s:%i - %s" % (self._host, self._port, e)) + self.handle_close() else: self.is_connected = True self.log.info("\tConnected to B&O Gateway") @@ -121,15 +133,15 @@ class MLCLIClient(asynchat.async_chat): self.is_connected = False self.close() - def send_cmd(self,telegram): + def send_cmd(self, telegram): try: self.push(telegram + "\r\n") - except socket.error, e: - self.log.info("Error sending data: %s" % e) - self.handle_close() except socket.timeout, e: self.log.info("\tSocket connection to %s:%i timed out- %s" % (self._host, self._port, e)) self.handle_close() + except socket.error, e: + self.log.info("Error sending data: %s" % e) + self.handle_close() else: self.last_sent = telegram self.last_sent_at = time.time() @@ -149,7 +161,8 @@ class MLCLIClient(asynchat.async_chat): # ######################################################################################## # ##### Utility functions - def _hexbyte(self, byte): + @staticmethod + def _hexbyte(byte): resultstr = hex(byte) if byte < 16: resultstr = resultstr[:2] + "0" + resultstr[2] @@ -162,12 +175,13 @@ class MLCLIClient(asynchat.async_chat): def _dictsanitize(self, d, s): result = d.get(s) - if result == None: + if result is None: result = self._hexbyte(s) self.log.debug("UNKNOWN (type=" + result + ")") return str(result) - def _get_type(self, d, s): + @staticmethod + def _get_type(d, s): rev_dict = {value: key for key, value in d.items()} for i in range(len(list(rev_dict))): if s in list(rev_dict)[i]: @@ -175,127 +189,195 @@ class MLCLIClient(asynchat.async_chat): # ######################################################################################## # ##### Decode Masterlink Protocol packet to a serializable dict - def _decode(self, telegram): # Decode header message = OrderedDict() - if const.devices: - for device in const.devices: + if CONST.devices: + for device in CONST.devices: if device['ML_ID'] == telegram[1]: message["Zone"] = device["Zone"].upper() message["Room"] = device["Room"].uppr() message["Type"] = "AV RENDERER" message["Device"] = device["Device"] - 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._dictsanitize(const._ml_device_dict, telegram[0]) - message["to_source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[4]) - message["type"] = self._dictsanitize(const._ml_telegram_type_dict, telegram[3]) - message["payload_type"] = self._dictsanitize(const._ml_command_type_dict, telegram[7]) + break + 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._dictsanitize(CONST.ml_device_dict, telegram[0]) + message["to_source"] = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[4]) + message["type"] = self._dictsanitize(CONST.ml_telegram_type_dict, telegram[3]) + message["payload_type"] = self._dictsanitize(CONST.ml_command_type_dict, telegram[7]) message["payload_len"] = telegram[8] + 1 - message["payload"] = OrderedDict() + message["State_Update"] = OrderedDict() # source status info # TTFF__TYDSOS__PTLLPS SR____LS______SLSHTR__ACSTPI________________________TRTR______ if message.get("payload_type") == "STATUS_INFO": - message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[10]) - message["payload"]["sourceID"] = telegram[10] - message["payload"]["source_type"] = telegram[22] - message["payload"]["local_source"] = telegram[13] - message["payload"]["source_medium"] = self._hexword(telegram[18], telegram[17]) - message["payload"]["channel_track"] = ( - telegram[19] if telegram[8] < 27 else (telegram[36] * 256 + telegram[37]) - ) - message["payload"]["picture_identifier"] = self._dictsanitize(const._ml_pictureformatdict, telegram[23]) - message["payload"]["state"] = self._dictsanitize(const._sourceactivitydict, telegram[21]) + message["State_Update"]["nowPlaying"] = '' + message["State_Update"]["nowPlayingDetails"] = OrderedDict() + message["State_Update"]["nowPlayingDetails"]["local_source"] = telegram[13] + message["State_Update"]["nowPlayingDetails"]["type"] = telegram[22] + if telegram[8] < 27: + message["State_Update"]["nowPlayingDetails"]["channel_track"] = telegram[19] + else: + message["State_Update"]["nowPlayingDetails"]["channel_track"] = telegram[36] * 256 + telegram[37] + message["State_Update"]["nowPlayingDetails"]["source_medium_position"] = \ + self._hexword(telegram[18], telegram[17]) + message["State_Update"]["nowPlayingDetails"]["picture_identifier"] = \ + self._dictsanitize(CONST.ml_pictureformatdict, telegram[23]) + source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[10]) + self._get_source_name(source, message) + message["State_Update"]["source"] = source + message["State_Update"]["sourceID"] = telegram[10] + self._get_channel_track(telegram, message) + message["State_Update"]["state"] = self._dictsanitize(CONST.sourceactivitydict, telegram[21]) # display source information if message.get("payload_type") == "DISPLAY_SOURCE": _s = "" for i in range(0, telegram[8] - 5): _s = _s + chr(telegram[i + 15]) - message["payload"]["display_source"] = _s.rstrip() + message["State_Update"]["display_source"] = _s.rstrip() # extended source information if message.get("payload_type") == "EXTENDED_SOURCE_INFORMATION": - message["payload"]["info_type"] = telegram[10] + message["State_Update"]["info_type"] = telegram[10] _s = "" for i in range(0, telegram[8] - 14): _s = _s + chr(telegram[i + 24]) - message["payload"]["info_value"] = _s + message["State_Update"]["info_value"] = _s # beo4 command if message.get("payload_type") == "BEO4_KEY": - message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[10]) - message["payload"]["sourceID"] = telegram[10] - message["payload"]["source_type"] = self._get_type(const._ml_selectedsource_type_dict, telegram[10]) - message["payload"]["command"] = self._dictsanitize(const.beo4_commanddict, telegram[11]) + source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[10]) + self._get_source_name(source, message) + message["State_Update"]["source"] = source + 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]) # audio track info long if message.get("payload_type") == "TRACK_INFO_LONG": - message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[11]) - message["payload"]["sourceID"] = telegram[11] - message["payload"]["source_type"] = self._get_type(const._ml_selectedsource_type_dict, telegram[11]) - message["payload"]["channel_track"] = telegram[12] - message["payload"]["state"] = self._dictsanitize(const._sourceactivitydict, telegram[13]) + message["State_Update"]["nowPlaying"] = '' + message["State_Update"]["nowPlayingDetails"] = OrderedDict() + message["State_Update"]["nowPlayingDetails"]["type"] = \ + self._get_type(CONST.ml_selectedsource_type_dict, telegram[11]) + message["State_Update"]["nowPlayingDetails"]["channel_track"] = telegram[12] + source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[11]) + self._get_source_name(source, message) + message["State_Update"]["source"] = source + message["State_Update"]["sourceID"] = telegram[11] + self._get_channel_track(telegram, message) + message["State_Update"]["state"] = self._dictsanitize(CONST.sourceactivitydict, telegram[13]) # video track info if message.get("payload_type") == "VIDEO_TRACK_INFO": - message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[13]) - message["payload"]["sourceID"] = telegram[13] - message["payload"]["source_type"] = self._get_type(const._ml_selectedsource_type_dict, telegram[13]) - message["payload"]["channel_track"] = telegram[11] * 256 + telegram[12] - message["payload"]["state"] = self._dictsanitize(const._sourceactivitydict, telegram[14]) + message["State_Update"]["nowPlaying"] = '' + message["State_Update"]["nowPlayingDetails"] = OrderedDict() + message["State_Update"]["nowPlayingDetails"]["source_type"] = \ + self._get_type(CONST.ml_selectedsource_type_dict, telegram[13]) + message["State_Update"]["nowPlayingDetails"]["channel_track"] = telegram[11] * 256 + telegram[12] + source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[13]) + self._get_source_name(source, message) + message["State_Update"]["source"] = source + message["State_Update"]["sourceID"] = telegram[13] + self._get_channel_track(telegram, message) + message["State_Update"]["state"] = self._dictsanitize(CONST.sourceactivitydict, telegram[14]) # track change info if message.get("payload_type") == "TRACK_INFO": - message["payload"]["subtype"] = self._dictsanitize(const._ml_trackinfo_subtype_dict, telegram[9]) - if message["payload"].get("subtype") == "Change Source": - message["payload"]["prev_source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[11]) - message["payload"]["prev_sourceID"] = telegram[11] - message["payload"]["prev_source_type"] = self._get_type( - const._ml_selectedsource_type_dict, telegram[11]) + message["State_Update"]["subtype"] = self._dictsanitize(CONST.ml_trackinfo_subtype_dict, telegram[9]) + if message["State_Update"].get("subtype") == "Change Source": + message["State_Update"]["prev_source"] = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[11]) + message["State_Update"]["prev_sourceID"] = telegram[11] + message["State_Update"]["prev_source_type"] = self._get_type( + CONST.ml_selectedsource_type_dict, telegram[11]) if len(telegram) > 18: - message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[22]) - message["payload"]["sourceID"] = telegram[22] - if message["payload"].get("subtype") == "Current Source": - message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[11]) - message["payload"]["sourceID"] = telegram[11] - message["payload"]["source_type"] = self._get_type(const._ml_selectedsource_type_dict, telegram[11]) + source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[22]) + self._get_source_name(source, message) + message["State_Update"]["source"] = source + message["State_Update"]["sourceID"] = telegram[22] + + if message["State_Update"].get("subtype") == "Current Source": + source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[11]) + self._get_source_name(source, message) + 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]) else: - message["payload"]["subtype"] = "Undefined: " + self._hexbyte(telegram[9]) + message["State_Update"]["subtype"] = "Undefined: " + self._hexbyte(telegram[9]) # goto source if message.get("payload_type") == "GOTO_SOURCE": - message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[11]) - message["payload"]["sourceID"] = telegram[11] - message["payload"]["source_type"] = self._get_type(const._ml_selectedsource_type_dict, telegram[11]) - message["payload"]["channel_track"] = telegram[12] + message["State_Update"]["nowPlaying"] = '' + message["State_Update"]["nowPlayingDetails"] = OrderedDict() + message["State_Update"]["nowPlayingDetails"]["source_type"] = \ + self._get_type(CONST.ml_selectedsource_type_dict, telegram[11]) + message["State_Update"]["nowPlayingDetails"]["channel_track"] = telegram[12] + source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[11]) + self._get_source_name(source, message) + message["State_Update"]["source"] = source + message["State_Update"]["sourceID"] = telegram[11] + self._get_channel_track(telegram, message) # remote request if message.get("payload_type") == "MLGW_REMOTE_BEO4": - message["payload"]["command"] = self._dictsanitize(const.beo4_commanddict, telegram[14]) - message["payload"]["destination"] = self._dictsanitize(const._destselectordict, telegram[11]) + message["State_Update"]["command"] = self._dictsanitize(CONST.beo4_commanddict, telegram[14]) + message["State_Update"]["destination"] = self._dictsanitize(CONST.destselectordict, telegram[11]) # request_key if message.get("payload_type") == "LOCK_MANAGER_COMMAND": - message["payload"]["subtype"] = self._dictsanitize( - const._ml_command_type_request_key_subtype_dict, telegram[9]) + message["State_Update"]["subtype"] = self._dictsanitize( + CONST.ml_command_type_request_key_subtype_dict, telegram[9]) # request distributed audio source if message.get("payload_type") == "REQUEST_DISTRIBUTED_SOURCE": - message["payload"]["subtype"] = self._dictsanitize(const._ml_activity_dict, telegram[9]) - if message["payload"].get('subtype') == "Source Active": - message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[13]) - message["payload"]["sourceID"] = telegram[13] - message["payload"]["source_type"] = self._get_type(const._ml_selectedsource_type_dict, telegram[13]) + message["State_Update"]["subtype"] = self._dictsanitize(CONST.ml_activity_dict, telegram[9]) + if message["State_Update"].get('subtype') == "Source Active": + source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[13]) + self._get_source_name(source, message) + message["State_Update"]["source"] = source + message["State_Update"]["sourceID"] = telegram[13] + message["State_Update"]["source_type"] = self._get_type(CONST.ml_selectedsource_type_dict, telegram[13]) # request local audio source if message.get("payload_type") == "REQUEST_LOCAL_SOURCE": - message["payload"]["subtype"] = self._dictsanitize(const._ml_activity_dict, telegram[9]) - if message["payload"].get('subtype') == "Source Active": - message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[11]) - message["payload"]["sourceID"] = telegram[11] - message["payload"]["source_type"] = self._get_type(const._ml_selectedsource_type_dict, telegram[11]) + message["State_Update"]["subtype"] = self._dictsanitize(CONST.ml_activity_dict, telegram[9]) + if message["State_Update"].get('subtype') == "Source Active": + source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[11]) + self._get_source_name(source, message) + 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]) - return message \ No newline at end of file + return message + + @staticmethod + def _get_channel_track(telegram, message): + # Check device list for channel name information + if CONST.devices: + for device in CONST.devices: + # Loop over devices to find source list for this specific device + if device['ML_ID'] == telegram[1]: + if 'channels' in device['Sources'][message["State_Update"]["source"]]: + for channel in device['Sources'][message["State_Update"]["source"]]['channels']: + if channel['number'] == message["State_Update"]["nowPlayingDetails"]["channel_track"]: + message["State_Update"]["nowPlaying"] = channel['name'] + break + # If device is a NetLink device and has no ML_ID, seek a generic solution from the first device that has + # this source available: This could give an incorrect response if a link room device has a different + # favorites list to the Audio Master! + for device in CONST.devices: + if message["State_Update"]["source"] in device['Sources']: + if 'channels' in device['Sources'][message["State_Update"]["source"]]: + for channel in device['Sources'][message["State_Update"]["source"]]['channels']: + if channel['number'] == message["State_Update"]["nowPlayingDetails"]["channel_track"]: + message["State_Update"]["nowPlaying"] = channel['name'] + break + + @staticmethod + def _get_source_name(source, message): + if CONST.available_sources: + for src in CONST.available_sources: + if src[1] == source: + message["State_Update"]["sourceName"] = src[0] + break diff --git a/Resources/MLCONFIG.py b/Resources/MLCONFIG.py index dca25c1..dbab6dd 100644 --- a/Resources/MLCONFIG.py +++ b/Resources/MLCONFIG.py @@ -4,34 +4,35 @@ import asyncore import json from collections import OrderedDict -import Resources.CONSTANTS as const +import Resources.CONSTANTS as CONST + class MLConfig: def __init__(self, host_address='blgw.local', user='admin', pwd='admin'): - self.log = logging.getLogger('Config:') + self.log = logging.getLogger('Config') self.log.setLevel('INFO') self._host = host_address self._user = user self._pwd = pwd - self._downloadData() + self._download_data() - def _downloadData(self): + def _download_data(self): self.log.info('Downloading configuration data from Gateway...') url = 'http://' + self._host + '/mlgwpservices.json' auth = (self._user, self._pwd) response = requests.get(url, auth=auth) configurationdata = json.loads(response.text) - self.configureMLGW((configurationdata)) + self.configure_mlgw(configurationdata) - def configureMLGW(self, data): + def configure_mlgw(self, data): self.log.info('Processing Gateway configuration data...\n') - const.gateway['Serial_Number'] = data['sn'] - const.gateway['Project'] = data['project'] - const.gateway['Installer'] = str(data['installer']['name']) - const.gateway['Contact'] = str(data['installer']['contact']) + CONST.gateway['Serial_Number'] = data['sn'] + CONST.gateway['Project'] = data['project'] + CONST.gateway['Installer'] = str(data['installer']['name']) + CONST.gateway['Contact'] = str(data['installer']['contact']) for zone in data["zones"]: if int(zone['number']) == 240: @@ -58,72 +59,73 @@ class MLConfig: device['Sources'][str(source["name"])] = OrderedDict() for selectCmd in source["selectCmds"]: source_id = str(source['sourceId'].split(':')[0]) - source_id = self._srcdictsanitize(const._blgw_srcdict, source_id).upper() + 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']) source_tuple = (str(source["name"]), source_id) - cmd_tuple = (source_id,(int(selectCmd["cmd"]),int(selectCmd["unit"]))) + cmd_tuple = (source_id, (int(selectCmd["cmd"]), int(selectCmd["unit"]))) device['Sources'][str(source["name"])]['BR1_cmd'] = cmd_tuple - if source.has_key('channels'): - device['Sources'][str(source["name"])][ - 'channel_track'] = OrderedDict() + if 'channels' in source: + device['Sources'][str(source["name"])]['channels'] = [] for channel in source['channels']: + c = OrderedDict() c_num = '' num = channel['selectSEQ'][::2] for n in num: c_num += str(n) - device['Sources'][str(source["name"])]['channel_track'][c_num] = OrderedDict() - device['Sources'][str(source["name"])]['channel_track'][c_num]['name'] = channel['name'] - device['Sources'][str(source["name"])]['channel_track'][c_num]['icon'] = channel['icon'] + c['number'] = int(c_num) + c['name'] = channel['name'] + c['icon'] = channel['icon'] + device['Sources'][str(source["name"])]['channels'].append(c) - if source_tuple not in const.available_sources: - const.available_sources.append(source_tuple) + if source_tuple not in CONST.available_sources: + CONST.available_sources.append(source_tuple) - const.devices.append(device) - const.rooms.append(room) + CONST.devices.append(device) + CONST.rooms.append(room) - self.log.info('Found ' + str(len(const.devices)) + ' AV Renderers!') - for i in range(len(const.devices)): - self.log.info('\tMLN ' + str(const.devices[i].get('MLN')) + ': ' + str(const.devices[i].get('Device'))) - 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('Found ' + str(len(CONST.devices)) + ' AV Renderers!') + for i in range(len(CONST.devices)): + self.log.info('\tMLN ' + str(CONST.devices[i].get('MLN')) + ': ' + str(CONST.devices[i].get('Device'))) + 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.debug(json.dumps(const.gateway, indent=4)) - self.log.debug(json.dumps(const.rooms, indent=4)) - self.log.debug(json.dumps(const.devices, indent=4)) + self.log.debug(json.dumps(CONST.gateway, indent=4)) + self.log.debug(json.dumps(CONST.rooms, indent=4)) + self.log.debug(json.dumps(CONST.devices, indent=4)) def get_masterlink_id(self, mlgw, mlcli): self.log.info("Finding MasterLink ID of products:") if mlgw.is_connected and mlcli.is_connected: - for device in const.devices: + for device in CONST.devices: self.log.info("Finding MasterLink ID of product " + device.get('Device')) # Ping the device with a light timeout to elicit a ML telegram containing its ML_ID mlgw.send_beo4_cmd(int(device.get('MLN')), - const.CMDS_DEST.get("AUDIO SOURCE"), - const.BEO4_CMDS.get("LIGHT TIMEOUT")) + CONST.CMDS_DEST.get("AUDIO SOURCE"), + CONST.BEO4_CMDS.get("LIGHT TIMEOUT")) if device.get('Serial_num') is None: # If this is a MasterLink product it has no serial number... # loop to until expected response received from ML Command Line Interface - while not mlcli.last_message.has_key('to_device') and mlcli.last_message[ - 'from_device'] == "MLGW" and mlcli.last_message[ - 'payload_type'] == "MLGW_REMOTE_BEO4" and mlcli.last_message[ - 'payload']['command'] == "LIGHT TIMEOUT": + while 'to_device' not in mlcli.last_message and mlcli.last_message['from_device'] == \ + "MLGW" and mlcli.last_message['payload_type'] == \ + "MLGW_REMOTE_BEO4" and mlcli.last_message['payload']['command'] == "LIGHT TIMEOUT": asyncore.loop(count=1, timeout=0.2) device['ML_ID'] = mlcli.last_message.get('to_device') - self.log.info("\tMasterLink ID of product " + - device.get('Device') + " is " + device.get('ML_ID') + ".\n") + self.log.info("\tMasterLink ID of product " + + device.get('Device') + " is " + device.get('ML_ID') + ".\n") 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") + self.log.info("\tNetworkLink ID of product " + device.get('Device') + " is " + + device.get('Serial_num') + ". No MasterLink ID assigned.\n") - def _srcdictsanitize(self, d, s): + @staticmethod + def _srcdictsanitize(d, s): result = d.get(s) - if result == None: + if result is None: result = s return str(result) diff --git a/Resources/MLGW_CLIENT.py b/Resources/MLGW_CLIENT.py index feede96..715af92 100644 --- a/Resources/MLGW_CLIENT.py +++ b/Resources/MLGW_CLIENT.py @@ -5,7 +5,8 @@ import time import json from collections import OrderedDict -import Resources.CONSTANTS as const +import Resources.CONSTANTS as CONST + class MLGWClient(asynchat.async_chat): """Client to interact with a B&O Gateway via the MasterLink Gateway Protocol @@ -19,7 +20,7 @@ class MLGWClient(asynchat.async_chat): self._host = host_address self._port = int(port) self._user = user - self._pwd = pwd + self._pwd = pwd self.name = name self.is_connected = False @@ -36,10 +37,11 @@ class MLGWClient(asynchat.async_chat): else: self.messageCallBack = None - #Expose dictionaries via API - self.BEO4_CMDS = const.BEO4_CMDS - self.CMDS_DEST = const.CMDS_DEST - self.MLGW_PL = const.MLGW_PL + # Expose dictionaries via API + self.BEO4_CMDS = CONST.BEO4_CMDS + self.BEORMT1_CMDS = CONST.beoremoteone_commanddict + self.CMDS_DEST = CONST.CMDS_DEST + self.MLGW_PL = CONST.MLGW_PL # ######################################################################################## # ##### Open Socket and connect to B&O Gateway @@ -52,10 +54,10 @@ class MLGWClient(asynchat.async_chat): self.log.debug(data) self._received_data = bytearray(data) - bit1 = int(self._received_data[0]) #Start of Header == 1 - bit2 = int(self._received_data[1]) #Message Type - bit3 = int(self._received_data[2]) #Payload length - bit4 = int(self._received_data[3]) #Spare Bit/End of Header == 0 + bit1 = int(self._received_data[0]) # Start of Header == 1 + bit2 = int(self._received_data[1]) # Message Type + bit3 = int(self._received_data[2]) # Payload length + bit4 = int(self._received_data[3]) # Spare Bit/End of Header == 0 payload = bytearray() for item in self._received_data[4:bit3 + 4]: @@ -77,8 +79,7 @@ class MLGWClient(asynchat.async_chat): def _decode(self, msg_type, header, payload): message = OrderedDict() - payload_type = self._dictsanitize(const._mlgw_payloadtypedict, msg_type) - message["Payload_type"] = payload_type + payload_type = self._dictsanitize(CONST.mlgw_payloadtypedict, msg_type) if payload_type == "MLGW virtual button event": virtual_btn = payload[0] @@ -87,6 +88,7 @@ class MLGWClient(asynchat.async_chat): else: virtual_action = self._getvirtualactionstr(payload[1]) + message["payload_type"] = payload_type message["button"] = virtual_btn message["action"] = virtual_action @@ -99,75 +101,89 @@ class MLGWClient(asynchat.async_chat): else: self.log.info("\tLogin successful to %s", self._host) self.is_connected = True + message["payload_type"] = payload_type message['Connected'] = "True" self.get_serial() elif payload_type == "Pong": - self.is_connected = True - message['CONNECTION'] = 'Online' + self.is_connected = True + message["payload_type"] = payload_type + message['CONNECTION'] = 'Online' elif payload_type == "Serial Number": sn = '' for c in payload: sn += chr(c) - message['Serial_Num'] = sn + message["payload_type"] = payload_type + message['serial_Num'] = sn elif payload_type == "Source Status": - if const.rooms and const.devices: - for device in const.devices: + if CONST.rooms and CONST.devices: + for device in CONST.devices: if device['MLN'] == payload[0]: name = device['Device'] - for room in const.rooms: + for room in CONST.rooms: if name in room['Products']: message["Zone"] = room['Zone'].upper() message["Room"] = room['Room_Name'].upper() message["Type"] = 'AV RENDERER' message["Device"] = name - + message["payload_type"] = payload_type message["MLN"] = payload[0] - message["Source"] = self._getselectedsourcestr(payload[1]).upper() - message["Source_medium_position"] = self._hexword(payload[2], payload[3]) - message["Source_position"] = self._hexword(payload[4], payload[5]) - message["Picture_format"] = self._getdictstr(const.ml_pictureformatdict, payload[7]) - message["State"] = self._getdictstr(const._sourceactivitydict, payload[6]) + message["State_Update"] = OrderedDict() + message["State_Update"]["nowPlaying"] = '' + message["State_Update"]["nowPlayingDetails"] = OrderedDict() + message["State_Update"]["nowPlayingDetails"]["channel_track"] = \ + self._hexword(payload[4], payload[5]) + message["State_Update"]["nowPlayingDetails"]["source_medium_position"] = \ + self._hexword(payload[2], payload[3]) + message["State_Update"]["nowPlayingDetails"]["picture_format"] = \ + self._getdictstr(CONST.ml_pictureformatdict, payload[7]) + source = self._getselectedsourcestr(payload[1]).upper() + self._get_source_name(source, message) + message["State_Update"]["source"] = source + self._get_channel_track(message) + message["State_Update"]["state"] = self._getdictstr(CONST.sourceactivitydict, payload[6]) elif payload_type == "Picture and Sound Status": - if const.rooms and const.devices: - for device in const.devices: + if CONST.rooms and CONST.devices: + for device in CONST.devices: if device['MLN'] == payload[0]: name = device['Device'] - for room in const.rooms: + for room in CONST.rooms: if name in room['Products']: message["Zone"] = room['Zone'].upper() message["Room"] = room['Room_Name'].upper() message["Type"] = 'AV RENDERER' message["Device"] = name - + message["payload_type"] = payload_type message["MLN"] = payload[0] - message["Sound_status"] = self._getdictstr(const.mlgw_soundstatusdict, payload[1]) - message["Speaker_mode"] = self._getdictstr(const._mlgw_speakermodedict, payload[2]) - message["Stereo_mode"] = self._getdictstr(const._mlgw_stereoindicatordict, payload[9]) - message["Volume"] = int(payload[3]) - message["Screen1_mute"] = self._getdictstr(const._mlgw_screenmutedict, payload[4]) - message["Screen1_active"] = self._getdictstr(const._mlgw_screenactivedict, payload[5]) - message["Screen2_mute"] = self._getdictstr(const._mlgw_screenmutedict, payload[6]) - message["Screen2_active"] = self._getdictstr(const._mlgw_screenactivedict, payload[7]) - message["Cinema_mode"] = self._getdictstr(const._mlgw_cinemamodedict, payload[8]) - + message["State_Update"] = OrderedDict() + message["State_Update"]["sound_status"] = self._getdictstr(CONST.mlgw_soundstatusdict, payload[1]) + message["State_Update"]["speakermode"] = self._getdictstr(CONST.mlgw_speakermodedict, payload[2]) + message["State_Update"]["stereo_mode"] = self._getdictstr(CONST.mlgw_stereoindicatordict, payload[9]) + message["State_Update"]["screen1_mute"] = self._getdictstr(CONST.mlgw_screenmutedict, payload[4]) + message["State_Update"]["screen1_active"] = self._getdictstr(CONST.mlgw_screenactivedict, payload[5]) + message["State_Update"]["screen2_mute"] = self._getdictstr(CONST.mlgw_screenmutedict, payload[6]) + message["State_Update"]["screen2_active"] = self._getdictstr(CONST.mlgw_screenactivedict, payload[7]) + message["State_Update"]["cinema_mode"] = self._getdictstr(CONST.mlgw_cinemamodedict, payload[8]) + message["volume"] = int(payload[3]) elif payload_type == "All standby notification": - message["Command"] = "All Standby" + message["payload_type"] = payload_type + message["command"] = "All Standby" elif payload_type == "Light and Control command": - if const.rooms: - for room in const.rooms: + if CONST.rooms: + for room in CONST.rooms: if room['Room_Number'] == payload[0]: message["Zone"] = room['Zone'].upper() message["Room"] = room['Room_Name'].upper() - message["Type"] = self._getdictstr(const._mlgw_lctypedict, payload[1]).upper() + " COMMAND" + message["Type"] = self._getdictstr(CONST.mlgw_lctypedict, payload[1]).upper() + " COMMAND" message["Device"] = 'Beo4/BeoRemote One' - message["Room number"] = str(payload[0]) - message["Command"] = self._getbeo4commandstr(payload[2]) + message["payload_type"] = payload_type + message["room_number"] = str(payload[0]) + message["command"] = self._getbeo4commandstr(payload[2]) if message != '': self._report(header, payload, message) @@ -177,7 +193,6 @@ class MLGWClient(asynchat.async_chat): self.set_terminator(b'\r\n') # Create the socket try: - socket.setdefaulttimeout(3) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) except socket.error, e: self.log.info("Error creating socket: %s" % e) @@ -188,18 +203,18 @@ class MLGWClient(asynchat.async_chat): except socket.gaierror, e: self.log.info("\tError with address %s:%i - %s" % (self._host, self._port, e)) self.handle_close() - except socket.error, e: - self.log.info("\tError opening connection to %s:%i - %s" % (self._host, self._port, e)) - self.handle_close() except socket.timeout, e: self.log.info("\tSocket connection to %s:%i timed out- %s" % (self._host, self._port, e)) self.handle_close() + except socket.error, e: + self.log.info("\tError opening connection to %s:%i - %s" % (self._host, self._port, e)) + self.handle_close() else: self.is_connected = True self.log.info("\tConnected to B&O Gateway") def handle_connect(self): - login=[] + login = [] for c in self._user: login.append(c) login.append(0x00) @@ -207,7 +222,7 @@ class MLGWClient(asynchat.async_chat): login.append(c) self.log.info("\tAttempting to Authenticate...") - self._send_cmd(const.MLGW_PL.get("LOGIN REQUEST"), login) + self._send_cmd(CONST.MLGW_PL.get("LOGIN REQUEST"), login) def handle_close(self): self.log.info(self.name + ": Closing socket") @@ -223,22 +238,22 @@ class MLGWClient(asynchat.async_chat): # ######################################################################################## # ##### mlgw send_cmder functions - ## send_cmd command to mlgw + # send_cmd command to mlgw def _send_cmd(self, msg_type, payload): - #construct header - telegram = [1,msg_type,len(payload),0] - #append payload + # Construct header + telegram = [1, msg_type, len(payload), 0] + # append payload for p in payload: telegram.append(p) try: self.push(str(bytearray(telegram))) - except socket.error, e: - self.log.info("Error sending data: %s" % e) - self.handle_close() except socket.timeout, e: self.log.info("\tSocket connection to %s:%i timed out- %s" % (self._host, self._port, e)) self.handle_close() + except socket.error, e: + self.log.info("Error sending data: %s" % e) + self.handle_close() else: self.last_sent = str(bytearray(telegram)) self.last_sent_at = time.time() @@ -253,53 +268,52 @@ class MLGWClient(asynchat.async_chat): time.sleep(0.2) def ping(self): - self._send_cmd(const.MLGW_PL.get("PING"), "") + self._send_cmd(CONST.MLGW_PL.get("PING"), "") - ## Get serial number of mlgw + # Get serial number of mlgw def get_serial(self): if self.is_connected: # Request serial number - self._send_cmd(const.MLGW_PL.get("REQUEST SERIAL NUMBER"), "") + self._send_cmd(CONST.MLGW_PL.get("REQUEST SERIAL NUMBER"), "") - ## send_cmd Beo4 command to mlgw + # send_cmd Beo4 command to mlgw def send_beo4_cmd(self, mln, dest, cmd, sec_source=0x00, link=0x00): - payload = [] - payload.append(mln) # byte[0] MLN - payload.append(dest) # byte[1] Dest-Sel (0x00, 0x01, 0x05, 0x0f) - payload.append(cmd) # byte[2] Beo4 Command - payload.append(sec_source) # byte[3] Sec-Source - payload.append(link) # byte[4] Link - self._send_cmd(const.MLGW_PL.get("BEO4 COMMAND"), payload) + payload = [ + mln, # byte[0] MLN + dest, # byte[1] Dest-Sel (0x00, 0x01, 0x05, 0x0f) + cmd, # byte[2] Beo4 Command + sec_source, # byte[3] Sec-Source + link] # byte[4] Link + self._send_cmd(CONST.MLGW_PL.get("BEO4 COMMAND"), payload) - ## send_cmd BeoRemote One command to mlgw + # send_cmd BeoRemote One command to mlgw def send_beoremoteone_cmd(self, mln, cmd, network_bit=0x00): - payload = [] - payload.append(mln) # byte[0] MLN - payload.append(cmd) # byte[1] Beo4 Command - payload.append(0x00) # byte[2] AV (needs to be 0) - payload.append(network_bit) # byte[3] Network_bit (0 = local source, 1 = network source) - self._send_cmd(const.MLGW_PL.get("BEOREMOTE ONE CONTROL COMMAND"), payload) + payload = [ + mln, # byte[0] MLN + cmd, # byte[1] Beo4 Command + 0x00, # byte[2] AV (needs to be 0) + network_bit] # byte[3] Network_bit (0 = local source, 1 = network source) + self._send_cmd(CONST.MLGW_PL.get("BEOREMOTE ONE CONTROL COMMAND"), payload) - ## send_cmd BeoRemote One Source Select to mlgw + # send_cmd BeoRemote One Source Select to mlgw def send_beoremoteone_select_source(self, mln, cmd, unit, network_bit=0x00): - payload = [] - payload.append(mln) # byte[0] MLN - payload.append(cmd) # byte[1] Beormyone Command - payload.append(unit) # byte[2] Unit - payload.append(0x00) # byte[3] AV (needs to be 0) - payload.append(network_bit) # byte[4] Network_bit (0 = local source, 1 = network source) - self._send_cmd(const.MLGW_PL.get("BEOREMOTE ONE SOURCE SELECTION"), payload) + payload = [ + mln, # byte[0] MLN + cmd, # byte[1] Beoremote One Command self.BEORMT1_CMD.get('cmd')[0] + unit, # byte[2] Unit self.BEORMT1_CMD.get('cmd')[1] + 0x00, # byte[3] AV (needs to be 0) + network_bit] # byte[4] Network_bit (0 = local source, 1 = network source) + self._send_cmd(CONST.MLGW_PL.get("BEOREMOTE ONE SOURCE SELECTION"), payload) - ## send_cmd Beo4 commmand and store the source name + # send_cmd Beo4 commmand and store the source name def send_beo4_select_source(self, mln, dest, source, sec_source=0x00, link=0x00): - beolink_source = self._dictsanitize(const.beo4_commanddict, source).upper() self.send_beo4_cmd(mln, dest, source, sec_source, link) # ######################################################################################## # ##### Utility functions - - def _hexbyte(self, byte): + @staticmethod + def _hexbyte(byte): resultstr = hex(byte) if byte < 16: resultstr = resultstr[:2] + "0" + resultstr[2] @@ -312,51 +326,66 @@ class MLGWClient(asynchat.async_chat): def _dictsanitize(self, d, s): result = d.get(s) - if result == None: + if result is None: result = "UNKNOWN (type=" + self._hexbyte(s) + ")" return str(result) # ######################################################################################## # ##### Decode MLGW Protocol packet to readable string - ## Get message string for mlgw packet's payload type - # + # Get message string for mlgw packet's payload type def _getpayloadtypestr(self, payloadtype): - result = const._mlgw_payloadtypedict.get(payloadtype) - if result == None: + result = CONST.mlgw_payloadtypedict.get(payloadtype) + if result is None: result = "UNKNOWN (type=" + self._hexbyte(payloadtype) + ")" return str(result) - def _getmlnstr(self, mln): - result = "MLN=" + str(mln) - return result - def _getbeo4commandstr(self, command): - result = const.beo4_commanddict.get(command) - if result == None: + result = CONST.beo4_commanddict.get(command) + if result is None: result = "Cmd=" + self._hexbyte(command) return result def _getvirtualactionstr(self, action): - result = const._mlgw_virtualactiondict.get(action) - if result == None: + result = CONST.mlgw_virtualactiondict.get(action) + if result is None: result = "Action=" + self._hexbyte(action) return result def _getselectedsourcestr(self, source): - result = const.ml_selectedsourcedict.get(source) - if result == None: + result = CONST.ml_selectedsourcedict.get(source) + if result is None: result = "Src=" + self._hexbyte(source) return result def _getspeakermodestr(self, source): - result = const._mlgw_speakermodedict.get(source) - if result == None: + result = CONST.mlgw_speakermodedict.get(source) + if result is None: result = "mode=" + self._hexbyte(source) return result def _getdictstr(self, mydict, mykey): result = mydict.get(mykey) - if result == None: + if result is None: result = self._hexbyte(mykey) - return result \ No newline at end of file + return result + + @staticmethod + def _get_source_name(source, message): + if CONST.available_sources: + for src in CONST.available_sources: + if src[1] == source: + message["State_Update"]["sourceName"] = src[0] + break + + @staticmethod + def _get_channel_track(message): + # Check device list for channel name information + if CONST.devices: + for device in CONST.devices: + if device['Device'] == message['Device']: + if 'channels' in device['Sources'][message["State_Update"]["source"]]: + for channel in device['Sources'][message["State_Update"]["source"]]['channels']: + if channel['number'] == int(message["State_Update"]['nowPlayingDetails']["channel_track"]): + message["State_Update"]["nowPlaying"] = channel['name'] + break diff --git a/Resources/MLtn_CLIENT.py b/Resources/MLtn_CLIENT.py index 8972a60..8cbeeba 100644 --- a/Resources/MLtn_CLIENT.py +++ b/Resources/MLtn_CLIENT.py @@ -5,7 +5,8 @@ import time import json from collections import OrderedDict -import Resources.CONSTANTS as const +import Resources.CONSTANTS as CONST + class MLtnClient(asynchat.async_chat): """Client to monitor network activity on a Masterlink Gateway via the telnet monitor""" @@ -74,10 +75,9 @@ class MLtnClient(asynchat.async_chat): self.last_received_at = 0 def _decode(self, items): - time_stamp = ''.join([items[1],' at ',items[0]]) header = items[3][:-1] - telegramStarts = len(''.join(items[:4])) + 4 - telegram = self._received_data[telegramStarts:].replace('!','').split('/') + telegram_starts = len(''.join(items[:4])) + 4 + telegram = self._received_data[telegram_starts:].replace('!', '').split('/') message = OrderedDict() if header == 'integration_protocol': @@ -125,22 +125,23 @@ class MLtnClient(asynchat.async_chat): message['Payload']['to_MLN'] = int(s[k], base=16) if k == 11: message['Payload']['Destination'] = self._dictsanitize( - const._mlgw_payloaddestdict, int(s[k], base=16)) + CONST.destselectordict, int(s[k], base=16)) if k == 12: message['Payload']['Command'] = self._dictsanitize( - const.beo4_commanddict, int(s[k], base=16)).upper() + CONST.beo4_commanddict, int(s[k], base=16)).upper() if k == 13: message['Payload']['Sec-Source'] = self._dictsanitize( - const._mlgw_secsourcedict, int(s[k], base=16)) + CONST.mlgw_secsourcedict, int(s[k], base=16)) if k == 14: message['Payload']['Link'] = self._dictsanitize( - const._mlgw_linkdict, int(s[k], base=16)) + CONST.mlgw_linkdict, int(s[k], base=16)) if k > 14: message['Payload']['cmd' + str(k - 9)] = self._dictsanitize( - const.beo4_commanddict, int(s[k], base=16)) + CONST.beo4_commanddict, int(s[k], base=16)) return message - def _decode_action(self, telegram, message): + @staticmethod + def _decode_action(telegram, message): message['Zone'] = telegram[0].upper() message['Room'] = telegram[1].upper() message['Type'] = telegram[2].upper() @@ -167,7 +168,8 @@ class MLtnClient(asynchat.async_chat): message['State_Update']['STATE'] = telegram[4] return message - def _decode_command(self, telegram, message): + @staticmethod + def _decode_command(telegram, message): message['Zone'] = telegram[0].upper() message['Room'] = telegram[1].upper() message['Type'] = telegram[2].upper() @@ -228,7 +230,7 @@ class MLtnClient(asynchat.async_chat): for s in state: message['State_Update'][s.split('=')[0].lower()] = s.split('=')[1].title() if message['State_Update'].get('command') == ' Cmd': - message['State_Update']['command'] = self._dictsanitize(const.beo4_commanddict, + message['State_Update']['command'] = self._dictsanitize(CONST.beo4_commanddict, int(s[13:].strip())).title() elif telegram[4][:7] == 'Control': state = telegram[4][6:].split('&') @@ -236,7 +238,7 @@ class MLtnClient(asynchat.async_chat): for s in state: message['State_Update'][s.split('=')[0].lower()] = s.split('=')[1] if message['State_Update'].get('command') == ' cmd': - message['State_Update']['command'] = self._dictsanitize(const.beo4_commanddict, + message['State_Update']['command'] = self._dictsanitize(CONST.beo4_commanddict, int(s[13:].strip())).title() elif telegram[4] == 'All standby': message['State_Update']['command'] = telegram[4] @@ -246,7 +248,7 @@ class MLtnClient(asynchat.async_chat): for s in state: if s.split('=')[0] == 'sourceUniqueId': src = s.split('=')[1].split(':')[0].upper() - message['State_Update']['source'] = self._srcdictsanitize(const._blgw_srcdict, src) + message['State_Update']['source'] = self._srcdictsanitize(CONST.blgw_srcdict, src) message['State_Update'][s.split('=')[0]] = s.split('=')[1] elif s.split('=')[0] == 'nowPlayingDetails': message['State_Update']['nowPlayingDetails'] = OrderedDict() @@ -274,7 +276,6 @@ class MLtnClient(asynchat.async_chat): self.set_terminator(b'\r\n') # Create the socket try: - socket.setdefaulttimeout(3) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) except socket.error, e: self.log.info("Error creating socket: %s" % e) @@ -285,12 +286,12 @@ class MLtnClient(asynchat.async_chat): except socket.gaierror, e: self.log.info("\tError with address %s:%i - %s" % (self._host, self._port, e)) self.handle_close() - except socket.error, e: - self.log.info("\tError opening connection to %s:%i - %s" % (self._host, self._port, e)) - self.handle_close() except socket.timeout, e: self.log.info("\tSocket connection to %s:%i timed out- %s" % (self._host, self._port, e)) self.handle_close() + except socket.error, e: + self.log.info("\tError opening connection to %s:%i - %s" % (self._host, self._port, e)) + self.handle_close() else: self.is_connected = True self.log.info("\tConnected to B&O Gateway") @@ -305,28 +306,28 @@ class MLtnClient(asynchat.async_chat): self.is_connected = False self.close() - def _send_cmd(self,telegram): + def _send_cmd(self, telegram): try: - self.push(telegram +"\r\n") - except socket.error, e: - self.log.info("Error sending data: %s" % e) - self.handle_close() + self.push(telegram + "\r\n") except socket.timeout, e: self.log.info("\tSocket connection to %s:%i timed out- %s" % (self._host, self._port, e)) self.handle_close() + except socket.error, e: + self.log.info("Error sending data: %s" % e) + self.handle_close() else: self.last_sent = telegram self.last_sent_at = time.time() self.log.info(self.name + " >>-SENT--> : " + telegram) time.sleep(0.2) - def toggleEvents(self): + def toggle_events(self): self._send_cmd('e') - def toggleMacros(self): + def toggle_macros(self): self._send_cmd('m') - def toggleCommands(self): + def toggle_commands(self): self._send_cmd('c') def ping(self): @@ -334,7 +335,8 @@ class MLtnClient(asynchat.async_chat): # ######################################################################################## # ##### Utility functions - def _hexbyte(self, byte): + @staticmethod + def _hexbyte(byte): resultstr = hex(byte) if byte < 16: resultstr = resultstr[:2] + "0" + resultstr[2] @@ -342,12 +344,13 @@ class MLtnClient(asynchat.async_chat): def _dictsanitize(self, d, s): result = d.get(s) - if result == None: + if result is None: result = "UNKNOWN (type=" + self._hexbyte(s) + ")" return str(result) - def _srcdictsanitize(self, d, s): + @staticmethod + def _srcdictsanitize(d, s): result = d.get(s) - if result == None: + if result is None: result = s - return str(result) \ No newline at end of file + return str(result) diff --git a/Resources/__init__.py b/Resources/__init__.py new file mode 100644 index 0000000..84952a8 --- /dev/null +++ b/Resources/__init__.py @@ -0,0 +1 @@ +# init \ No newline at end of file