diff --git a/Resources/ASBridge.py b/Resources/ASBridge.py new file mode 100644 index 0000000..e1337fd --- /dev/null +++ b/Resources/ASBridge.py @@ -0,0 +1,111 @@ +#!/usr/bin/python +import os +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 + reporting messages in the notification centre''' + +PLAYSTATE = dict([ + (1800426320, "Play"), + (1800426352, "Pause"), + (1800426323, "Stop"), + (1800426310, "Wind"), + (1800426322, "Rewind") + ]) + + +class MusicController(object): + + def __init__(self): + self.app = SBApplication.applicationWithBundleIdentifier_("com.apple.Music") + + # 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()) + return [name, album, artist] + + def get_current_play_state(self): + return PLAYSTATE.get(self.app.playerState()) + + def get_current_track_position(self): + return self.app.playerPosition() + + # ######################################################################################## + # Transport Controls + def playpause(self): + self.app.playpause() + + def play(self): + if PLAYSTATE.get(self.app.playerState()) in ['Wind', 'Rewind']: + self.app.resume() + elif PLAYSTATE.get(self.app.playerState()) == 'Pause': + self.app.playpause() + elif PLAYSTATE.get(self.app.playerState()) == 'Stop': + playlist = self.app.sources().objectWithName_("Library") + playlist.playOnce_(None) + + def pause(self): + if PLAYSTATE.get(self.app.playerState()) == 'Play': + self.app.pause() + + def stop(self): + if PLAYSTATE.get(self.app.playerState()) != 'Stop': + self.app.stop() + + def next_track(self): + self.app.nextTrack() + + def previous_track(self): + self.app.previousTrack() + + def wind(self, time): + # Native wind function can be a bit annoying + # I provide an alternative below that skips a set number of seconds forwards + # self.app.wind() + self.set_current_track_position(time) + + def rewind(self, time): + # Native rewind function can be a bit annoying + # I provide an alternative below that skips a set number of seconds back + # self.app.rewind() + self.set_current_track_position(time) + + # ######################################################################################## + # More complex playback control functions + def set_current_track_position(self, time, mode='Relative'): + if mode == 'Relative': + # Set playback position in seconds relative to current position + self.app.setPlayerPosition_(self.app.playerPosition() + time) + elif mode == 'Absolute': + # Set playback position in seconds from the start of the track + self.app.setPlayerPosition_(time) + + def play_playlist(self, playlist): + self.app.stop() + playlist = self.app.sources().objectWithName_("Library").playlists().objectWithName_(playlist) + playlist.playOnce_(None) + + # ######################################################################################## + # Accessory functions + @staticmethod + def run_script(script): + # Run an external applescript file + script = 'run script ("' + script + '" as POSIX file)' + s = NSAppleScript.alloc().initWithSource_(script) + s.executeAndReturnError_(None) + + @staticmethod + def notify(message): + # Post message in notification center + path = os.path.dirname(os.path.abspath(__file__)) + script = 'tell application "' + path + '/Notify.app" to notify("BeoGateway", "' + \ + message + '")' + s = NSAppleScript.alloc().initWithSource_(script) + s.executeAndReturnError_(None) diff --git a/Resources/BLHIP_CLIENT.py b/Resources/BLHIP_CLIENT.py index e2e670e..7749f5e 100644 --- a/Resources/BLHIP_CLIENT.py +++ b/Resources/BLHIP_CLIENT.py @@ -173,7 +173,7 @@ class BLHIPClient(asynchat.async_chat): self.log.info("\tAttempting to Authenticate...") self.send_cmd(self._user) self.send_cmd(self._pwd) - self.statefiler() + self.statefilter() def handle_close(self): self.log.info(self.name + ": Closing socket") @@ -219,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 statefilter(self, zone='*', room='*', dev_type='*', device='*'): s_filter = "f " + zone + "/" + room + "/" + dev_type + '/' + device self.send_cmd(s_filter) @@ -241,12 +241,16 @@ class BLHIPClient(asynchat.async_chat): @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 + try: + # 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 + except KeyError: + pass diff --git a/Resources/CONSTANTS.py b/Resources/CONSTANTS.py index 48c601b..4d5fd51 100644 --- a/Resources/CONSTANTS.py +++ b/Resources/CONSTANTS.py @@ -1,11 +1,31 @@ # Constants for B&O telegram protocols # ######################################################################################## # Config data (set on initialisation) -gateway = dict() +gateway = dict([("Current Audio Source", ''), ("Current Video Source", '')]) rooms = [] devices = [] available_sources = [] +# ######################################################################################## +# Source Types + +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", + "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", + "MATRIX_12", "MATRIX_13", "MATRIX_14", "MATRIX_15", "MATRIX_16", "PERSONAL_1", + "PERSONAL_2", "PERSONAL_3", "PERSONAL_4", "PERSONAL_5", "PERSONAL_6", "PERSONAL_7", + "PERSONAL_8")), + ("Audio Sources", ("RADIO", "A.AUX", "A.TAPE/A.MEM", "CD", "PHONO/N.RADIO", "A.TAPE2/N.MUSIC", + "SERVER", "SPOTIFY", "CD2/JOIN", "TUNEIN", "DVB_RADIO", "LINE.IN", "BLUETOOTH", + "MUSIC", "AIRPLAY", "SPOTIFY", "DEEZER", "QPLAY")) + ] +) + # ######################################################################################## # Beo4 Commands beo4_commanddict = dict( @@ -18,7 +38,7 @@ beo4_commanddict = dict( (0x82, "V.Aux/DTV2"), (0x83, "A.Aux"), (0x84, "Media"), - (0x85, "V.Tape/V.Mem/DVD2"), + (0x85, "V.Tape/V.Mem"), (0x86, "DVD"), (0x87, "Camera"), (0x88, "Text"), @@ -350,9 +370,9 @@ ml_selectedsourcedict = dict( (0x0B, "TV"), (0x15, "V.TAPE/V.MEM"), (0x16, "DVD2"), - (0x1F, "DTV"), + (0x1F, "SAT/DTV"), (0x29, "DVD"), - (0x33, "V.AUX"), + (0x33, "V.AUX/DTV2"), (0x3E, "DOORCAM"), (0x47, "PC"), (0x6F, "RADIO"), @@ -369,6 +389,8 @@ ml_selectedsourcedict = dict( ml_trackinfo_subtype_dict = dict([(0x05, "Current Source"), (0x07, "Change Source"), ]) +ml_sourcekind_dict = dict([(0x01, "audio source"), (0x02, "video source"), (0xFF, "undefined")]) + ml_selectedsource_type_dict = dict( [ ("VIDEO", (0x0B, 0x1F)), diff --git a/Resources/MLCLI_CLIENT.py b/Resources/MLCLI_CLIENT.py index 9939eed..188cb30 100644 --- a/Resources/MLCLI_CLIENT.py +++ b/Resources/MLCLI_CLIENT.py @@ -84,7 +84,7 @@ class MLCLIClient(asynchat.async_chat): for item in items: try: telegram.append(int(item[:-1], base=16)) - except TypeError: + except (ValueError, TypeError): # abort if invalid character found self.log.debug('Invalid character ' + str(item) + ' found in telegram: ' + ''.join(items) + '\nAborting!') @@ -192,37 +192,35 @@ class MLCLIClient(asynchat.async_chat): def _decode(self, telegram): # Decode header message = OrderedDict() - 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"] - break - message["from_device"] = self._dictsanitize(CONST.ml_device_dict, telegram[1]) + self._get_device_info(message, telegram) + message["from_device"] = self._get_device_name(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_device"] = self._get_device_name(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["State_Update"] = OrderedDict() + # RELEASE command signifies product standby + if message.get("payload_type") in ["RELEASE", "STANDBY"]: + message["State_Update"]["state"] = 'Standby' + # source status info # TTFF__TYDSOS__PTLLPS SR____LS______SLSHTR__ACSTPI________________________TRTR______ if message.get("payload_type") == "STATUS_INFO": - message["State_Update"]["nowPlaying"] = '' + message["State_Update"]["nowPlaying"] = 'Unknown' message["State_Update"]["nowPlayingDetails"] = OrderedDict() message["State_Update"]["nowPlayingDetails"]["local_source"] = telegram[13] - message["State_Update"]["nowPlayingDetails"]["type"] = telegram[22] + message["State_Update"]["nowPlayingDetails"]["type"] = \ + self._dictsanitize(CONST.ml_sourcekind_dict, 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"] = \ + message["State_Update"]["nowPlayingDetails"]["picture_format"] = \ self._dictsanitize(CONST.ml_pictureformatdict, telegram[23]) source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[10]) self._get_source_name(source, message) @@ -257,7 +255,7 @@ class MLCLIClient(asynchat.async_chat): # audio track info long if message.get("payload_type") == "TRACK_INFO_LONG": - message["State_Update"]["nowPlaying"] = '' + message["State_Update"]["nowPlaying"] = 'Unknown' message["State_Update"]["nowPlayingDetails"] = OrderedDict() message["State_Update"]["nowPlayingDetails"]["type"] = \ self._get_type(CONST.ml_selectedsource_type_dict, telegram[11]) @@ -271,7 +269,7 @@ class MLCLIClient(asynchat.async_chat): # video track info if message.get("payload_type") == "VIDEO_TRACK_INFO": - message["State_Update"]["nowPlaying"] = '' + message["State_Update"]["nowPlaying"] = 'Unknown' message["State_Update"]["nowPlayingDetails"] = OrderedDict() message["State_Update"]["nowPlayingDetails"]["source_type"] = \ self._get_type(CONST.ml_selectedsource_type_dict, telegram[13]) @@ -286,6 +284,8 @@ class MLCLIClient(asynchat.async_chat): # track change info if message.get("payload_type") == "TRACK_INFO": message["State_Update"]["subtype"] = self._dictsanitize(CONST.ml_trackinfo_subtype_dict, telegram[9]) + + # Change source 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] @@ -297,6 +297,7 @@ class MLCLIClient(asynchat.async_chat): message["State_Update"]["source"] = source message["State_Update"]["sourceID"] = telegram[22] + # Current Source if message["State_Update"].get("subtype") == "Current Source": source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[11]) self._get_source_name(source, message) @@ -308,7 +309,7 @@ class MLCLIClient(asynchat.async_chat): # goto source if message.get("payload_type") == "GOTO_SOURCE": - message["State_Update"]["nowPlaying"] = '' + message["State_Update"]["nowPlaying"] = 'Unknown' message["State_Update"]["nowPlayingDetails"] = OrderedDict() message["State_Update"]["nowPlayingDetails"]["source_type"] = \ self._get_type(CONST.ml_selectedsource_type_dict, telegram[11]) @@ -374,6 +375,27 @@ class MLCLIClient(asynchat.async_chat): message["State_Update"]["nowPlaying"] = channel['name'] return + def _get_device_info(self, message, telegram): + if CONST.devices: + for device in CONST.devices: + if device['ML_ID'] == self._dictsanitize(CONST.ml_device_dict, telegram[1]): + try: + message["Zone"] = device['Zone'].upper() + except KeyError: + pass + message["Room"] = device["Room"].upper() + message["Type"] = "AV RENDERER" + message["Device"] = device["Device"] + break + + @staticmethod + def _get_device_name(dev): + if CONST.devices: + for device in CONST.devices: + if device['ML_ID'] == dev: + return device['Device'] + return dev + @staticmethod def _get_source_name(source, message): if CONST.available_sources: diff --git a/Resources/MLCONFIG.py b/Resources/MLCONFIG.py index dbab6dd..5b5b654 100644 --- a/Resources/MLCONFIG.py +++ b/Resources/MLCONFIG.py @@ -1,7 +1,9 @@ import logging -import requests import asyncore import json +import time +import requests +from requests.auth import HTTPDigestAuth, HTTPBasicAuth from collections import OrderedDict import Resources.CONSTANTS as CONST @@ -20,48 +22,95 @@ class MLConfig: self._download_data() 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.configure_mlgw(configurationdata) + try: + self.log.info('Downloading configuration data from Gateway...') + url = 'http://' + self._host + '/mlgwpservices.json' + # try Basic Auth next (this is needed for the BLGW) + response = requests.get(url, auth=HTTPBasicAuth(self._user, self._pwd)) + + if response.status_code == 401: + # try Digest Auth first (this is needed for the MLGW) + response = requests.get(url, auth=HTTPDigestAuth(self._user, self._pwd)) + + if response.status_code == 401: + return + else: + # Once logged in successfully download and process the configuration data + configurationdata = json.loads(response.text) + self.log.debug(json.dumps(configurationdata, indent=4)) + self.configure_mlgw(configurationdata) + except ValueError: + pass 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']) + try: + CONST.gateway['Installer'] = str(data['installer']['name']) + CONST.gateway['Contact'] = str(data['installer']['contact']) + gateway_type = 'blgw' + except KeyError: + gateway_type = 'mlgw' for zone in data["zones"]: if int(zone['number']) == 240: continue room = OrderedDict() room['Room_Number'] = zone['number'] - room['Zone'] = str(zone['name']).split('/')[0] - room['Room_Name'] = str(zone['name']).split('/')[1] + if gateway_type == 'blgw': + # BLGW arranges rooms within zones + room['Zone'] = str(zone['name']).split('/')[0] + room['Room_Name'] = str(zone['name']).split('/')[1] + elif gateway_type == 'mlgw': + # MLGW has no zoning concept - devices are arranged in rooms only + room['Room_Name'] = str(zone['name']) + room['Products'] = [] for product in zone["products"]: device = OrderedDict() room['Products'].append(str(product["name"])) + # Device identification device['Device'] = str(product["name"]) device['MLN'] = product["MLN"] device['ML_ID'] = '' - device['Serial_num'] = str(product["sn"]) - device['Zone'] = str(zone["name"]).split('/')[0] - device['Room'] = str(zone["name"]).split('/')[1] + try: + device['Serial_num'] = str(product["sn"]) + except KeyError: + device['Serial_num'] = '' + + # Physical location + if gateway_type == 'blgw': + # BLGW arranges rooms within zones + device['Zone'] = str(zone['name']).split('/')[0] + device['Room'] = str(zone['name']).split('/')[1] + elif gateway_type == 'mlgw': + # MLGW has no zoning concept - devices are arranged in rooms only + device['Room'] = str(zone['name']) device['Room_Number'] = str(zone["number"]) + # Source logging parameters for managing notifications device['Sources'] = OrderedDict() + device['Current Source'] = 'None' + device['Current Source Type'] = 'None' + device['Now Playing'] = 'None' + device['Channel/Track'] = '0' + device['State'] = 'Standby' + device['last update'] = time.time() for source in product["sources"]: 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() - device['Sources'][str(source["name"])]['source'] = source_id - device['Sources'][str(source["name"])]['uniqueID'] = str(source['sourceId']) + 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() + device['Sources'][str(source["name"])]['source'] = source_id + device['Sources'][str(source["name"])]['uniqueID'] = str(source['sourceId']) + else: + # MLGW config file is structured differently + source_id = self._srcdictsanitize(CONST.beo4_commanddict, source['selectID']).upper() + device['Sources'][str(source["name"])]['source'] = source_id source_tuple = (str(source["name"]), source_id) cmd_tuple = (source_id, (int(selectCmd["cmd"]), int(selectCmd["unit"]))) device['Sources'][str(source["name"])]['BR1_cmd'] = cmd_tuple @@ -70,7 +119,10 @@ class MLConfig: for channel in source['channels']: c = OrderedDict() c_num = '' - num = channel['selectSEQ'][::2] + if gateway_type == 'blgw': + num = channel['selectSEQ'][::2] + else: + num = channel['selectSEQ'][:-1] for n in num: c_num += str(n) c['number'] = int(c_num) @@ -98,7 +150,7 @@ class MLConfig: def get_masterlink_id(self, mlgw, mlcli): self.log.info("Finding MasterLink ID of products:") - if mlgw.is_connected and mlcli.is_connected: + if mlgw.is_connected and mlcli.is_connected and 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 @@ -106,23 +158,33 @@ class MLConfig: CONST.CMDS_DEST.get("AUDIO SOURCE"), CONST.BEO4_CMDS.get("LIGHT TIMEOUT")) - if device.get('Serial_num') is None: + if device.get('Serial_num') in [None, '']: # If this is a MasterLink product it has no serial number... # loop to until expected response received from ML Command Line Interface - 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) + test = True + while test: + try: + if mlcli.last_message['from_device'] == "MLGW" and \ + mlcli.last_message['payload_type'] == "MLGW_REMOTE_BEO4" and \ + mlcli.last_message['State_Update']['command'] == "Light Timeout": + + 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") + test = False + except KeyError: + 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") 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.debug(json.dumps(device, indent=4)) + + # ######################################################################################## + # Utility Functions @staticmethod def _srcdictsanitize(d, s): result = d.get(s) diff --git a/Resources/MLGW_CLIENT.py b/Resources/MLGW_CLIENT.py index 715af92..edfbb92 100644 --- a/Resources/MLGW_CLIENT.py +++ b/Resources/MLGW_CLIENT.py @@ -118,20 +118,11 @@ class MLGWClient(asynchat.async_chat): message['serial_Num'] = sn elif payload_type == "Source Status": - if CONST.rooms and CONST.devices: - for device in CONST.devices: - if device['MLN'] == payload[0]: - name = device['Device'] - 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 + self._get_device_info(message, payload) message["payload_type"] = payload_type message["MLN"] = payload[0] message["State_Update"] = OrderedDict() - message["State_Update"]["nowPlaying"] = '' + message["State_Update"]["nowPlaying"] = 'Unknown' message["State_Update"]["nowPlayingDetails"] = OrderedDict() message["State_Update"]["nowPlayingDetails"]["channel_track"] = \ self._hexword(payload[4], payload[5]) @@ -146,27 +137,26 @@ class MLGWClient(asynchat.async_chat): 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 device['MLN'] == payload[0]: - name = device['Device'] - 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 + self._get_device_info(message, payload) message["payload_type"] = payload_type message["MLN"] = payload[0] - 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["State_Update"] = OrderedDict([("sound_status", OrderedDict()), ("picture_status", OrderedDict())]) + message["State_Update"]["sound_status"]["mute_status"] = \ + self._getdictstr(CONST.mlgw_soundstatusdict, payload[1]) + message["State_Update"]["sound_status"]["speakermode"] = \ + self._getdictstr(CONST.mlgw_speakermodedict, payload[2]) + message["State_Update"]["sound_status"]["stereo_mode"] = \ + self._getdictstr(CONST.mlgw_stereoindicatordict, payload[9]) + message["State_Update"]["picture_status"]["screen1_mute"] = \ + self._getdictstr(CONST.mlgw_screenmutedict, payload[4]) + message["State_Update"]["picture_status"]["screen1_active"] = \ + self._getdictstr(CONST.mlgw_screenactivedict, payload[5]) + message["State_Update"]["picture_status"]["screen2_mute"] = \ + self._getdictstr(CONST.mlgw_screenmutedict, payload[6]) + message["State_Update"]["picture_status"]["screen2_active"] = \ + self._getdictstr(CONST.mlgw_screenactivedict, payload[7]) + message["State_Update"]["picture_status"]["cinema_mode"] = \ + self._getdictstr(CONST.mlgw_cinemamodedict, payload[8]) message["volume"] = int(payload[3]) elif payload_type == "All standby notification": @@ -177,7 +167,10 @@ class MLGWClient(asynchat.async_chat): if CONST.rooms: for room in CONST.rooms: if room['Room_Number'] == payload[0]: - message["Zone"] = room['Zone'].upper() + try: + message["Zone"] = room['Zone'].upper() + except KeyError: + pass message["Room"] = room['Room_Name'].upper() message["Type"] = self._getdictstr(CONST.mlgw_lctypedict, payload[1]).upper() + " COMMAND" message["Device"] = 'Beo4/BeoRemote One' @@ -236,7 +229,7 @@ class MLGWClient(asynchat.async_chat): self.messageCallBack(self.name, str(list(header)), str(list(payload)), message) # ######################################################################################## - # ##### mlgw send_cmder functions + # ##### mlgw send functions # send_cmd command to mlgw def _send_cmd(self, msg_type, payload): @@ -267,6 +260,7 @@ class MLGWClient(asynchat.async_chat): # Sleep to allow msg to arrive time.sleep(0.2) + # Ping the gateway def ping(self): self._send_cmd(CONST.MLGW_PL.get("PING"), "") @@ -337,31 +331,31 @@ class MLGWClient(asynchat.async_chat): def _getpayloadtypestr(self, payloadtype): result = CONST.mlgw_payloadtypedict.get(payloadtype) if result is None: - result = "UNKNOWN (type=" + self._hexbyte(payloadtype) + ")" + result = "UNKNOWN (type = " + self._hexbyte(payloadtype) + ")" return str(result) def _getbeo4commandstr(self, command): result = CONST.beo4_commanddict.get(command) if result is None: - result = "Cmd=" + self._hexbyte(command) + result = "CMD = " + self._hexbyte(command) return result def _getvirtualactionstr(self, action): result = CONST.mlgw_virtualactiondict.get(action) if result is None: - result = "Action=" + self._hexbyte(action) + result = "Action = " + self._hexbyte(action) return result def _getselectedsourcestr(self, source): result = CONST.ml_selectedsourcedict.get(source) if result is None: - result = "Src=" + self._hexbyte(source) + result = "SRC = " + self._hexbyte(source) return result def _getspeakermodestr(self, source): result = CONST.mlgw_speakermodedict.get(source) if result is None: - result = "mode=" + self._hexbyte(source) + result = "mode = " + self._hexbyte(source) return result def _getdictstr(self, mydict, mykey): @@ -376,16 +370,40 @@ class MLGWClient(asynchat.async_chat): for src in CONST.available_sources: if src[1] == source: message["State_Update"]["sourceName"] = src[0] - break + return + + @staticmethod + def _get_device_info(message, payload): + if CONST.rooms and CONST.devices: + for device in CONST.devices: + try: + if device['MLN'] == payload[0]: + name = device['Device'] + for room in CONST.rooms: + if name in room['Products']: + try: + message["Zone"] = room['Zone'].upper() + except KeyError: + pass + message["Room"] = room['Room_Name'].upper() + message["Type"] = 'AV RENDERER' + message["Device"] = name + return + except KeyError: + pass @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 + try: + 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'] + return + except KeyError: + pass diff --git a/Resources/MLtn_CLIENT.py b/Resources/MLtn_CLIENT.py index 8cbeeba..367abb1 100644 --- a/Resources/MLtn_CLIENT.py +++ b/Resources/MLtn_CLIENT.py @@ -66,13 +66,12 @@ class MLtnClient(asynchat.async_chat): self.handle_close() else: - self._decode(items) + try: + self._decode(items) + except IndexError: + self.log.debug(str(list(items))) self._received_data = "" - self.last_sent = '' - self.last_sent_at = 0 - self.last_received = '' - self.last_received_at = 0 def _decode(self, items): header = items[3][:-1] @@ -80,6 +79,10 @@ class MLtnClient(asynchat.async_chat): telegram = self._received_data[telegram_starts:].replace('!', '').split('/') message = OrderedDict() + if telegram[0] == 'Monitor events ( keys: M, E, C, (spc), Q ) ----': + self.toggle_commands() + self.toggle_macros() + if header == 'integration_protocol': message = self._decode_ip(telegram, message) @@ -322,13 +325,25 @@ class MLtnClient(asynchat.async_chat): time.sleep(0.2) def toggle_events(self): - self._send_cmd('e') + try: + self.push('e') + except socket.error, e: + self.log.info("Error sending data: %s" % e) + self.handle_close() def toggle_macros(self): - self._send_cmd('m') + try: + self.push('m') + except socket.error, e: + self.log.info("Error sending data: %s" % e) + self.handle_close() def toggle_commands(self): - self._send_cmd('c') + try: + self.push('c') + except socket.error, e: + self.log.info("Error sending data: %s" % e) + self.handle_close() def ping(self): self._send_cmd('')