diff --git a/Server Plugin/Actions.xml b/Server Plugin/Actions.xml deleted file mode 100644 index b807e45..0000000 --- a/Server Plugin/Actions.xml +++ /dev/null @@ -1,203 +0,0 @@ - - - http:// - - Send Beo4 Source Selection Command - send_beo4_src - - - - - - - - - - - - - - - - - - - - - Send Beo4 Key - send_beo4_key - - - - - - - - - - - - - - - - - - - - - Send BeoRemote One Source Selection Command - send_br1_src - - - - - - - - - - - - - - - - - Send BeoRemote One Key - send_br1_key - - - - - - - - - - - - - - - - - Request Device State Update - request_state_update - - - - Request BLGW Home Integration Protocol State Updates - send_hip_query - - - - - - - - - - - - - - - - - - - - - Send BLGW Home Integration Protocol Command - send_hip_cmd - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Send Free Text Home Integration Protocol Command - send_hip_cmd2 - - - - - - - - - Send Virtual Button - send_virtual_button - - - - - - - - - - - - - - - - - Post Message in Notification Centre - post_notification - - - - - - - - - - - - Send All Standby Command - all_standby - - diff --git a/Server Plugin/Devices.xml b/Server Plugin/Devices.xml deleted file mode 100644 index 97e5f01..0000000 --- a/Server Plugin/Devices.xml +++ /dev/null @@ -1,157 +0,0 @@ - - - - B&O Gateway (MLGW, BLGW) - - - - - - - - - - - - - - - - - - - - - String - Audio Source Changed - Current Audio Source is - - - String - Audio SourceName Changed - Current Audio SourceName is - - - String - Now Playing - Now Playing - - - Integer - Count of Active Audio Renderers - Count of Active Audio Renderers - - - String - Names of Active Audio Renderers - Names of Active Audio Renderers - - - Integer - Count of Active Video Renderers - Count of Active Video Renderers - - - String - Names of Active Video Renderers - Names of Active Video Renderers - - - - - - AV renderer (Beovision, Beosound) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Player Status Changed - Player Status is - Current Player Status - Player Status is - - - Boolean - Mute - Mute - - - Integer - Current Volume - Current Volume - - - Separator - - - - Integer - Channel/Track - Channel/Track - - - String - Now Playing - Now Playing - - - Separator - - - - - - - - - Source Changed - Source is - Current Source - Source is - - - playState - - diff --git a/Server Plugin/Events.xml b/Server Plugin/Events.xml deleted file mode 100644 index 66473d6..0000000 --- a/Server Plugin/Events.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - http:// - - All Standby - - - - Light Command Received - - - - - - - - - - - - - - Control Command Received - - - - - - - - - - - - - - BeoRemote Command Received - - - - - - - - - - - - - - Virtual Button Pressed - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Server Plugin/MenuItems.xml b/Server Plugin/MenuItems.xml deleted file mode 100644 index d8b9ee9..0000000 --- a/Server Plugin/MenuItems.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - Request Gateway Serial Number - request_serial_number - - - Request Device State Update - request_device_update - - - Reset Gateway Client Connections - reset_clients - - \ No newline at end of file diff --git a/Server Plugin/PluginConfig.xml b/Server Plugin/PluginConfig.xml deleted file mode 100644 index 93cb36a..0000000 --- a/Server Plugin/PluginConfig.xml +++ /dev/null @@ -1,93 +0,0 @@ - - - http:// - - - - - - - - - set_gateway - - - - set_gateway - - - - set_gateway - - - - - - - - set_login - - - - set_login - - - - - - - - - - - - - - - - set_default_audio - - - - set_music_control - play and control Apple Music - - - - - - - - - - - - set_music_control - - - - set_trackmode - prints track info to the Indigo log - - - - - - - - set_verbose - prints device telegrams to the Indigo log - - - - set_notifymode - posts information to the Notification Centre - - - - set_debug - prints debug info to the Indigo log - - diff --git a/Server Plugin/Resources/ASBridge.py b/Server Plugin/Resources/ASBridge.py deleted file mode 100644 index 31a6fd4..0000000 --- a/Server Plugin/Resources/ASBridge.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/python -import indigo -import logging -import os -import unicodedata -import threading -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, - running external appleScripts for more complex control, and - 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 = self.app.currentTrack().name() - album = self.app.currentTrack().album() - artist = self.app.currentTrack().artist() - number = self.app.currentTrack().trackNumber() - if name: - # Deal with tracks with non-ascii characters such as accents - name = unicodedata.normalize('NFD', name).encode('ascii', 'ignore') - album = unicodedata.normalize('NFD', album).encode('ascii', 'ignore') - artist = unicodedata.normalize('NFD', artist).encode('ascii', 'ignore') - return [name, album, artist, number] - - 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': - self. app.setValue_forKey_('true', 'shuffleEnabled') - 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): - # self.app.wind() - - # Native wind function can be a bit annoying - # I provide an alternative below that skips a set number of seconds forwards - self.set_current_track_position(time) - - def rewind(self, time): - # self.app.rewind() - - # Native rewind function can be a bit annoying - # I provide an alternative below that skips a set number of seconds back - self.set_current_track_position(time) - - # ######################################################################################## - # More complex playback control functions - def shuffle(self): - if self.app.shuffleEnabled(): - self.app.setValue_forKey_('false', 'shuffleEnabled') - else: - self.app.setValue_forKey_('true', 'shuffleEnabled') - - def set_current_track_position(self, time, mode='Relative'): - if mode == 'Relative': - # Set playback position in seconds relative to current position - 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 - threaded due to execution time - @staticmethod - def run_script(script, debug): - script = 'run script ("' + script + '" as POSIX file)' - if debug: - indigo.server.log(script, level=logging.DEBUG) - - def applet(_script): - # Run an external applescript file - s = NSAppleScript.alloc().initWithSource_(_script) - s.executeAndReturnError_(None) - - threading.Thread(target=applet, args=(script,)).start() - - @staticmethod - def notify(message, subtitle): - def applet(body, title): - # Post message in notification center - path = os.path.dirname(os.path.abspath(__file__)) - script = 'tell application "' + path + '/Notify.app" to notify("BeoGateway", "' + \ - body + '", "' + title + '")' - s = NSAppleScript.alloc().initWithSource_(script) - s.executeAndReturnError_(None) - - threading.Thread(target=applet, args=(message, subtitle,)).start() diff --git a/Server Plugin/Resources/ASBridge.pyc b/Server Plugin/Resources/ASBridge.pyc deleted file mode 100644 index 0fa8ae4..0000000 Binary files a/Server Plugin/Resources/ASBridge.pyc and /dev/null differ diff --git a/Server Plugin/Resources/BLHIP_CLIENT.py b/Server Plugin/Resources/BLHIP_CLIENT.py deleted file mode 100644 index 0ec9dbb..0000000 --- a/Server Plugin/Resources/BLHIP_CLIENT.py +++ /dev/null @@ -1,282 +0,0 @@ -import indigo -import asynchat -import socket -import time -import urllib -import logging -from collections import OrderedDict - -import Resources.CONSTANTS as CONST - - -class BLHIPClient(asynchat.async_chat): - """Client to interact with a Beolink Gateway via the Home Integration Protocol - https://manualzz.com/download/14415327 - Full documentation of states, commands and events can be found in the driver development guide - https://vdocument.in//blgw-driver-development-guide-blgw-driver-development-guide-7-2016-10-10""" - def __init__(self, host_address='blgw.local', port=9100, user='admin', pwd='admin', name='BLGW_HIP', - debug=False, cb=None): - asynchat.async_chat.__init__(self) - - self.debug = debug - - self._host = host_address - self._port = int(port) - self._user = user - self._pwd = pwd - self.name = name - self.is_connected = False - - self._received_data = '' - self.last_sent = '' - self.last_sent_at = time.time() - self.last_received = '' - self.last_received_at = time.time() - self.last_message = {} - - # Optional callback function - if cb: - self.messageCallBack = cb - else: - self.messageCallBack = None - - # ######################################################################################## - # ##### Open Socket and connect to B&O Gateway - self.client_connect() - - # ######################################################################################## - # ##### Client functions - def collect_incoming_data(self, data): - self.is_connected = True - self._received_data += data - - def found_terminator(self): - # indigo.server.log("Raw Data: " + self._received_data) - self.last_received = self._received_data - self.last_received_at = time.time() - - if self._received_data == 'error': - self.handle_close() - - if self._received_data == 'e OK f%20%2A/%2A/%2A/%2A': - indigo.server.log('\tAuthentication Successful!', level=logging.DEBUG) - self.query(dev_type="AV renderer") - - self._received_data = urllib.unquote(self._received_data) - telegram = self._received_data.replace("%201", "") - telegram = telegram.split('/') - header = telegram[0:4] - - self._decode(header, telegram) - - def _decode(self, header, telegram): - e_string = str(header[0]) - if e_string[0] == 'e': - if e_string[2:4] == 'OK' and self.debug: - indigo.server.log('Command Successfully Processed: ' + str(urllib.unquote(self._received_data)), - level=logging.DEBUG) - - elif e_string[2:5] == 'CMD': - indigo.server.log('Wrong or Unrecognised Command: ' + str(urllib.unquote(self._received_data)), - level=logging.WARNING) - - elif e_string[2:5] == 'SYN': - indigo.server.log('Bad Syntax, or Wrong Character Encoding: ' + - str(urllib.unquote(self._received_data)), level=logging.WARNING) - - elif e_string[2:5] == 'ACC': - indigo.server.log('Zone Access Violation: ' + str(urllib.unquote(self._received_data)), - level=logging.WARNING) - - elif e_string[2:5] == 'LEN': - indigo.server.log('Received Message Too Long: ' + str(urllib.unquote(self._received_data)), - level=logging.WARNING) - - self._received_data = "" - return - else: - self._received_data = "" - - if len(telegram) > 4: - 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['Device'] = telegram[3] - message['State_Update'] = OrderedDict() - - for s in state: - if s.split('=')[0] == "nowPlayingDetails": - play_details = s.split('=') - if len(play_details[1]) > 0: - play_details = play_details[1].split('; ') - message['State_Update']["nowPlayingDetails"] = OrderedDict() - 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'][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 - try: - if 'nowPlayingDetails' in message['State_Update'] \ - and message['State_Update']['nowPlayingDetails']['type'] == 'Legacy': - self._get_channel_track(message) - except KeyError: - pass - - if message.get('Type') == 'BUTTON': - if message['State_Update'].get('STATE') == '0': - message['State_Update']['Status'] = 'Off' - else: - message['State_Update']['Status'] = 'On' - - if message.get('Type') == 'DIMMER': - if message['State_Update'].get('LEVEL') == '0': - message['State_Update']['Status'] = 'Off' - else: - message['State_Update']['Status'] = 'On' - - self._report(header, state, message) - - def _report(self, header, payload, message): - self.last_message = message - if self.messageCallBack: - self.messageCallBack(self.name, str(list(header)), str(list(payload)), message) - - def client_connect(self): - indigo.server.log('Connecting to host at ' + self._host + ', port ' + str(self._port), level=logging.WARNING) - self.set_terminator(b'\r\n') - # Create the socket - try: - self.create_socket(socket.AF_INET, socket.SOCK_STREAM) - except socket.error, e: - indigo.server.log("Error creating socket: " + str(e), level=logging.ERROR) - self.handle_close() - # Now connect - try: - self.connect((self._host, self._port)) - except socket.gaierror, e: - indigo.server.log("\tError with address: " + str(e), level=logging.ERROR) - self.handle_close() - except socket.timeout, e: - indigo.server.log("\tSocket connection timed out: " + str(e), level=logging.ERROR) - self.handle_close() - except socket.error, e: - indigo.server.log("\tError opening connection: " + str(e), level=logging.ERROR) - self.handle_close() - else: - self.is_connected = True - indigo.server.log("\tConnected to B&O Gateway", level=logging.DEBUG) - - def handle_connect(self): - indigo.server.log("\tAttempting to Authenticate...", level=logging.WARNING) - self.send_cmd(self._user) - self.send_cmd(self._pwd) - self.statefilter() - - def handle_close(self): - indigo.server.log(self.name + ": Closing socket", level=logging.ERROR) - self.is_connected = False - self.close() - - def send_cmd(self, telegram): - try: - self.push(telegram.encode("ascii") + "\r\n") - except socket.timeout, e: - indigo.server.log("\tSocket connection timed out: " + str(e), level=logging.ERROR) - self.handle_close() - except socket.error, e: - indigo.server.log("Error sending data: " + str(e), level=logging.ERROR) - self.handle_close() - else: - self.last_sent = telegram - self.last_sent_at = time.time() - if telegram == 'q Main/global/SYSTEM/BeoLink': - if self.debug: - indigo.server.log(self.name + " >>-SENT--> : " + telegram, level=logging.DEBUG) - else: - indigo.server.log(self.name + " >>-SENT--> : " + telegram, level=logging.INFO) - time.sleep(0.2) - - def query(self, zone='*', room='*', dev_type='*', device='*'): - query = "q " + zone + "/" + room + "/" + dev_type + '/' + device - - # Convert to human readable string - if zone == '*': - zone = ' in all zones.' - else: - zone = ' in zone ' + zone + '.' - if room == '*': - room = ' in all rooms' - else: - room = ' in room ' + room - if dev_type == '*': - dev_type = ' of all types' - else: - dev_type = ' of type ' + dev_type - if device == '*': - device = ' all devices' - else: - device = ' devices called ' + device - - if self.debug: - indigo.server.log(self.name + ": sending state update request for" + device + dev_type + room + zone, - level=logging.DEBUG) - self.send_cmd(query) - - def statefilter(self, zone='*', room='*', dev_type='*', device='*'): - s_filter = "f " + zone + "/" + room + "/" + dev_type + '/' + device - self.send_cmd(s_filter) - - def locationevent(self, event): - if event in ['leave', 'arrive']: - event = 'l ' + event - self.send_cmd(event) - - def ping(self): - self.query('Main', 'global', 'SYSTEM', 'BeoLink') - - # Utility Functions - @staticmethod - def _srcdictsanitize(d, s): - result = d.get(s) - if result is None: - result = s - return str(result) - - def _get_channel_track(self, message): - try: - node = indigo.devices[message['Device']] - # Get properties - node_props = node.pluginProps - source_name = message["State_Update"]["sourceName"].strip().replace(" ", "_") - if self.debug: - indigo.server.log('searching device ' + node.name + ' channel list for source ' + source_name, - level=logging.DEBUG) - if 'channels' in node_props['sources'][source_name]: - for channel in node_props['sources'][source_name]['channels']: - if self.debug: - indigo.server.log(source_name + " Channel " + channel[0][1:] + " = " + channel[1], - level=logging.DEBUG) - if int(channel[0][1:]) == int( - message["State_Update"]['nowPlayingDetails']["channel_track"]): - message["State_Update"]["nowPlaying"] = channel[1] - if self.debug: - indigo.server.log("Current Channel: " + channel[1], level=logging.DEBUG) - return - - # If source list exhausted then return Unknown - message["State_Update"]["nowPlaying"] = 'Unknown' - except KeyError: - message["State_Update"]["nowPlaying"] = 'Unknown' diff --git a/Server Plugin/Resources/BLHIP_CLIENT.pyc b/Server Plugin/Resources/BLHIP_CLIENT.pyc deleted file mode 100644 index d7ec4e8..0000000 Binary files a/Server Plugin/Resources/BLHIP_CLIENT.pyc and /dev/null differ diff --git a/Server Plugin/Resources/CONSTANTS.py b/Server Plugin/Resources/CONSTANTS.py deleted file mode 100644 index 7ea7e41..0000000 --- a/Server Plugin/Resources/CONSTANTS.py +++ /dev/null @@ -1,683 +0,0 @@ -from collections import OrderedDict - - -# Constants for B&O telegram protocols -# ######################################################################################## -# Config data (set on initialisation) -rooms = [] -available_sources = [] - -standby_state = [ - {'key': 'onOffState', 'value': False}, - {'key': 'playState', 'value': 'Standby'}, - {'key': 'source', 'value': 'Standby'}, - {'key': 'nowPlaying', 'value': 'Unknown'}, - {'key': 'channelTrack', 'value': 0}, - {'key': 'mute', 'value': True}, - {'key': 'volume', 'value': 0}, -] - -gw_all_stb = [ - {'key': 'AudioRenderers', 'value': ''}, - {'key': 'VideoRenderers', 'value': ''}, - {'key': 'nAudioRenderers', 'value': 0}, - {'key': 'nVideoRenderers', 'value': 0}, - {'key': 'currentAudioSource', 'value': 'Unknown'}, - {'key': 'currentAudioSourceName', 'value': 'Unknown'}, - {'key': 'nowPlaying', 'value': 'Unknown'}, -] - -# ######################################################################################## -# Source Types - -source_type_dict = dict( - [ - ("Video Sources", ("TV", "V.AUX/DTV2", "MEDIA", "V.TAPE/V.MEM/DVD2", "DVD", "DVD2", "CAMERA", - "SAT/DTV", "PC", "WEB", "DOORCAM", "PHOTO", "USB2", "WEBMEDIA", "AV.IN", - "HOMEMEDIA", "DVB_RADIO", "DNLA", "RECORDINGS", "CAMERA", "USB", "DNLA-DMR", "YOUTUBE", - "HOME.APP", "HDMI_1", "HDMI_2", "HDMI_3", "HDMI_4", "HDMI_5", "HDMI_6", - "HDMI_7", "HDMI_8", "MATRIX_1", "MATRIX_2", "MATRIX_3", "MATRIX_4", "MATRIX_5", - "MATRIX_6", "MATRIX_7", "MATRIX_8", "MATRIX_9", "MATRIX_10", "MATRIX_11", - "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_srcdict = OrderedDict( - [ - # Source selection: - (0x0C, "Standby"), - (0x47, "Sleep"), - (0x80, "TV"), - (0x81, "Radio"), - (0x82, "V.Aux/DTV2"), - (0x83, "A.Aux"), - (0x84, "Media"), - (0x85, "V.Tape/V.Mem"), - (0x86, "DVD"), - (0x87, "Camera"), - (0x88, "Text"), - (0x8A, "Sat/DTV"), - (0x8B, "PC"), - (0x8C, "Web"), - (0x8D, "Doorcam"), - (0x8E, "Photo"), - (0x90, "USB2"), - (0x91, "A.Tape/A.Mem"), - (0x92, "CD"), - (0x93, "Phono/N.Radio"), - (0x94, "A.Tape2/N.Music"), - (0x95, "Server"), - (0x96, "Spotify"), - (0x97, "CD2/Join"), - (0xBF, "AV"), - ] -) - -beo4_commanddict = OrderedDict( - [ - # Source selection: - (0x0C, "Standby"), - (0x47, "Sleep"), - (0x80, "TV"), - (0x81, "Radio"), - (0x82, "V.Aux/DTV2"), - (0x83, "A.Aux"), - (0x84, "Media"), - (0x85, "V.Tape/V.Mem"), - (0x86, "DVD"), - (0x87, "Camera"), - (0x88, "Text"), - (0x8A, "Sat/DTV"), - (0x8B, "PC"), - (0x8C, "Web"), - (0x8D, "Doorcam"), - (0x8E, "Photo"), - (0x90, "USB2"), - (0x91, "A.Tape/A.Mem"), - (0x92, "CD"), - (0x93, "Phono/N.Radio"), - (0x94, "A.Tape2/N.Music"), - (0x95, "Server"), - (0x96, "Spotify"), - (0x97, "CD2/Join"), - (0xBF, "AV"), - (0xFA, "P-IN-P"), - # Digits: - (0x00, "Digit-0"), - (0x01, "Digit-1"), - (0x02, "Digit-2"), - (0x03, "Digit-3"), - (0x04, "Digit-4"), - (0x05, "Digit-5"), - (0x06, "Digit-6"), - (0x07, "Digit-7"), - (0x08, "Digit-8"), - (0x09, "Digit-9"), - # Source control: - (0x1E, "Step Up"), - (0x1F, "Step Down"), - (0x32, "Rewind"), - (0x33, "Return"), - (0x34, "Wind"), - (0x35, "Go/Play"), - (0x36, "Stop"), - (0xD4, "Yellow"), - (0xD5, "Green"), - (0xD8, "Blue"), - (0xD9, "Red"), - # Sound and picture control - (0x0D, "Mute"), - (0x1C, "P.Mute"), - (0x2A, "Format"), - (0x44, "Sound/Speaker"), - (0x5C, "Menu"), - (0x60, "Volume Up"), - (0x64, "Volume Down"), - (0xDA, "Cinema_On"), - (0xDB, "Cinema_Off"), - # Other controls: - (0xF7, "Stand"), - (0x0A, "Clear"), - (0x0B, "Store"), - (0x0E, "Reset"), - (0x14, "Back"), - (0x15, "MOTS"), - (0x20, "Goto"), - (0x28, "Show Clock"), - (0x2D, "Eject"), - (0x37, "Record"), - (0x3F, "Select"), - (0x46, "Sound"), - (0x7F, "Exit"), - (0xC0, "Shift-0/Edit"), - (0xC1, "Shift-1/Random"), - (0xC2, "Shift-2"), - (0xC3, "Shift-3/Repeat"), - (0xC4, "Shift-4/Select"), - (0xC5, "Shift-5"), - (0xC6, "Shift-6"), - (0xC7, "Shift-7"), - (0xC8, "Shift-8"), - (0xC9, "Shift-9"), - # Continue functionality: - (0x70, "Rewind Repeat"), - (0x71, "Wind Repeat"), - (0x72, "Step_UP Repeat"), - (0x73, "Step_DW Repeat"), - (0x75, "Go Repeat"), - (0x76, "Green Repeat"), - (0x77, "Yellow Repeat"), - (0x78, "Blue Repeat"), - (0x79, "Red Repeat"), - (0x7E, "Key Release"), - # Functions: - (0x40, "Guide"), - (0x43, "Info"), - (0x0F, "Function_1"), - (0x10, "Function_2"), - (0x11, "Function_3"), - (0x12, "Function_4"), - (0x19, "Function_5"), - (0x1A, "Function_6"), - (0x21, "Function_7"), - (0x22, "Function_8"), - (0x23, "Function_9"), - (0x24, "Function_10"), - (0x25, "Function_11"), - (0x26, "Function_12"), - (0x27, "Function_13"), - (0x39, "Function_14"), - (0x3A, "Function_15"), - (0x3B, "Function_16"), - (0x3C, "Function_17"), - (0x3D, "Function_18"), - (0x3E, "Function_19"), - (0x4B, "Function_20"), - (0x4C, "Function_21"), - (0x50, "Function_22"), - (0x51, "Function_23"), - (0x7D, "Function_24"), - (0xA5, "Function_25"), - (0xA6, "Function_26"), - (0xA9, "Function_27"), - (0xAA, "Function_28"), - (0xDD, "Function_29"), - (0xDE, "Function_30"), - (0xE0, "Function_31"), - (0xE1, "Function_32"), - (0xE2, "Function_33"), - (0xE6, "Function_34"), - (0xE7, "Function_35"), - (0xF2, "Function_36"), - (0xF3, "Function_37"), - (0xF4, "Function_38"), - (0xF5, "Function_39"), - (0xF6, "Function_40"), - # Cursor functions: - (0x13, "Select"), - (0xCA, "Cursor_Up"), - (0xCB, "Cursor_Down"), - (0xCC, "Cursor_Left"), - (0xCD, "Cursor_Right"), - # Light/Control commands - (0x9B, "Light"), - (0x9C, "Command"), - (0x58, "Light Timeout"), - # Dummy for 'Listen for all commands' - (0xFF, ""), - ] -) -BEO4_CMDS = {v.upper(): k for k, v in beo4_commanddict.items()} - -# BeoRemote One Commands -beoremoteone_commanddict = OrderedDict( - [ - # Source, (Cmd, Unit) - ("Standby", (0x0C, 0)), - ("TV", (0x80, 0)), - ("RADIO", (0x81, 0)), - ("TUNEIN", (0x81, 1)), - ("DVB_RADIO", (0x81, 2)), - ("AV.IN", (0x82, 0)), - ("LINE.IN", (0x83, 0)), - ("A.AUX", (0x83, 1)), - ("BLUETOOTH", (0x83, 2)), - ("HOMEMEDIA", (0x84, 0)), - ("DNLA", (0x84, 1)), - ("RECORDINGS", (0x85, 0)), - ("CAMERA", (0x87, 0)), - ("FUTURE.USE", (0x89, 0)), - ("USB", (0x90, 0)), - ("A.MEM", (0x91, 0)), - ("CD", (0x92, 0)), - ("N.RADIO", (0x93, 0)), - ("A.TAPE2/N.MUSIC", (0x94, 0)), - ("MUSIC", (0x94, 0)), - ("DNLA-DMR", (0x94, 1)), - ("AIRPLAY", (0x94, 2)), - ("SPOTIFY", (0x96, 0)), - ("DEEZER", (0x96, 1)), - ("QPLAY", (0x96, 2)), - ("JOIN", (0x97, 0)), - ("WEBMEDIA", (0x8C, 0)), - ("YOUTUBE", (0x8C, 1)), - ("HOME.APP", (0x8C, 2)), - ("HDMI_1", (0xCE, 0)), - ("HDMI_2", (0xCE, 1)), - ("HDMI_3", (0xCE, 2)), - ("HDMI_4", (0xCE, 3)), - ("HDMI_5", (0xCE, 4)), - ("HDMI_6", (0xCE, 5)), - ("HDMI_7", (0xCE, 6)), - ("HDMI_8", (0xCE, 7)), - ("MATRIX_1", (0xCF, 0)), - ("MATRIX_2", (0xCF, 1)), - ("MATRIX_3", (0xCF, 2)), - ("MATRIX_4", (0xCF, 3)), - ("MATRIX_5", (0xCF, 4)), - ("MATRIX_6", (0xCF, 5)), - ("MATRIX_7", (0xCF, 6)), - ("MATRIX_8", (0xCF, 7)), - ("MATRIX_9", (0xD0, 0)), - ("MATRIX_10", (0xD0, 1)), - ("MATRIX_11", (0xD0, 2)), - ("MATRIX_12", (0xD0, 3)), - ("MATRIX_13", (0xD0, 4)), - ("MATRIX_14", (0xD0, 5)), - ("MATRIX_15", (0xD0, 6)), - ("MATRIX_16", (0xD0, 7)), - ("PERSONAL_1", (0xD1, 0)), - ("PERSONAL_2", (0xD1, 1)), - ("PERSONAL_3", (0xD1, 2)), - ("PERSONAL_4", (0xD1, 3)), - ("PERSONAL_5", (0xD1, 4)), - ("PERSONAL_6", (0xD1, 5)), - ("PERSONAL_7", (0xD1, 6)), - ("PERSONAL_8", (0xD1, 7)), - ("TV.ON", (0xD2, 0)), - ("MUSIC.ON", (0xD3, 0)), - ("PATTERNPLAY", (0xD3, 1)), - ] -) - -beoremoteone_keydict = OrderedDict( - [ - (0x0C, "Standby"), - # Digits: - (0x00, "Digit-0"), - (0x01, "Digit-1"), - (0x02, "Digit-2"), - (0x03, "Digit-3"), - (0x04, "Digit-4"), - (0x05, "Digit-5"), - (0x06, "Digit-6"), - (0x07, "Digit-7"), - (0x08, "Digit-8"), - (0x09, "Digit-9"), - # Source control: - (0x1E, "Step Up"), - (0x1F, "Step Down"), - (0x32, "Rewind"), - (0x33, "Return"), - (0x34, "Wind"), - (0x35, "Go/Play"), - (0x36, "Stop"), - (0xD4, "Yellow"), - (0xD5, "Green"), - (0xD8, "Blue"), - (0xD9, "Red"), - # Sound and picture control - (0x0D, "Mute"), - (0x1C, "P.Mute"), - (0x2A, "Format"), - (0x44, "Sound/Speaker"), - (0x5C, "Menu"), - (0x60, "Volume Up"), - (0x64, "Volume Down"), - (0xDA, "Cinema_On"), - (0xDB, "Cinema_Off"), - # Other controls: - (0xF7, "Stand"), - (0x0A, "Clear"), - (0x0B, "Store"), - (0x0E, "Reset"), - (0x14, "Back"), - (0x15, "MOTS"), - (0x20, "Goto"), - (0x28, "Show Clock"), - (0x2D, "Eject"), - (0x37, "Record"), - (0x3F, "Select"), - (0x46, "Sound"), - (0x7F, "Exit"), - (0xC0, "Shift-0/Edit"), - (0xC1, "Shift-1/Random"), - (0xC2, "Shift-2"), - (0xC3, "Shift-3/Repeat"), - (0xC4, "Shift-4/Select"), - (0xC5, "Shift-5"), - (0xC6, "Shift-6"), - (0xC7, "Shift-7"), - (0xC8, "Shift-8"), - (0xC9, "Shift-9"), - # Continue functionality: - (0x70, "Rewind Repeat"), - (0x71, "Wind Repeat"), - (0x72, "Step_UP Repeat"), - (0x73, "Step_DW Repeat"), - (0x75, "Go Repeat"), - (0x76, "Green Repeat"), - (0x77, "Yellow Repeat"), - (0x78, "Blue Repeat"), - (0x79, "Red Repeat"), - (0x7E, "Key Release"), - # Functions: - (0x40, "Guide"), - (0x43, "Info"), - # Cursor functions: - (0x13, "Select"), - (0xCA, "Cursor_Up"), - (0xCB, "Cursor_Down"), - (0xCC, "Cursor_Left"), - (0xCD, "Cursor_Right"), - # Light/Control commands - (0x9B, "Light"), - (0x9C, "Command"), - (0x58, "Light Timeout") - ] -) - -# ######################################################################################## -# Source Activity -sourceactivitydict = OrderedDict( - [ - (0x00, "Unknown"), - (0x01, "Stop"), - (0x02, "Play"), - (0x03, "Wind"), - (0x04, "Rewind"), - (0x05, "Record Lock"), - (0x06, "Standby"), - (0x07, "Load/No Media"), - (0x08, "Still Picture"), - (0x14, "Scan Forward"), - (0x15, "Scan Reverse"), - (0xFF, "None"), - ] -) - -# ######################################################################################## -# ##### MasterLink (not MLGW) Protocol packet constants -ml_telegram_type_dict = dict( - [ - (0x0A, "COMMAND"), - (0x0B, "REQUEST"), - (0x14, "RESPONSE"), - (0x2C, "INFO"), - (0x5E, "CONFIG"), - ] -) - -ml_command_type_dict = dict( - [ - (0x04, "MASTER_PRESENT"), - # REQUEST_DISTRIBUTED_SOURCE: seen when a device asks what source is being distributed - # subtypes seen 01:request 04:no source 06:has source (byte 13 is source) - (0x08, "REQUEST_DISTRIBUTED_SOURCE"), - (0x0D, "BEO4_KEY"), - (0x10, "STANDBY"), - (0x11, "RELEASE"), # when a device turns off - (0x20, "MLGW_REMOTE_BEO4"), - # REQUEST_LOCAL_SOURCE: Seen when a device asks what source is playing locally to a device - # subtypes seen 02:request 04:no source 05:secondary source 06:primary source (byte 11 is source) - # byte 10 is bitmask for distribution: 0x01: coaxial cable - 0x02: MasterLink ML_BUS - - # 0x08: local screen - (0x30, "REQUEST_LOCAL_SOURCE"), - (0x3C, "TIMER"), - (0x40, "CLOCK"), - (0x44, "TRACK_INFO"), - (0x45, "GOTO_SOURCE"), - # LOCKMANAGER_COMMAND: Lock to Determine what device issues source commands - # reference: https://tidsskrift.dk/daimipb/article/download/7043/6004/0 - (0x5C, "LOCK_MANAGER_COMMAND"), - (0x6C, "DISTRIBUTION_REQUEST"), - (0x82, "TRACK_INFO_LONG"), - # Source Status - # byte 10:source - byte 13: 80 when DTV is turned off. 00 when it's on - # byte 18H 17L: source medium - byte 19: channel/track - byte 21:activity - # byte 22: 01: audio source 02: video source ff:undefined - byte 23: picture identifier - (0x87, "STATUS_INFO"), - (0x94, "VIDEO_TRACK_INFO"), - # - # ----------------------------------------------------------------------- - # More packets that we see on the bus, with a guess of the type - # DISPLAY_SOURCE: Message sent with a payload showing the displayed source name. - # subtype 3 has the printable source name starting at byte 10 of the payload - (0x06, "DISPLAY_SOURCE"), - # START_VIDEO_DISTRIBUTION: Sent when a locally playing source starts being distributed on coaxial cable - (0x07, "START_VIDEO_DISTRIBUTION"), - # EXTENDED_SOURCE_INFORMATION: message with 6 subtypes showing information about the source. - # Printable info at byte 14 of the payload - # For Radio: 1: "" 2: Genre 3: Country 4: RDS info 5: Associated beo4 button 6: "Unknown" - # For A.Mem: 1: Genre 2: Album 3: Artist 4: Track name 5: Associated beo4 button 6: "Unknown" - (0x0B, "EXTENDED_SOURCE_INFORMATION"), - (0x96, "PC_PRESENT"), - # PICTURE AND SOUND STATUS - # byte 0: bit 0-1: sound status - bit 2-3: stereo mode (can be 0 in a 5.1 setup) - # byte 1: speaker mode (see below) - # byte 2: audio volume - # byte 3: picture format identifier (see below) - # byte 4: bit 0: screen1 mute - bit 1: screen2 mute - bit 2: screen1 active - - # bit 3: screen2 active - bit 4: cinema mode - (0x98, "PICTURE_AND_SOUND_STATUS"), - # Unknown commands - seen on power up and initialisation - ######################################################### - # On power up all devices send out a request key telegram. If - # no lock manager is allocated the devices send out a key_lost telegram. The Video Master (or Power - # Master in older implementations) then asserts a NEW_LOCKmANAGER telegram and assumes responsibility - # for LOCKMANAGER_COMMAND telegrams until a key transfer occurs. - # reference: https://tidsskrift.dk/daimipb/article/download/7043/6004/0 - (0x12, "KEY_LOST"), # ? - # Unknown command with payload of length 1. - # bit 0: unknown - # bit 1: unknown - (0xA0, "NEW_LOCKMANAGER"), # ? - # Unknown command with payload of length 2 - # bit 0: unknown - # bit 1: unknown - # bit 2: unknown - ] -) - -ml_command_type_request_key_subtype_dict = dict( - [ - (0x01, "Request Key"), - (0x02, "Transfer Key"), - (0x03, "Transfer Impossible"), - (0x04, "Key Received"), - (0x05, "Timeout"), - (0xFF, "Undefined"), - ] -) - -ml_activity_dict = dict( - [ - (0x01, "Request Source"), - (0x02, "Request Source"), - (0x04, "No Source"), - (0x06, "Source Active"), - ] -) - -ml_device_dict = dict( - [ - (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"), - (0x29, "SYSTEM CONTROLLER/TIMER"), - # Power Master exists in older (pre 1996?) ML implementations. Later revisions enforced the Video Master - # as lock key manager for the system and the concept was phased out. If your system is older than 2000 - # you may see this device type on the network. - # reference: https://tidsskrift.dk/daimipb/article/download/7043/6004/0 - (0xFF, "POWER MASTER"), # ? - ] -) - -ml_pictureformatdict = dict( - [ - (0x00, "Not known"), - (0x01, "Known by decoder"), - (0x02, "4:3"), - (0x03, "16:9"), - (0x04, "4:3 Letterbox middle"), - (0x05, "4:3 Letterbox top"), - (0x06, "4:3 Letterbox bottom"), - (0xFF, "Blank picture"), - ] -) - -ml_selectedsourcedict = dict( - [ - (0x00, "NONE"), - (0x0B, "TV"), - (0x15, "V.TAPE/V.MEM"), - (0x16, "DVD2"), - (0x1F, "SAT/DTV"), - (0x29, "DVD"), - (0x33, "V.AUX/DTV2"), - (0x3E, "DOORCAM"), - (0x47, "PC"), - (0x6F, "RADIO"), - (0x79, "A.TAPE/A.MEM"), - (0x7A, "A.TAPE2/N.MUSIC"), - (0x8D, "CD"), - (0x97, "A.AUX"), - (0xA1, "PHONO/N.RADIO"), - # Dummy for 'Listen for all sources' - (0xFE, "ALL"), # have also seen 0xFF as "all" - (0xFF, "ALL"), - ] -) - -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)), - ("VIDEO_PAUSABLE", (0x15, 0x16, 0x29, 0x33)), - ("AUDIO", (0x6F, 0x97)), - ("AUDIO_PAUSABLE", (0x8D, 0x79, 0x7A, 0xA1, 0x8D)), - ("ALL", (0xFE, 0xFF)), - ("OTHER", (0x47, 0x3E)), - ] -) - -# ######################################################################################## -# ##### MLGW Protocol packet constants -mlgw_payloadtypedict = dict( - [ - (0x01, "Beo4 Command"), - (0x02, "Source Status"), - (0x03, "Picture and Sound Status"), - (0x04, "Light and Control command"), - (0x05, "All standby notification"), - (0x06, "BeoRemote One control command"), - (0x07, "BeoRemote One source selection"), - (0x20, "MLGW virtual button event"), - (0x30, "Login request"), - (0x31, "Login status"), - (0x32, "Change password request"), - (0x33, "Change password response"), - (0x34, "Secure login request"), - (0x36, "Ping"), - (0x37, "Pong"), - (0x38, "Configuration change notification"), - (0x39, "Request Serial Number"), - (0x3A, "Serial Number"), - (0x40, "Location based event"), - ] -) -MLGW_PL = {v.upper(): k for k, v in mlgw_payloadtypedict.items()} - -destselectordict = OrderedDict( - [ - (0x00, "Video Source"), - (0x01, "Audio Source"), - (0x05, "Peripheral Video Source (V.TAPE/V.MEM/DVD)"), - (0x06, "Secondary Peripheral Video Source (V.TAPE2/V.MEM2/DVD2)"), - (0x0F, "All Products"), - (0x1B, "MLGW"), - ] -) -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_virtualactiondict = dict([(0x01, "PRESS"), (0x02, "HOLD"), (0x03, "RELEASE")]) - -# for '0x03: Picture and Sound Status' -mlgw_soundstatusdict = dict([(0x00, "Not muted"), (0x01, "Muted")]) - -mlgw_speakermodedict = dict( - [ - (0x01, "Center channel"), - (0x02, "2 channel stereo"), - (0x03, "Front surround"), - (0x04, "4 channel stereo"), - (0x05, "Full surround"), - (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, "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 '0x31: Login Status -mlgw_loginstatusdict = dict([(0x00, "OK"), (0x01, "FAIL")]) - -# ######################################################################################## -# ##### BeoLink Gateway Protocol packet constants -blgw_srcdict = dict( - [ - ("TV", "TV"), - ("DVD", "DVD"), - ("RADIO", "RADIO"), - ("TP1", "A.TAPE/A.MEM"), - ("TP2", "A.TAPE2/N.MUSIC"), - ("CD", "CD"), - ("PH", "PHONO/N.RADIO"), - ] -) - -blgw_devtypes = OrderedDict( - [ - ("*", "All"), - ("SYSTEM", "System"), - ("AV renderer", "AV Renderer"), - ("BUTTON", "Button"), - ("Dimmer", "Dimmer"), - ("GPIO", "GPIO"), - ("Thermostat 1 setpoint", "Thermostat 1 setpoint"), - ("Thermostat 2 setpoints", "Thermostat 2 setpoints") - ] -) diff --git a/Server Plugin/Resources/CONSTANTS.pyc b/Server Plugin/Resources/CONSTANTS.pyc deleted file mode 100644 index 3583052..0000000 Binary files a/Server Plugin/Resources/CONSTANTS.pyc and /dev/null differ diff --git a/Server Plugin/Resources/MLCLI_CLIENT.py b/Server Plugin/Resources/MLCLI_CLIENT.py deleted file mode 100644 index 3ceef3a..0000000 --- a/Server Plugin/Resources/MLCLI_CLIENT.py +++ /dev/null @@ -1,462 +0,0 @@ -import indigo -import asynchat -import socket -import time -import logging -from collections import OrderedDict - -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 - of the Bang & Olufsen Gateway.""" - def __init__(self, host_address='blgw.local', port=23, user='admin', pwd='admin', name='ML_CLI', - debug=False, cb=None): - asynchat.async_chat.__init__(self) - - self.debug = debug - - self._host = host_address - self._port = int(port) - self._user = user - self._pwd = pwd - self.name = name - self.is_connected = False - - self._i = 0 - self._header_lines = 6 - self._received_data = "" - self.last_sent = '' - self.last_sent_at = time.time() - self.last_received = '' - self.last_received_at = time.time() - self.last_message = {} - - self.isBLGW = False - - # Optional callback function - if cb: - self.messageCallBack = cb - else: - self.messageCallBack = None - - # ######################################################################################## - # ##### Open Socket and connect to B&O Gateway - self.client_connect() - - # ######################################################################################## - # ##### Client functions - def collect_incoming_data(self, data): - self._received_data += data - - def found_terminator(self): - self.last_received = self._received_data - self.last_received_at = time.time() - - telegram = self._received_data - self._received_data = "" - - # Clear login process lines before processing telegrams - if self._i <= self._header_lines: - self._i += 1 - if self._i == self._header_lines - 1: - indigo.server.log("\tAuthenticated! Gateway type is " + telegram[0:4] + "\n", level=logging.DEBUG) - if telegram[0:4] != "MLGW": - self.isBLGW = True - - # Process telegrams and return json data in human readable format - if self._i > self._header_lines: - if "---- Logging" in telegram: - # Pong telegram - header = telegram - payload = [] - message = OrderedDict([('payload_type', 'Pong'), ('State_Update', dict([('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 (ValueError, TypeError): - # abort if invalid character found - if self.debug: - indigo.server.log('Invalid character ' + str(item) + ' found in telegram: ' + - ''.join(items) + '\nAborting!', level=logging.ERROR) - break - - # Decode any telegram with a valid 9 byte header, excluding typy 0x14 (regular clock sync pings) - if len(telegram) >= 9: - # 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): - indigo.server.log('Connecting to host at ' + self._host + ', port ' + str(self._port), level=logging.WARNING) - self.set_terminator(b'\r\n') - # Create the socket - try: - self.create_socket(socket.AF_INET, socket.SOCK_STREAM) - except socket.error, e: - indigo.server.log("Error creating socket: " + str(e), level=logging.ERROR) - self.handle_close() - # Now connect - try: - self.connect((self._host, self._port)) - except socket.gaierror, e: - indigo.server.log("\tError with address: " + str(e), level=logging.ERROR) - self.handle_close() - except socket.timeout, e: - indigo.server.log("\tSocket connection timed out: " + str(e), level=logging.ERROR) - self.handle_close() - except socket.error, e: - indigo.server.log("\tError opening connection: " + str(e), level=logging.ERROR) - self.handle_close() - else: - self.is_connected = True - indigo.server.log("\tConnected to B&O Gateway", level=logging.DEBUG) - - def handle_connect(self): - indigo.server.log("\tAttempting to Authenticate...", level=logging.WARNING) - self.send_cmd(self._pwd) - self.send_cmd("_MLLOG ONLINE") - - def handle_close(self): - indigo.server.log(self.name + ": Closing socket", level=logging.ERROR) - self.is_connected = False - self.close() - - def send_cmd(self, telegram): - try: - self.push(telegram + "\r\n") - except socket.timeout, e: - indigo.server.log("\tSocket connection timed out: " + str(e), level=logging.ERROR) - self.handle_close() - except socket.error, e: - indigo.server.log("Error sending data: " + str(e), level=logging.ERROR) - self.handle_close() - else: - self.last_sent = telegram - self.last_sent_at = time.time() - indigo.server.log(self.name + " >>-SENT--> : " + telegram, level=logging.INFO) - time.sleep(0.2) - - def _report(self, header, payload, message): - self.last_message = message - if self.messageCallBack: - self.messageCallBack(self.name, str(list(header)), str(list(payload)), message) - - def ping(self): - if self.debug: - indigo.server.log(self.name + " >>-SENT--> : Ping", level=logging.DEBUG) - self.push('\n') - - # ######################################################################################## - # ##### Utility functions - @staticmethod - def _hexbyte(byte): - resultstr = hex(byte) - if byte < 16: - resultstr = resultstr[:2] + "0" + resultstr[2] - return resultstr - - def _hexword(self, byte1, byte2): - resultstr = self._hexbyte(byte2) - resultstr = self._hexbyte(byte1) + resultstr[2:] - return resultstr - - def _dictsanitize(self, d, s): - result = d.get(s) - if result is None: - result = self._hexbyte(s) - return str(result) - - @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]: - return rev_dict.get(list(rev_dict)[i]) - - # ######################################################################################## - # ##### Decode Masterlink Protocol packet to a serializable dict - def _decode(self, telegram): - # Decode header - message = OrderedDict() - self._get_device_info(message, telegram) - if 'Device' not in message: - # If ML telegram has been matched to a Masterlink node in the devices list then the 'from_device' - # key is redundant - it will always be identical to the 'Device' key - message["from_device"] = self._dictsanitize(CONST.ml_device_dict, telegram[1]) - message["from_source"] = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[5]) - message["to_device"] = self._get_device_name(self._dictsanitize(CONST.ml_device_dict, telegram[0])) - message["to_source"] = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[4]) - 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"] = 'Unknown' - - if telegram[8] < 27: - c_trk = telegram[19] - else: - c_trk = telegram[36] * 256 + telegram[37] - - message["State_Update"]["nowPlayingDetails"] = OrderedDict( - [ - ("local_source", telegram[13]), - ("type", self._dictsanitize(CONST.ml_sourcekind_dict, telegram[22])), - ("channel_track", c_trk), - ("source_medium_position", self._hexword(telegram[18], telegram[17])), - ("picture_format", 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(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["State_Update"]["display_source"] = _s.rstrip() - - # extended source information - if message.get("payload_type") == "EXTENDED_SOURCE_INFORMATION": - message["State_Update"]["info_type"] = telegram[10] - _s = "" - for i in range(0, telegram[8] - 14): - _s = _s + chr(telegram[i + 24]) - message["State_Update"]["info_value"] = _s - - # beo4 command - if message.get("payload_type") == "BEO4_KEY": - source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[10]) - self._get_source_name(source, message) - message["State_Update"] = OrderedDict( - [ - ("source", source), - ("sourceID", telegram[10]), - ("source_type", self._get_type(CONST.ml_selectedsource_type_dict, telegram[10])), - ("command", self._dictsanitize(CONST.beo4_commanddict, telegram[11])) - ] - ) - - # audio track info long - if message.get("payload_type") == "TRACK_INFO_LONG": - message["State_Update"]["nowPlaying"] = 'Unknown' - message["State_Update"]["nowPlayingDetails"] = OrderedDict( - [ - ("type", self._get_type(CONST.ml_selectedsource_type_dict, telegram[11])), - ("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(message) - message["State_Update"]["state"] = self._dictsanitize(CONST.sourceactivitydict, telegram[13]) - - # video track info - if message.get("payload_type") == "VIDEO_TRACK_INFO": - message["State_Update"]["nowPlaying"] = 'Unknown' - message["State_Update"]["nowPlayingDetails"] = OrderedDict( - [ - ("source_type", self._get_type(CONST.ml_selectedsource_type_dict, telegram[13])), - ("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(message) - message["State_Update"]["state"] = self._dictsanitize(CONST.sourceactivitydict, telegram[14]) - - # 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] - message["State_Update"]["prev_source_type"] = self._get_type( - CONST.ml_selectedsource_type_dict, telegram[11]) - if len(telegram) > 18: - 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] - - # Current Source - 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]) - message["State_Update"]["state"] = 'Unknown' - else: - message["State_Update"]["subtype"] = "Undefined: " + self._hexbyte(telegram[9]) - - # goto source - if message.get("payload_type") == "GOTO_SOURCE": - message["State_Update"]["nowPlaying"] = 'Unknown' - message["State_Update"]["nowPlayingDetails"] = OrderedDict( - [ - ("source_type", self._get_type(CONST.ml_selectedsource_type_dict, telegram[11])), - ("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] - if telegram[12] not in [0, 255]: - self._get_channel_track(message) - # Device sending goto source command is playing - message["State_Update"]["state"] = 'Play' - - # remote request - if message.get("payload_type") == "MLGW_REMOTE_BEO4": - 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["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["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["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]) - - # request local audio source - if message.get("payload_type") == "PICTURE_AND_SOUND_STATUS": - message["State_Update"]["sound_status"] = OrderedDict( - [ - ("mute_status", self._dictsanitize(CONST.mlgw_soundstatusdict, telegram[10])), - ("speaker_mode", self._dictsanitize(CONST.mlgw_speakermodedict, telegram[11])), - # ("stereo_mode", self._dictsanitize(CONST.mlgw_stereoindicatordict, telegram[9])) - ] - ) - # message["State_Update"]["picture_status"] = OrderedDict() - - message['State_Update']['source'] = 'Unknown' - message['State_Update']['sourceName'] = 'Unknown' - message["State_Update"]["state"] = 'Unknown' - message["volume"] = int(telegram[12]) - - return message - - @staticmethod - def _get_device_info(message, telegram): - # Loop over the device list - for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'): - # Get properties - node_props = node.pluginProps - - # Skip netlink devices with no ml_id - if node_props['mlid'] == 'NA': - continue - - # identify if the mlid is a number or a text string - try: - ml_id = int(node_props['mlid'], base=16) - except ValueError: - # If it is a text mlid then loop over the dictionary and get the numeric key - for item in CONST.ml_device_dict.items(): - if item[1] == node_props['mlid']: - ml_id = int(item[0]) - - if ml_id == int(telegram[1]): # Match ML_ID - try: - message["Zone"] = node_props['zone'].upper() - except KeyError: - pass - message["Room"] = node_props['room'].upper() - message["Type"] = "AV RENDERER" - message["Device"] = node.name - break - - def _get_channel_track(self, message): - try: - node = indigo.devices[message['Device']] - # Get properties - node_props = node.pluginProps - source_name = message["State_Update"]["sourceName"].strip().replace(" ", "_") - if self.debug: - indigo.server.log('searching device ' + node.name + ' channel list for source ' + source_name, - level=logging.DEBUG) - if 'channels' in node_props['sources'][source_name]: - for channel in node_props['sources'][source_name]['channels']: - if self.debug: - indigo.server.log(source_name + " Channel " + channel[0][1:] + " = " + channel[1], - level=logging.DEBUG) - if int(channel[0][1:]) == int( - message["State_Update"]['nowPlayingDetails']["channel_track"]): - message["State_Update"]["nowPlaying"] = channel[1] - if self.debug: - indigo.server.log("Current Channel: " + channel[1], level=logging.DEBUG) - return - - # If source list exhausted then return Unknown - message["State_Update"]["nowPlaying"] = 'Unknown' - except KeyError: - message["State_Update"]["nowPlaying"] = 'Unknown' - - @staticmethod - def _get_device_name(dev): - for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'): - # Get properties - node_props = node.pluginProps - if node_props['mlid'] == dev: - return node.name - return dev - - @staticmethod - def _get_source_name(source, message): - if CONST.available_sources: - for src in CONST.available_sources: - if str(src[0]) == str(source): - message["State_Update"]["sourceName"] = src[1] - return - # If source list exhausted then return Unknown - message["State_Update"]["sourceName"] = 'Unknown' diff --git a/Server Plugin/Resources/MLCLI_CLIENT.pyc b/Server Plugin/Resources/MLCLI_CLIENT.pyc deleted file mode 100644 index b9b8d8d..0000000 Binary files a/Server Plugin/Resources/MLCLI_CLIENT.pyc and /dev/null differ diff --git a/Server Plugin/Resources/MLCONFIG.py b/Server Plugin/Resources/MLCONFIG.py deleted file mode 100644 index c1fc0bc..0000000 --- a/Server Plugin/Resources/MLCONFIG.py +++ /dev/null @@ -1,298 +0,0 @@ -import indigo -import asyncore -import json -import requests -import logging -from requests.auth import HTTPDigestAuth, HTTPBasicAuth -from collections import OrderedDict - -import Resources.CONSTANTS as CONST - - -class MLConfig: - - def __init__(self, host_address='blgw.local', user='admin', pwd='admin', debug=False): - self.debug =debug - - self._host = host_address - self._user = user - self._pwd = pwd - - self._download_data() - - def _download_data(self): - try: - indigo.server.log('Downloading configuration data from Gateway...', level=logging.WARNING) - 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.configure_mlgw(configurationdata) - except ValueError: - pass - - def configure_mlgw(self, data): - if "BeoGateway" not in indigo.devices.folders: - indigo.devices.folder.create("BeoGateway") - folder_id = indigo.devices.folders.getId("BeoGateway") - - # Check to see if any devices already exist to avoid duplication - _nodes = [] - for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'): - _nodes.append(int(node.address)) - - indigo.server.log('Processing Gateway configuration data...\n', level=logging.WARNING) - # Check to see if gateway device exists and create one if not - try: - gw = indigo.device.create( - protocol=indigo.kProtocol.Plugin, - name="Bang and Olufsen Gateway", - description="Automatically generated device for BeoGateway plugin:\n" - " - Please do not delete or rename!\n" - " - Editing device properties for advanced users only!", - deviceTypeId="BOGateway", - pluginId='uk.co.lukes_plugins.BeoGateway.plugin', - folder=folder_id, - address=1 - ) - except ValueError: - gw = indigo.devices['Bang and Olufsen Gateway'] - - try: - gateway_type = 'blgw' - gw.replacePluginPropsOnServer( - { - 'serial_no': data['sn'], - 'project': data['project'], - 'installer': str(data['installer']['name']), - 'contact': str(data['installer']['contact']), - 'isBLGW': 'BLGW' - } - ) - except KeyError: - gateway_type = 'mlgw' - gw.replacePluginPropsOnServer( - { - 'serial_no': data['sn'], - 'project': data['project'], - 'isBLGW': 'MLGW' - } - ) - - # Replace States - gw.updateStatesOnServer(CONST.gw_all_stb) - - # Loop over the config data to find the rooms, devices and sources in the installation - for zone in data["zones"]: - # Get rooms - if int(zone['number']) == 240: - continue - room = OrderedDict() - room['Room_Number'] = zone['number'] - 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']) - - # Get products - for product in zone["products"]: - device = OrderedDict() - - # Device identification - device['Device'] = str(product["name"]) - device['MLN'] = product["MLN"] - device['ML_ID'] = '' - try: - device['Serial_num'] = str(product["sn"]) - except KeyError: - device['Serial_num'] = 'NA' - - # 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 information - device['Sources'] = dict() - - for source in product["sources"]: - src_name = str(source["name"]).strip().replace(' ', '_') - device['Sources'][src_name] = dict() - for selectCmd in source["selectCmds"]: - if gateway_type == 'blgw': - # get source information from the BLGW config file - if str(source['sourceId']) == '': - source_id = self._srcdictsanitize(CONST.beo4_commanddict, source['selectID']).upper() - else: - source_id = str(source['sourceId'].split(':')[0]) - source_id = self._srcdictsanitize(CONST.blgw_srcdict, source_id).upper() - device['Sources'][src_name]['source'] = source_id - device['Sources'][src_name]['uniqueID'] = str(source['sourceId']) - else: - # MLGW config file is structured differently - source_id = self._srcdictsanitize(CONST.beo4_commanddict, source['selectID']).upper() - device['Sources'][src_name]['source'] = source_id - source_tuple = (str(source_id), str(source["name"])) - device['Sources'][src_name]['BR1_cmd'] = \ - dict([('command', int(selectCmd["cmd"])), ('unit', int(selectCmd["unit"]))]) - - # Establish the channel list for sources with favourites lists - if 'channels' in source: - device['Sources'][src_name]['channels'] = [] - for channel in source['channels']: - c_num = 'c' - if gateway_type == 'blgw': - num = channel['selectSEQ'][::2] - else: - num = channel['selectSEQ'][:-1] - for n in num: - c_num += str(n) - c = (c_num, str(channel['name'])) - device['Sources'][src_name]['channels'].append(c) - - if source_tuple not in CONST.available_sources: - CONST.available_sources.append(source_tuple) - - # Create indigo devices to represent the B&O AV renderers in the installation - if int(device['MLN']) not in _nodes: - if self.debug: - indigo.server.log("New Device! Creating Indigo Device " + device['Device'], - level=logging.DEBUG) - - node = indigo.device.create( - protocol=indigo.kProtocol.Plugin, - name=device['Device'], - description="Automatically generated device for BeoGateway plugin:\n" - "- Device data sourced from gateway config:\n" - "- Please do not delete or rename!\n" - "- Editing device properties for advanced users only!", - deviceTypeId="AVrenderer", - pluginId='uk.co.lukes_plugins.BeoGateway.plugin', - folder=folder_id, - address=device['MLN'], - props={ - 'serial_no': device['Serial_num'], - 'mlid': 'NA', - 'zone': room['Zone'], - 'room': device['Room'], - 'roomnum': device['Room_Number'], - 'sources': device['Sources'] - } - ) - - # Update the device states - node.updateStatesOnServer(CONST.standby_state) - node.updateStateImageOnServer(indigo.kStateImageSel.PowerOff) - else: - # if node already exists, update the properties in case they have been updated - for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'): - if int(node.address) == int(device['MLN']): - if self.debug: - indigo.server.log("Old Device! Updating Properties for " + device['Device'] + "\n", - level=logging.DEBUG) - # Update the name of the device - node.name = device['Device'] - node.description = "Automatically generated device for BeoGateway plugin:\n" \ - " - Device data sourced from gateway config:\n" \ - " - Please do not delete or rename!\n" \ - " - Editing device properties for advanced users only!" - node.replaceOnServer() - # Update the properties of the device - node_props = node.pluginProps - node_props.update( - { - 'serial_no': device['Serial_num'], - 'zone': room['Zone'], - 'room': device['Room'], - 'roomnum': device['Room_Number'], - 'sources': device['Sources'] - } - ) - node.replacePluginPropsOnServer(node_props) - - # Update the device states - node.stateListOrDisplayStateIdChanged() - node.updateStatesOnServer(CONST.standby_state) - node.updateStateImageOnServer(indigo.kStateImageSel.PowerOff) - indigo.device.moveToFolder(node.id, value=folder_id) - break - - # Keep track of the room data - CONST.rooms.append(room) - - # Report details of the configuration - n_devices = indigo.devices.len(filter="uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer") - 1 - indigo.server.log('Found ' + str(n_devices) + ' AV Renderers!', level=logging.DEBUG) - for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'): - indigo.server.log('\tMLN ' + str(node.address) + ': ' + str(node.name), level=logging.INFO) - indigo.server.log('\tFound ' + str(len(CONST.available_sources)) + ' Available Sources [Type, Name]:', - level=logging.DEBUG) - for i in range(len(CONST.available_sources)): - indigo.server.log('\t\t' + str(list(CONST.available_sources[i])), level=logging.INFO) - indigo.server.log('\tDone!\n', level=logging.DEBUG) - - @staticmethod - def get_masterlink_id(mlgw, mlcli): - # Identify the MasterLink ID of products - indigo.server.log("Finding MasterLink ID of products:", level=logging.WARNING) - if mlgw.is_connected and mlcli.is_connected: - for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'): - indigo.server.log("Finding MasterLink ID of product " + node.name, level=logging.WARNING) - # Ping the device with a light timeout to elicit a ML telegram containing its ML_ID - mlgw.send_beo4_cmd(int(node.address), - CONST.CMDS_DEST.get("AUDIO SOURCE"), - CONST.BEO4_CMDS.get("LIGHT TIMEOUT")) - node_props = node.pluginProps - if node_props['serial_no'] in [None, 'NA', '']: - # If this is a MasterLink product it has no serial number... - # loop to until expected response received from ML Command Line Interface - 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": - - if node_props['mlid'] == 'NA': - node_props['mlid'] = mlcli.last_message.get('to_device') - node_props['serial_no'] = 'NA' - node.replacePluginPropsOnServer(node_props) - - indigo.server.log("\tMasterLink ID of product " + - node.name + " is " + node_props['mlid'] + ".\n", - level=logging.DEBUG) - test = False - except KeyError: - asyncore.loop(count=1, timeout=0.2) - - else: - # If this is a NetLink product then it has a serial number and no ML_ID - indigo.server.log("\tNetworkLink ID of product " + node.name + " is " + - node_props['serial_no'] + ". No MasterLink ID assigned.\n", - level=logging.DEBUG) - - # ######################################################################################## - # Utility Functions - @staticmethod - def _srcdictsanitize(d, s): - result = d.get(s) - if result is None: - result = s - return str(result) diff --git a/Server Plugin/Resources/MLCONFIG.pyc b/Server Plugin/Resources/MLCONFIG.pyc deleted file mode 100644 index d8c3b25..0000000 Binary files a/Server Plugin/Resources/MLCONFIG.pyc and /dev/null differ diff --git a/Server Plugin/Resources/MLGW_CLIENT.py b/Server Plugin/Resources/MLGW_CLIENT.py deleted file mode 100644 index e10f46c..0000000 --- a/Server Plugin/Resources/MLGW_CLIENT.py +++ /dev/null @@ -1,431 +0,0 @@ -import indigo -import asynchat -import socket -import time -import logging -from collections import OrderedDict - -import Resources.CONSTANTS as CONST - - -class MLGWClient(asynchat.async_chat): - """Client to interact with a B&O Gateway via the MasterLink Gateway Protocol - http://mlgw.bang-olufsen.dk/source/documents/mlgw_2.24b/MlgwProto0240.pdf""" - - def __init__(self, host_address='blgw.local', port=9000, user='admin', pwd='admin', name='MLGW_Protocol', - debug=False, cb=None): - asynchat.async_chat.__init__(self) - - self.debug = debug - - self._host = host_address - self._port = int(port) - self._user = user - self._pwd = pwd - self.name = name - self.is_connected = False - - self._received_data = bytearray() - self.last_sent = '' - self.last_sent_at = time.time() - self.last_received = '' - self.last_received_at = time.time() - self.last_message = {} - - # Optional callback function - if cb: - self.messageCallBack = cb - else: - self.messageCallBack = None - - # 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 - self.client_connect() - - # ######################################################################################## - # ##### Client functions - def collect_incoming_data(self, data): - self.is_connected = True - 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 - - payload = bytearray() - for item in self._received_data[4:bit3 + 4]: - payload.append(item) - - if bit1 == 1 and len(self._received_data) == bit3 + 4 and bit4 == 0: - self.found_terminator(bit2, payload) - else: - if self.debug: - indigo.server.log("Incomplete Telegram Received: " + str(list(self._received_data)) + " - Ignoring!\n", - level=logging.ERROR) - self._received_data = "" - - def found_terminator(self, msg_type, payload): - self.last_received = str(list(self._received_data)) - self.last_received_at = time.time() - - header = self._received_data[0:4] - self._received_data = "" - self._decode(msg_type, header, payload) - - def _decode(self, msg_type, header, payload): - message = OrderedDict() - payload_type = self._dictsanitize(CONST.mlgw_payloadtypedict, msg_type) - - if payload_type == "MLGW virtual button event": - virtual_btn = payload[0] - if len(payload) < 1: - virtual_action = self._getvirtualactionstr(0x01) - else: - virtual_action = self._getvirtualactionstr(payload[1]) - - message["payload_type"] = payload_type - message["button"] = virtual_btn - message["action"] = virtual_action - - elif payload_type == "Login status": - if payload == 0: - indigo.server.log("\tAuthentication Failed: Incorrect Password", level=logging.ERROR) - self.handle_close() - message['Connected'] = "False" - return - else: - indigo.server.log("\tLogin successful to " + self._host, level=logging.DEBUG) - self.is_connected = True - message["payload_type"] = payload_type - message['Connected'] = "True" - self.get_serial() - - elif payload_type == "Pong": - self.is_connected = True - message = OrderedDict([('payload_type', 'Pong'), ('State_Update', dict([('CONNECTION', 'Online')]))]) - - elif payload_type == "Serial Number": - sn = '' - for c in payload: - sn += chr(c) - message["payload_type"] = payload_type - message['serial_Num'] = sn - - elif payload_type == "Source Status": - self._get_device_info(message, payload) - message["payload_type"] = payload_type - message["MLN"] = payload[0] - message["State_Update"] = OrderedDict() - message["State_Update"]["nowPlaying"] = 'Unknown' - message["State_Update"]["nowPlayingDetails"] = OrderedDict( - [ - ("channel_track", self._hexword(payload[4], payload[5])), - ("source_medium_position", self._hexword(payload[2], payload[3])), - ("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": - self._get_device_info(message, payload) - message["payload_type"] = payload_type - message["MLN"] = payload[0] - message["State_Update"] = OrderedDict() - message["State_Update"]["sound_status"] = OrderedDict( - [ - ("mute_status", self._getdictstr(CONST.mlgw_soundstatusdict, payload[1])), - ("speaker_mode", self._getdictstr(CONST.mlgw_speakermodedict, payload[2])), - ("stereo_mode", self._getdictstr(CONST.mlgw_stereoindicatordict, payload[9])), - ] - ) - message["State_Update"]["picture_status"] = OrderedDict( - [ - ("screen1_mute", self._getdictstr(CONST.mlgw_screenmutedict, payload[4])), - ("screen1_active", self._getdictstr(CONST.mlgw_screenactivedict, payload[5])), - ("screen2_mute", self._getdictstr(CONST.mlgw_screenmutedict, payload[6])), - ("screen2_active", self._getdictstr(CONST.mlgw_screenactivedict, payload[7])), - ("cinema_mode", self._getdictstr(CONST.mlgw_cinemamodedict, payload[8])), - ] - ) - message["State_Update"]["state"] = 'Unknown' - message["volume"] = int(payload[3]) - - elif payload_type == "All standby notification": - 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 room['Room_Number'] == payload[0]: - 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' - message["payload_type"] = payload_type - message["room_number"] = str(payload[0]) - message["command"] = self._getbeo4commandstr(payload[2]) - - if message != '': - self._report(header, payload, message) - - def client_connect(self): - indigo.server.log('Connecting to host at ' + self._host + ', port ' + str(self._port), level=logging.WARNING) - self.set_terminator(b'\r\n') - # Create the socket - try: - self.create_socket(socket.AF_INET, socket.SOCK_STREAM) - except socket.error, e: - indigo.server.log("Error creating socket: " + str(e), level=logging.ERROR) - self.handle_close() - # Now connect - try: - self.connect((self._host, self._port)) - except socket.gaierror, e: - indigo.server.log("\tError with address: " + str(e), level=logging.ERROR) - self.handle_close() - except socket.timeout, e: - indigo.server.log("\tSocket connection timed out: " + str(e), level=logging.ERROR) - self.handle_close() - except socket.error, e: - indigo.server.log("\tError opening connection: " + str(e), level=logging.ERROR) - self.handle_close() - else: - self.is_connected = True - indigo.server.log("\tConnected to B&O Gateway", level=logging.DEBUG) - - def handle_connect(self): - login = [] - for c in self._user: - login.append(c) - login.append(0x00) - for c in self._pwd: - login.append(c) - - indigo.server.log("\tAttempting to Authenticate...", level=logging.WARNING) - self._send_cmd(CONST.MLGW_PL.get("LOGIN REQUEST"), login) - - def handle_close(self): - indigo.server.log(self.name + ": Closing socket", level=logging.ERROR) - self.is_connected = False - self.close() - - def _report(self, header, payload, message): - self.last_message = message - if self.messageCallBack: - self.messageCallBack(self.name, str(list(header)), str(list(payload)), message) - - # ######################################################################################## - # ##### mlgw send functions - - # send_cmd command to mlgw - def _send_cmd(self, msg_type, 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.timeout, e: - indigo.server.log("\tSocket connection to timed out: " + str(e), level=logging.ERROR) - self.handle_close() - except socket.error, e: - indigo.server.log("Error sending data: " + str(e), level=logging.ERROR) - self.handle_close() - else: - self.last_sent = str(bytearray(telegram)) - self.last_sent_at = time.time() - if msg_type != CONST.MLGW_PL.get("PING"): - indigo.server.log( - self.name + " >>-SENT--> " - + self._getpayloadtypestr(msg_type) - + ": " - + str(list(telegram)), - level=logging.INFO) - else: - if self.debug: - indigo.server.log( - self.name + " >>-SENT--> " - + self._getpayloadtypestr(msg_type) - + ": " - + str(list(telegram)), - level=logging.DEBUG - ) - - # Sleep to allow msg to arrive - time.sleep(0.2) - - # Ping the gateway - def ping(self): - self._send_cmd(CONST.MLGW_PL.get("PING"), "") - - # 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"), "") - - # send_cmd Beo4 command to mlgw - def send_beo4_cmd(self, mln, dest, cmd, sec_source=0x00, link=0x00): - 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 - def send_beoremoteone_cmd(self, mln, cmd, network_bit=0x00): - 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 - def send_beoremoteone_select_source(self, mln, cmd, unit, network_bit=0x00): - payload = [ - mln, # byte[0] MLN - cmd, # byte[1] Beoremote One Command - unit, # byte[2] Unit - 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) - - def send_virtualbutton(self, button, action): - payload = [ - button, # byte[0] Button number - action] # byte[1] Action ID - self._send_cmd(CONST.MLGW_PL.get("MLGW VIRTUAL BUTTON EVENT"), payload) - - # ######################################################################################## - # ##### Utility functions - - @staticmethod - def _hexbyte(byte): - resultstr = hex(byte) - if byte < 16: - resultstr = resultstr[:2] + "0" + resultstr[2] - return resultstr - - def _hexword(self, byte1, byte2): - resultstr = self._hexbyte(byte2) - resultstr = self._hexbyte(byte1) + resultstr[2:] - return resultstr - - def _dictsanitize(self, d, s): - result = d.get(s) - 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 - def _getpayloadtypestr(self, payloadtype): - result = CONST.mlgw_payloadtypedict.get(payloadtype) - if result is None: - 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) - return result - - def _getvirtualactionstr(self, action): - 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 is None: - 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) - return result - - def _getdictstr(self, mydict, mykey): - result = mydict.get(mykey) - if result is None: - result = self._hexbyte(mykey) - 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] - return - # If source list exhausted then return Unknown - message["State_Update"]["sourceName"] = 'Unknown' - - @staticmethod - def _get_device_info(message, payload): - # Loop over the device list - for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'): - # Get properties - node_props = node.pluginProps - - if int(node.address) == int(payload[0]): # Match MLN - try: - message["Zone"] = node_props['zone'].upper() - except KeyError: - pass - message["Room"] = node_props['room'].upper() - message["Type"] = "AV RENDERER" - message["Device"] = node.name - break - - def _get_channel_track(self, message): - try: - node = indigo.devices[message['Device']] - # Get properties - node_props = node.pluginProps - source_name = message["State_Update"]["sourceName"].strip().replace(" ", "_") - if self.debug: - indigo.server.log('searching device ' + node.name + ' channel list for source ' + source_name, - level=logging.DEBUG) - if 'channels' in node_props['sources'][source_name]: - for channel in node_props['sources'][source_name]['channels']: - if self.debug: - indigo.server.log(source_name + " Channel " + channel[0][1:] + " = " + channel[1], - level=logging.DEBUG) - if int(channel[0][1:]) == int( - message["State_Update"]['nowPlayingDetails']["channel_track"]): - message["State_Update"]["nowPlaying"] = channel[1] - if self.debug: - indigo.server.log("Current Channel: " + channel[1], level=logging.DEBUG) - return - - # If source list exhausted then return Unknown - message["State_Update"]["nowPlaying"] = 'Unknown' - except KeyError: - message["State_Update"]["nowPlaying"] = 'Unknown' diff --git a/Server Plugin/Resources/MLGW_CLIENT.pyc b/Server Plugin/Resources/MLGW_CLIENT.pyc deleted file mode 100644 index d6c86c8..0000000 Binary files a/Server Plugin/Resources/MLGW_CLIENT.pyc and /dev/null differ diff --git a/Server Plugin/Resources/MLtn_CLIENT.py b/Server Plugin/Resources/MLtn_CLIENT.py deleted file mode 100644 index de1361d..0000000 --- a/Server Plugin/Resources/MLtn_CLIENT.py +++ /dev/null @@ -1,370 +0,0 @@ -import indigo -import asynchat -import socket -import time -import logging -from collections import OrderedDict - -import Resources.CONSTANTS as CONST - - -class MLtnClient(asynchat.async_chat): - """Client to monitor network activity on a Masterlink Gateway via the telnet monitor""" - def __init__(self, host_address='mlgw.local', port=23, user='admin', pwd='admin', name='MLGW_HIP', - debug=False, cb=None): - asynchat.async_chat.__init__(self) - - self.debug = debug - - self._host = host_address - self._port = int(port) - self._user = user - self._pwd = pwd - self.name = name - self.is_connected = False - - self._i = 0 - self._header_lines = 4 - self._received_data = '' - self.last_sent = '' - self.last_sent_at = time.time() - self.last_received = '' - self.last_received_at = time.time() - self.last_message = {} - - self.isBLGW = False - - # Optional callback function - if cb: - self.messageCallBack = cb - else: - self.messageCallBack = None - - # ######################################################################################## - # ##### Open Socket and connect to B&O Gateway - self.client_connect() - - # ######################################################################################## - # ##### Client functions - def collect_incoming_data(self, data): - self._received_data += data - - def found_terminator(self): - self.last_received = self._received_data - self.last_received_at = time.time() - - items = self._received_data.split(' ') - - if self._i <= self._header_lines: - self._i += 1 - if self._received_data[0:4] != "MLGW": - self.isBLGW = True - if self._i == self._header_lines - 1: - if self.debug: - indigo.server.log("\t" + self._received_data, level=logging.DEBUG) - if self._received_data == 'incorrect password': - self.handle_close() - - else: - try: - self._decode(items) - except IndexError: - pass - - self._received_data = "" - - def _decode(self, items): - header = items[3][:-1] - telegram_starts = len(''.join(items[:4])) + 4 - 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) - - if header == 'resource_found': - message['State_Update'] = telegram[0] - - if header == 'action_executed': - message = self._decode_action(telegram, message) - - if header == 'command_executed': - message = self._decode_command(telegram, message) - - if header == 'macro_fired': - message['Zone'] = telegram[0].upper() - message['Room'] = telegram[1].upper() - message['Macro_Name'] = telegram[3] - - if header == 'trigger_fired': - message = self._decode_trigger(telegram, message) - - self._report(header, telegram, message) - - def _decode_ip(self, telegram, message): - if ''.join(telegram).split(':')[0] == 'Integration Protocol login': - chars = ''.join(telegram).split(':')[1][2:].split(' ') - message['Payload'] = '' - for c in chars: - if c == '0x0': - message['Payload'] += '0' - else: - message['Payload'] += chr(int(c, base=16)) - - if ''.join(telegram).split(':')[0] == 'Integration Protocol': - if ''.join(telegram).split(':')[1] == ' processed serial number request': - message['Payload'] = 'processed serial number request' - else: - s = ''.join(telegram).split(' ') - message['Type'] = 'Send Beo4 Command' - message[s[5]] = s[6] - message['Payload'] = OrderedDict() - for k in range(10, len(s)): - if k == 10: - message['Payload']['to_MLN'] = int(s[k], base=16) - if k == 11: - message['Payload']['Destination'] = self._dictsanitize( - CONST.destselectordict, int(s[k], base=16)) - if k == 12: - message['Payload']['Command'] = self._dictsanitize( - 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)) - if k == 14: - message['Payload']['Link'] = self._dictsanitize( - 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)) - return message - - @staticmethod - def _decode_action(telegram, message): - message['Zone'] = telegram[0].upper() - message['Room'] = telegram[1].upper() - message['Type'] = telegram[2].upper() - message['Device'] = telegram[3] - message['State_Update'] = OrderedDict() - if message.get('Type') == 'BUTTON': - if telegram[4].split('=')[0] == '_SET STATE?STATE': - message['State_Update']['STATE'] = telegram[4].split('=')[1] - if message['State_Update'].get('STATE') == '0': - message['State_Update']['Status'] = "Off" - else: - message['State_Update']['Status'] = "On" - else: - message['State_Update']['STATE'] = telegram[4] - - if message.get('Type') == 'DIMMER': # e.g. DownstairsHallwayDIMMERWall LightSTATE_UPDATE?LEVEL=5 - if telegram[4].split('=')[0] == '_SET STATE?LEVEL': - message['State_Update']['LEVEL'] = telegram[4].split('=')[1] - if message['State_Update'].get('LEVEL') == '0': - message['State_Update']['Status'] = "Off" - else: - message['State_Update']['Status'] = "On" - else: - message['State_Update']['STATE'] = telegram[4] - return message - - @staticmethod - def _decode_command(telegram, message): - message['Zone'] = telegram[0].upper() - message['Room'] = telegram[1].upper() - message['Type'] = telegram[2].upper() - message['Device'] = telegram[3] - message['State_Update'] = OrderedDict() - if message.get('Type') == 'BUTTON': - if telegram[4].split('=')[0] == '_SET STATE?STATE': - message['State_Update']['STATE'] = telegram[4].split('=')[1] - if message['State_Update'].get('STATE') == '0': - message['State_Update']['Status'] = "Off" - else: - message['State_Update']['Status'] = "On" - else: - message['State_Update']['STATE'] = telegram[4] - - if message.get('Type') == 'DIMMER': - if telegram[4].split('=')[0] == '_SET STATE?LEVEL': - message['State_Update']['LEVEL'] = telegram[4].split('=')[1] - if message['State_Update'].get('LEVEL') == '0': - message['State_Update']['Status'] = "Off" - else: - message['State_Update']['Status'] = "On" - else: - message['State_Update']['STATE'] = telegram[4] - return message - - def _decode_trigger(self, telegram, message): - message['Zone'] = telegram[0].upper() - message['Room'] = telegram[1].upper() - message['Type'] = telegram[2].upper() - message['Device'] = telegram[3] - message['State_Update'] = OrderedDict() - - if message.get('Type') == 'BUTTON': - if telegram[4].split('=')[0] == 'STATE_UPDATE?STATE': - message['State_Update']['STATE'] = telegram[4].split('=')[1] - if message['State_Update'].get('STATE') == '0': - message['State_Update']['Status'] = "Off" - else: - message['State_Update']['Status'] = "On" - else: - message['State_Update']['STATE'] = telegram[4] - - if message.get('Type') == 'DIMMER': - if telegram[4].split('=')[0] == 'STATE_UPDATE?LEVEL': - message['State_Update']['LEVEL'] = telegram[4].split('=')[1] - if message['State_Update'].get('LEVEL') == '0': - message['State_Update']['Status'] = "Off" - else: - message['State_Update']['Status'] = "On" - else: - message['State_Update']['STATE'] = telegram[4] - - if message.get('Type') == 'AV RENDERER': - if telegram[4][:5] == 'Light': - state = telegram[4][6:].split('&') - message['State_Update']['type'] = 'Light Command' - 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, - int(s[13:].strip())).title() - elif telegram[4][:7] == 'Control': - state = telegram[4][6:].split('&') - message['State_Update']['type'] = 'Control Command' - 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, - int(s[13:].strip())).title() - elif telegram[4] == 'All standby': - message['State_Update']['command'] = telegram[4] - - else: - state = telegram[4][13:].split('&') - 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'][s.split('=')[0]] = s.split('=')[1] - elif s.split('=')[0] == 'nowPlayingDetails': - message['State_Update']['nowPlayingDetails'] = OrderedDict() - details = s.split('=')[1].split(';') - if len(details) > 1: - for d in details: - if d.split(':')[0].strip() in ['track number', 'channel number']: - message['State_Update']['nowPlayingDetails']['channel_track'] \ - = d.split(':')[1].strip() - else: - message['State_Update']['nowPlayingDetails'][d.split(':')[0].strip()] \ - = d.split(':')[1].strip() - else: - message['State_Update'][s.split('=')[0]] = s.split('=')[1] - return message - - def _report(self, header, telegram, message): - self.last_message = message - if self.messageCallBack: - self.messageCallBack(self.name, ''.join(header).upper(), ''.join(telegram), message) - - def client_connect(self): - indigo.server.log('Connecting to host at ' + self._host + ', port ' + str(self._port), level=logging.WARNING) - self.set_terminator(b'\r\n') - # Create the socket - try: - self.create_socket(socket.AF_INET, socket.SOCK_STREAM) - except socket.error, e: - indigo.server.log("Error creating socket: " + str(e), level=logging.ERROR) - self.handle_close() - # Now connect - try: - self.connect((self._host, self._port)) - except socket.gaierror, e: - indigo.server.log("\tError with address: " + str(e), level=logging.ERROR) - self.handle_close() - except socket.timeout, e: - indigo.server.log("\tSocket connection timed out: " + str(e), level=logging.ERROR) - self.handle_close() - except socket.error, e: - indigo.server.log("\tError opening connection: " + str(e), level=logging.ERROR) - self.handle_close() - else: - self.is_connected = True - indigo.server.log("\tConnected to B&O Gateway", level=logging.DEBUG) - - def handle_connect(self): - indigo.server.log("\tAttempting to Authenticate...", level=logging.WARNING) - self._send_cmd(self._pwd) - self._send_cmd("MONITOR") - - def handle_close(self): - indigo.server.log(self.name + ": Closing socket", level=logging.ERROR) - self.is_connected = False - self.close() - - def _send_cmd(self, telegram): - try: - self.push(telegram + "\r\n") - except socket.timeout, e: - indigo.server.log("\tSocket connection timed out: " + str(e), level=logging.ERROR) - self.handle_close() - except socket.error, e: - indigo.server.log("Error sending data: " + str(e), level=logging.ERROR) - self.handle_close() - else: - self.last_sent = telegram - self.last_sent_at = time.time() - indigo.server.log(self.name + " >>-SENT--> : " + telegram, level=logging.INFO) - time.sleep(0.2) - - def toggle_events(self): - try: - self.push('e') - except socket.error, e: - indigo.server.log("Error sending data: " + str(e), level=logging.ERROR) - self.handle_close() - - def toggle_macros(self): - try: - self.push('m') - except socket.error, e: - indigo.server.log("Error sending data: " + str(e), level=logging.ERROR) - self.handle_close() - - def toggle_commands(self): - try: - self.push('c') - except socket.error, e: - indigo.server.log("Error sending data: " + str(e), level=logging.ERROR) - self.handle_close() - - def ping(self): - self._send_cmd('') - - # ######################################################################################## - # ##### Utility functions - @staticmethod - def _hexbyte(byte): - resultstr = hex(byte) - if byte < 16: - resultstr = resultstr[:2] + "0" + resultstr[2] - return resultstr - - def _dictsanitize(self, d, s): - result = d.get(s) - if result is None: - result = "UNKNOWN (type=" + self._hexbyte(s) + ")" - return str(result) - - @staticmethod - def _srcdictsanitize(d, s): - result = d.get(s) - if result is None: - result = s - return str(result) diff --git a/Server Plugin/Resources/MLtn_CLIENT.pyc b/Server Plugin/Resources/MLtn_CLIENT.pyc deleted file mode 100644 index 7be73e3..0000000 Binary files a/Server Plugin/Resources/MLtn_CLIENT.pyc and /dev/null differ diff --git a/Server Plugin/Resources/Notify.app/Contents/Info.plist b/Server Plugin/Resources/Notify.app/Contents/Info.plist deleted file mode 100644 index 429fb28..0000000 --- a/Server Plugin/Resources/Notify.app/Contents/Info.plist +++ /dev/null @@ -1,78 +0,0 @@ - - - - - CFBundleAllowMixedLocalizations - - CFBundleDevelopmentRegion - en - CFBundleExecutable - applet - CFBundleIconFile - applet - CFBundleIdentifier - com.apple.ScriptEditor.id.Notify - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Notify - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - aplt - LSMinimumSystemVersionByArchitecture - - x86_64 - 10.6 - - LSRequiresCarbon - - NSAppleEventsUsageDescription - This script needs to control other applications to run. - NSAppleMusicUsageDescription - This script needs access to your music to run. - NSCalendarsUsageDescription - This script needs access to your calendars to run. - NSCameraUsageDescription - This script needs access to your camera to run. - NSContactsUsageDescription - This script needs access to your contacts to run. - NSHomeKitUsageDescription - This script needs access to your HomeKit Home to run. - NSMicrophoneUsageDescription - This script needs access to your microphone to run. - NSPhotoLibraryUsageDescription - This script needs access to your photos to run. - NSRemindersUsageDescription - This script needs access to your reminders to run. - NSSiriUsageDescription - This script needs access to Siri to run. - NSSystemAdministrationUsageDescription - This script needs access to administer this system to run. - OSAAppletShowStartupScreen - - OSAAppletStayOpen - - WindowState - - bundleDividerCollapsed - - bundlePositionOfDivider - 0.0 - dividerCollapsed - - eventLogLevel - 2 - name - ScriptWindowState - positionOfDivider - 419 - savedFrame - 523 274 700 678 0 0 1680 1025 - selectedTab - description - - - diff --git a/Server Plugin/Resources/Notify.app/Contents/MacOS/applet b/Server Plugin/Resources/Notify.app/Contents/MacOS/applet deleted file mode 100644 index 7927912..0000000 Binary files a/Server Plugin/Resources/Notify.app/Contents/MacOS/applet and /dev/null differ diff --git a/Server Plugin/Resources/Notify.app/Contents/PkgInfo b/Server Plugin/Resources/Notify.app/Contents/PkgInfo deleted file mode 100644 index 3253614..0000000 --- a/Server Plugin/Resources/Notify.app/Contents/PkgInfo +++ /dev/null @@ -1 +0,0 @@ -APPLaplt \ No newline at end of file diff --git a/Server Plugin/Resources/Notify.app/Contents/Resources/Scripts/main.scpt b/Server Plugin/Resources/Notify.app/Contents/Resources/Scripts/main.scpt deleted file mode 100644 index c478d35..0000000 Binary files a/Server Plugin/Resources/Notify.app/Contents/Resources/Scripts/main.scpt and /dev/null differ diff --git a/Server Plugin/Resources/Notify.app/Contents/Resources/applet.icns b/Server Plugin/Resources/Notify.app/Contents/Resources/applet.icns deleted file mode 100644 index 33f6b86..0000000 Binary files a/Server Plugin/Resources/Notify.app/Contents/Resources/applet.icns and /dev/null differ diff --git a/Server Plugin/Resources/Notify.app/Contents/Resources/applet.rsrc b/Server Plugin/Resources/Notify.app/Contents/Resources/applet.rsrc deleted file mode 100644 index 9471572..0000000 Binary files a/Server Plugin/Resources/Notify.app/Contents/Resources/applet.rsrc and /dev/null differ diff --git a/Server Plugin/Resources/Notify.app/Contents/Resources/description.rtfd/TXT.rtf b/Server Plugin/Resources/Notify.app/Contents/Resources/description.rtfd/TXT.rtf deleted file mode 100644 index 4d315a3..0000000 --- a/Server Plugin/Resources/Notify.app/Contents/Resources/description.rtfd/TXT.rtf +++ /dev/null @@ -1,5 +0,0 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2636 -\cocoatextscaling0\cocoaplatform0{\fonttbl} -{\colortbl;\red255\green255\blue255;} -{\*\expandedcolortbl;;} -} \ No newline at end of file diff --git a/Server Plugin/Resources/Scripts/blue.scpt b/Server Plugin/Resources/Scripts/blue.scpt deleted file mode 100644 index c68227c..0000000 Binary files a/Server Plugin/Resources/Scripts/blue.scpt and /dev/null differ diff --git a/Server Plugin/Resources/Scripts/green.scpt b/Server Plugin/Resources/Scripts/green.scpt deleted file mode 100644 index fb4aa4a..0000000 Binary files a/Server Plugin/Resources/Scripts/green.scpt and /dev/null differ diff --git a/Server Plugin/Resources/Scripts/red.scpt b/Server Plugin/Resources/Scripts/red.scpt deleted file mode 100644 index 8fd8a2e..0000000 Binary files a/Server Plugin/Resources/Scripts/red.scpt and /dev/null differ diff --git a/Server Plugin/Resources/Scripts/yellow.scpt b/Server Plugin/Resources/Scripts/yellow.scpt deleted file mode 100644 index 2f082f5..0000000 Binary files a/Server Plugin/Resources/Scripts/yellow.scpt and /dev/null differ diff --git a/Server Plugin/Resources/__init__.py b/Server Plugin/Resources/__init__.py deleted file mode 100644 index a6131c1..0000000 --- a/Server Plugin/Resources/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# init diff --git a/Server Plugin/Resources/__init__.pyc b/Server Plugin/Resources/__init__.pyc deleted file mode 100644 index c3063b0..0000000 Binary files a/Server Plugin/Resources/__init__.pyc and /dev/null differ diff --git a/Server Plugin/plugin.py b/Server Plugin/plugin.py deleted file mode 100644 index 184cfcf..0000000 --- a/Server Plugin/plugin.py +++ /dev/null @@ -1,1277 +0,0 @@ -import indigo -import asyncore -import json -import time -import logging -from datetime import datetime - -import Resources.CONSTANTS as CONST -import Resources.MLCONFIG as MLCONFIG -import Resources.MLGW_CLIENT as MLGW -import Resources.MLCLI_CLIENT as MLCLI -import Resources.BLHIP_CLIENT as BLHIP -import Resources.MLtn_CLIENT as MLtn -import Resources.ASBridge as ASBridge - - -class Plugin(indigo.PluginBase): - - def __init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs): - indigo.PluginBase.__init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs) - - self.pollinterval = 595 # ping sent out at 9:55, response evaluated at 10:00 - self.trackmode = self.pluginPrefs.get("trackMode") - self.verbose = self.pluginPrefs.get("verboseMode") - self.notifymode = self.pluginPrefs.get("notifyMode") - self.debug = self.pluginPrefs.get("debugMode") - self.default_audio_source = self.pluginPrefs.get("defaultAudio") - self.itunes_control = self.pluginPrefs.get("iTunesControl") - self.itunes_source = self.pluginPrefs.get("iTunesSource") - self.goto_flag = datetime(1982, 04, 01, 13, 30, 00, 342380) - - self.triggers = [] - - self.host = str(self.pluginPrefs.get('address')).encode('ascii') - self.port = [int(self.pluginPrefs.get('mlgw_port')), - int(self.pluginPrefs.get('hip_port')), - 23] # Telnet port - 23 - self.user = str(self.pluginPrefs.get('userID')).encode('ascii') - self.pwd = str(self.pluginPrefs.get('password')).encode('ascii') - - # Instantiate an AppleScriptBridge MusicController for N.MUSIC control of apple Music - self.iTunes = ASBridge.MusicController() - - def triggerStartProcessing(self, trigger): - self.triggers.append(trigger) - - def getDeviceStateList(self, dev): - stateList = indigo.PluginBase.getDeviceStateList(self, dev) - - if stateList is not None: - if dev.deviceTypeId in self.devicesTypeDict and dev.deviceTypeId == u"AVrenderer": - # Add dynamic states onto stateList for devices of type AV renderer - try: - sources = dev.pluginProps['sources'] - for source_name in sources: - stateList.append( - { - "Disabled": False, - "Key": "source." + source_name, - "StateKey": sources[source_name]['source'], - "StateLabel": "Source is " + source_name, - "TriggerLabel": "Source is " + source_name, - "Type": 50 - } - ) - except KeyError: - indigo.server.log("Device " + dev.name + " does not have state key 'sources'\n", - level=logging.WARNING) - pass - return stateList - - def deviceStartComm(self, dev): - dev.stateListOrDisplayStateIdChanged() - - def __del__(self): - indigo.PluginBase.__del__(self) - - # ######################################################################################## - # ##### Indigo UI menu constructors - @staticmethod - def zonelistgenerator(filter="", valuesDict=None, typeId="", targetId=0): - myarray = [("*", "All"), ("Main", "Main")] - for room in CONST.rooms: - if (room['Zone'], room['Zone']) not in myarray: - myarray.append((room['Zone'], room['Zone'])) - return myarray - - @staticmethod - def roomlistgenerator(filter="", valuesDict=None, typeId="", targetId=0): - myarray = [("99", "Any")] - for room in CONST.rooms: - myarray.append((room['Room_Number'], room['Room_Name'])) - return myarray - - @staticmethod - def roomlistgenerator2(filter="", valuesDict=None, typeId="", targetId=0): - myarray = [("*", "All"), ("global", "global")] - for room in CONST.rooms: - myarray.append((room['Room_Name'], room['Room_Name'])) - return myarray - - @staticmethod - def keylistgenerator(filter="", valuesDict=None, typeId="", targetId=0): - myarray = [] - for item in CONST.beo4_commanddict.items(): - myarray.append(item) - return myarray - - @staticmethod - def keylistgenerator2(filter="", valuesDict=None, typeId="", targetId=0): - myarray = [] - for item in CONST.beoremoteone_keydict.items(): - myarray.append(item) - return myarray - - @staticmethod - def beo4sourcelistgenerator(filter="", valuesDict=None, typeId="", targetId=0): - myarray = [] - for item in CONST.beo4_srcdict.items(): - myarray.append(item) - return myarray - - @staticmethod - def beo4sourcelistgenerator2(filter="", valuesDict=None, typeId="", targetId=0): - myarray = [('Any Source', 'Any Source'), ('Any Audio', 'Any Audio'), ('Any Video', 'Any Video')] - for item in CONST.beo4_srcdict.items(): - myarray.append(item) - return myarray - - @staticmethod - def br1sourcelistgenerator(filter="", valuesDict=None, typeId="", targetId=0): - myarray = [] - for item in CONST.available_sources: - myarray.append(item) - return myarray - - @staticmethod - def destinationlistgenerator(filter="", valuesDict=None, typeId="", targetId=0): - myarray = [] - for item in CONST.destselectordict.items(): - myarray.append(item) - return myarray - - @staticmethod - def srcactivitylistgenerator(filter="", valuesDict=None, typeId="", targetId=0): - myarray = [(0x00, "Unknown")] - for item in CONST.sourceactivitydict.items(): - if item not in myarray: - myarray.append(item) - return myarray - - @staticmethod - def hiptypelistgenerator(filter="", valuesDict=None, typeId="", targetId=0): - myarray = [] - for item in CONST.blgw_devtypes.items(): - myarray.append(item) - return myarray - - # ######################################################################################## - # ##### Indigo UI Prefs - def set_login(self, ui): - # If LogIn data is updated in config, update the values in the plugin - self.user = str(ui.get('userID')).encode('ascii') - self.pwd = str(ui.get('password')).encode('ascii') - indigo.server.log("BeoGateway device login details updated!", level=logging.DEBUG) - - def set_gateway(self, ui): - # If gateway network address data is updated in config, update the values in the plugin - self.host = str(ui.get('address')).encode('ascii') - self.port = [int(ui.get('mlgw_port')), - int(ui.get('hip_port')), - 23] # Telnet port - 23 - indigo.server.log("BeoGateway device network address details updated!", level=logging.DEBUG) - - def set_trackmode(self, ui): - # If Track Mode setting is updated in config, update the value in the plugin - self.trackmode = ui.get("trackMode") - indigo.server.log("Track reporting set to " + str(self.trackmode), level=logging.DEBUG) - - def set_verbose(self, ui): - # If Verbose Mode setting is updated in config, update the value in the plugin - self.verbose = ui.get("verboseMode") - indigo.server.log("Verbose Mode set to " + str(self.verbose), level=logging.DEBUG) - - def set_notifymode(self, ui): - # If Notify Mode setting is updated in config, update the value in the plugin - self.notifymode = ui.get("notifyMode") - indigo.server.log("Verbose Mode set to " + str(self.notifymode), level=logging.DEBUG) - - def set_debug(self, ui): - # If Debug Mode setting is updated in config, update the value in the plugin - self.debug = ui.get("debugMode") - - # Set the debug flag for the clients - self.mlcli.debug = self.debug - self.mlgw.debug = self.debug - if self.mlcli.isBLGW: - self.blgw.debug = self.debug - else: - self.mltn.debug = self.debug - - # Report the debug flag change - indigo.server.log("Debug Mode set to " + str(self.debug), level=logging.DEBUG) - - def set_music_control(self, ui): - # If Apple Music Control setting is updated in config, update the value in the plugin - self.itunes_control = ui.get("iTunesControl") - self.itunes_source = ui.get("iTunesSource") - - if self.itunes_control: - indigo.server.log("Apple Music Control enabled on source: " + str(self.itunes_source), level=logging.DEBUG) - else: - indigo.server.log("Apple Music Control set to " + str(self.itunes_control), level=logging.DEBUG) - - def set_default_audio(self, ui): - # Define default audio source for AVrenderers - self.default_audio_source = ui.get("defaultAudio") - indigo.server.log("Default Audio Source set to " + str(self.default_audio_source), level=logging.DEBUG) - - # ######################################################################################## - # ##### Indigo UI Actions - def send_beo_4key(self, action, device): - device_id = int(device.address) - key_code = int(action.props.get("keyCode", 0)) - destination = int(action.props.get("destination", 0)) - link = int(action.props.get("linkcmd", 0)) - if destination == 0x06: - self.mlgw.send_beo4_cmd(device_id, destination, key_code, 0x01, link) - else: - self.mlgw.send_beo4_cmd(device_id, destination, key_code, 0x00, link) - - def send_beo4_src(self, action, device): - device_id = int(device.address) - key_code = int(action.props.get("keyCode", 0)) - destination = int(action.props.get("destination", 0)) - link = int(action.props.get("linkcmd", 0)) - if destination == 0x06: - self.mlgw.send_beo4_cmd(device_id, destination, key_code, 0x01, link) - else: - self.mlgw.send_beo4_cmd(device_id, destination, key_code, 0x00, link) - - def send_br1_key(self, action, device): - device_id = int(device.address) - key_code = int(action.props.get("keyCode", 0)) - network_bit = int(action.props.get("netBit", 0)) - self.mlgw.send_beoremoteone_cmd(device_id, key_code, network_bit) - - def send_br1_src(self, action, device): - device_id = int(device.address) - key_code = str(action.props.get("keyCode", 0)) - network_bit = int(action.props.get("netBit", 0)) - try: - key_code = CONST.beoremoteone_commanddict.get(key_code) - self.mlgw.send_beoremoteone_select_source(device_id, key_code[0], key_code[1], network_bit) - except KeyError: - pass - - def send_hip_cmd(self, action): - zone = str(action.props.get("zone", 0)) - room = str(action.props.get("room", 0)) - device_type = str(action.props.get("devType", 0)) - device_id = str(action.props.get("deviceID", 0)) - hip_command = str(action.props.get("hip_cmd", 0)) - telegram = "c " + zone + "/" + room + "/" + device_type + "/" + device_id + "/" + hip_command - if self.mlcli.isBLGW: - self.blgw.send_cmd(telegram) - - def send_hip_cmd2(self, action): - hip_command = str(action.props.get("hip_cmd", 0)) - if self.mlcli.isBLGW: - self.blgw.send_cmd(hip_command) - - def send_hip_query(self, action): - zone = str(action.props.get("zone", 0)) - room = str(action.props.get("room", 0)) - device_type = str(action.props.get("devType", 0)) - device_id = str(action.props.get("deviceID", 0)) - if self.mlcli.isBLGW: - self.blgw.query(zone, room, device_type, device_id) - - def request_state_update(self, action, device): - action_id = str(action.props.get("id", 0)) - zone = str(device.pluginProps['zone']) - room = str(device.pluginProps['room']) - device_type = "AV renderer" - device_id = str(device.name) - if self.mlcli.isBLGW: - self.blgw.query(zone, room, device_type, device_id) - - def send_virtual_button(self, action): - button_id = int(action.props.get("buttonID", 0)) - button_action = int(action.props.get("action", 0)) - self.mlgw.send_virtualbutton(button_id, button_action) - - def post_notification(self, action): - title = str(action.props.get("title", 0)) - body = str(action.props.get("body", 0)) - self.iTunes.notify(body, title) - - def all_standby(self, action): - self.mlgw.send_beo4_cmd(1, CONST.CMDS_DEST.get("ALL PRODUCTS"), CONST.BEO4_CMDS.get("STANDBY")) - - def request_serial_number(self): - self.mlgw.get_serial() - - def request_device_update(self): - if self.mlcli.isBLGW: - self.blgw.query(dev_type="AV renderer") - - def reset_clients(self): - self.check_connection(self.mlgw) - self.check_connection(self.mlcli) - - if self.mlcli.isBLGW: - self.check_connection(self.blgw) - else: - self.check_connection(self.mltn) - - # ######################################################################################## - # ##### Indigo UI Events - def light_key(self, message): - room = message['room_number'] - key_code = CONST.BEO4_CMDS.get(message['command'].upper()) - - for trigger in self.triggers: - props = trigger.globalProps["uk.co.lukes_plugins.BeoGateway.plugin"] - if trigger.pluginTypeId == "lightKey" and \ - (props["room"] == str(99) or props["room"] == str(room)) and \ - props["keyCode"] == str(key_code): - indigo.trigger.execute(trigger) - break - - def control_key(self, message): - room = message['room_number'] - key_code = CONST.BEO4_CMDS.get(message['command'].upper()) - - for trigger in self.triggers: - props = trigger.globalProps["uk.co.lukes_plugins.BeoGateway.plugin"] - if trigger.pluginTypeId == "controlkey" and \ - (props["room"] == str(99) or props["room"] == str(room)) and \ - props["keyCode"] == str(key_code): - indigo.trigger.execute(trigger) - break - - def beo4_key(self, message): - source = message['State_Update']['source'] - source_type = message['State_Update']['source'] - key_code = CONST.BEO4_CMDS.get(message['State_Update']['command'].upper()) - - for trigger in self.triggers: - props = trigger.globalProps["uk.co.lukes_plugins.BeoGateway.plugin"] - if trigger.pluginTypeId == "beo4Key" and props["keyCode"] == str(key_code): - if props["sourceType"] == "Any Source": - indigo.trigger.execute(trigger) - break - elif props["sourceType"] == "Any Audio" and "AUDIO" in source_type: - indigo.trigger.execute(trigger) - break - elif props["sourceType"] == "Any Audio" and "AUDIO" in source_type: - indigo.trigger.execute(trigger) - break - elif props["sourceType"] == source: - indigo.trigger.execute(trigger) - break - - def virtual_button(self, message): - button_id = message['button'] - action = message['action'] - for trigger in self.triggers: - props = trigger.globalProps["uk.co.lukes_pugins.mlgw.plugin"] - if trigger.pluginTypeId == "virtualButton" and \ - props["buttonID"] == str(button_id) and \ - props["action"] == str(action): - indigo.trigger.execute(trigger) - break - - # ######################################################################################## - # ##### Indigo UI Device Controls - def actionControlDevice(self, action, node): - """ Callback Method to Control a Relay Device. """ - if action.deviceAction == indigo.kDeviceAction.TurnOn: - self._dev_on(node) - elif action.deviceAction == indigo.kDeviceAction.TurnOff: - self._dev_off(node) - elif action.deviceAction == indigo.kDeviceAction.Toggle: - if node.states["onOffState"]: - self._dev_off(node) - else: - self._dev_on(node) - elif action.deviceAction == indigo.kDeviceAction.RequestStatus: - self._status_request(node) - - def _dev_on(self, node): - indigo.server.log(node.name + " turned On") - - # Get a local copy of the gateway states from server - active_renderers = self.gateway.states['AudioRenderers'] - active_source = self.gateway.states['currentAudioSource'] - active_sourceName = self.gateway.states['currentAudioSourceName'] - - if self.debug: - indigo.server.log('Active renderers: ' + active_renderers, level=logging.DEBUG) - indigo.server.log('Active Audio Source: ' + active_source, level=logging.DEBUG) - - # Join if music already playing - if active_renderers != '' and active_source != 'Unknown': - # Send Beo4 command - source = active_source - self.mlgw.send_beo4_cmd( - int(node.address), - int(CONST.CMDS_DEST.get("AUDIO SOURCE")), - int(CONST.BEO4_CMDS.get(source)) - ) - - # Update device states - sourceName = active_sourceName - key_value_list = [ - {'key': 'onOffState', 'value': True}, - {'key': 'playState', 'value': 'Play'}, - {'key': 'source', 'value': sourceName}, - {'key': 'mute', 'value': False}, - ] - - if self.debug: - indigo.server.log( - node.name + " joining current audio experience " + sourceName + " (" + source + - "). Joining active renderer(s): " + active_renderers, - level=logging.DEBUG - ) - - # Otherwise start default music source - else: - # Send Beo4 command - source = self.default_audio_source - self.mlgw.send_beo4_cmd( - int(node.address), - int(CONST.CMDS_DEST.get("AUDIO SOURCE")), - int(CONST.BEO4_CMDS.get(source)) - ) - - # Update device states - sourceName = dict(CONST.available_sources).get(self.default_audio_source) - key_value_list = [ - {'key': 'onOffState', 'value': True}, - {'key': 'playState', 'value': 'Play'}, - {'key': 'source', 'value': sourceName}, - {'key': 'mute', 'value': False}, - ] - - if self.debug: - indigo.server.log( - node.name + " starting audio experience " + sourceName + " (" + source + ").", - level=logging.DEBUG - ) - - # Update states on server - node.updateStatesOnServer(key_value_list) - node.updateStateImageOnServer(indigo.kStateImageSel.AvPlaying) - - # Add device to active renderers lists and update gateway - self.add_to_renderers_list(node.name, 'Audio') - key_value_list = [ - {'key': 'currentAudioSource', 'value': source}, - {'key': 'currentAudioSourceName', 'value': sourceName}, - ] - self.gateway.updateStatesOnServer(key_value_list) - - def _dev_off(self, node): - indigo.server.log(node.name + " turned Off") - - # Send Beo4 command - self.mlgw.send_beo4_cmd( - int(node.address), - int(CONST.CMDS_DEST.get("AUDIO SOURCE")), - int(CONST.BEO4_CMDS.get('STANDBY')) - ) - - # Update states to standby values - node.updateStatesOnServer(CONST.standby_state) - node.updateStateImageOnServer(indigo.kStateImageSel.PowerOff) - - # Remove device from active renderers lists - self.remove_from_renderers_list(node.name, 'All') - - def _status_request(self, node): - if node.pluginProps['serial_no'] == 'NA': # Check if this is a netlink device - indigo.server.log(node.name + " does not support status requests") - else: # If netlink, request a status update - self.blgw.query(dev_type="AV renderer", device=node.name) - - # ######################################################################################## - # Define callback function for message return from B&O Gateway - def cb(self, name, header, payload, message): - # ######################################################################################## - # Message handler - # Handle Beo4 Command Events - try: - if message['payload_type'] == "BEO4_KEY": - self.beo4_key(message) - except KeyError: - pass - - # Handle Light and Command Events - try: - if message['Type'] == "LIGHT COMMAND": - self.light_key(message) - elif message['Type'] == "CONTROL COMMAND": - self.control_key(message) - except KeyError: - pass - - # Handle Virtual Button Events - try: - if message['payload_type'] == "MLGW virtual button event": - self.virtual_button(message) - except KeyError: - pass - - # Handle all standby events - try: - if message["command"] == "All Standby": - for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'): - node.updateStatesOnServer(CONST.standby_state) - node.updateStateImageOnServer(indigo.kStateImageSel.PowerOff) - - indigo.devices['Bang and Olufsen Gateway'].updateStatesOnServer(CONST.gw_all_stb) - except KeyError: - pass - - # Handle AV Events - try: - # Use regular incoming messages to sync nowPlaying data - if self.gateway.states['AudioRenderers'] != '' and \ - self.gateway.states['currentAudioSource'] == str(self.itunes_source) and \ - self.itunes_control: - self._get_itunes_track_info(message) - - # For messages of type AV RENDERER, scan keys to update device states - if message["Type"] == "AV RENDERER": - # Tidy up messages - self.av_sanitise(message) - # Filter messages that don't constitute meaningful state updates - actionable = self.filter_messages(message) - if self.debug: - indigo.server.log("Process message: " + str(actionable), level=logging.WARNING) - if actionable: - # Keep track of what sources are playing on the network - self.src_tracking(message) - # Update individual devices based on updates - self.dev_update(message) - except KeyError: - pass - - # Report message content to log - if self.verbose: - if 'State_Update' in message and 'CONNECTION' in message['State_Update']: - # Don't print pong responses from regular client ping to check sockets are open - - # approx every 600 seconds - pass - elif 'Device' in message and message['Device'] == 'Clock': - # Don't print the Clock sync telegrams from the HIP - - # approx every 60 seconds - pass - elif 'payload_type' in message and message['payload_type'] == '0x14': - # Don't print the Clock sync telegrams from the ML - - # approx every 6 seconds - pass - elif 'payload_type' in message and message['payload_type'] == 'CLOCK' and not self.debug: - # Don't print the Clock message telegrams from the ML - pass - else: - self.message_log(name, header, payload, message) - - # ######################################################################################## - # AV Handler Functions - - # #### Message Conditioning - def av_sanitise(self, message): - # Sanitise AV messages - try: # Check for missing source information - if message['State_Update']['source'] in [None, 'None', '']: - message['State_Update']['source'] = 'Unknown' - message['State_Update']['sourceName'] = 'Unknown' - - # Update for standby condition - message['State_Update']['state'] = "Standby" - except KeyError: - pass - - try: # Check for unknown state - if message['State_Update']['state'] in [None, 'None', '']: - message['State_Update']['state'] = 'Unknown' - except KeyError: - pass - - try: # Sanitise unknown Channel/Tracks - if message['State_Update']['nowPlayingDetails']['channel_track'] in [0, 255, '0', '255']: - del message['State_Update']['nowPlayingDetails']['channel_track'] - except KeyError: - pass - - try: # Add sourceName if not in message block - if 'sourceName' not in message['State_Update']: - # Find the sourceName from the source list for this device - if 'Device' in message: # If device known use local source data - self.find_source_name(message['State_Update']['source'], - indigo.devices[message['Device']].pluginProps['sources']) - else: # If device not known use global source data - message['State_Update']['sourceName'] = \ - dict(CONST.available_sources).get(message['State_Update']['source']) - except KeyError: - pass - - try: # Catch GOTO_SOURCE commands and set the goto_flag - if message['payload_type'] == 'GOTO_SOURCE': - self.goto_flag = datetime.now() - if self.debug: - indigo.server.log("GOTO_SOURCE command received - goto_flag set", level=logging.WARNING) - except KeyError: - pass - - def filter_messages(self, message): - # Filter state updates that are: - # 1. Standby states that are received between source changes: - # If device is changing source the state changes as follows [Old Source -> Standby -> New Source]. - # If the standby condition is processed, the New Source state will be filtered by the condition below - # - # 2. Play states that received <1.5 seconds after standby state set: - # Some messages come in on the ML and HIP protocols relating to previous state etc. - # These can be ignored to avoid false states for the indigo devices - try: - for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'): - if node.name == message['Device']: - - # Get time since last state update for this device - time_delta1 = datetime.now() - node.lastChanged - time_delta1 = time_delta1.total_seconds() - # Get time since last GOTO_SOURCE command - time_delta2 = datetime.now() - self.goto_flag - time_delta2 = time_delta2.total_seconds() - - # If standby command received <2.0 seconds after GOTO_SOURCE command , ignore - if 'state' in message['State_Update'] and message['State_Update']['state'] == "Standby" \ - and time_delta2 < 2.0: # Condition 1 - if self.debug: - indigo.server.log(message['Device'] + " ignoring Standby: " + str(round(time_delta2, 2)) + - " seconds elapsed since GOTO_STATE command - ignoring message!", - level=logging.DEBUG) - return False - - # If message received <1.5 seconds after standby state, ignore - elif node.states['playState'] == "Standby" and time_delta1 < 1.5: # Condition 2 - if self.debug: - indigo.server.log(message['Device'] + " in Standby: " + str(round(time_delta1, 2)) + - " seconds elapsed since last state update - ignoring message!", - level=logging.DEBUG) - return False - else: - return True - except KeyError: - return False - - # #### State tracking - def src_tracking(self, message): - # Track active renderers via gateway device - try: - # If new source is an audio source then update the gateway accordingly - if message['State_Update']['source'] in CONST.source_type_dict.get('Audio Sources'): - try: - # Keep track of which devices are playing audio sources - if message['Device'] not in self.gateway.states['AudioRenderers'] and \ - message['State_Update']['state'] not in ['Standby', 'Unknown', 'None']: - - # Log current audio source (MasterLink allows a single audio source for distribution) - source = message['State_Update']['source'] - sourceName = dict(CONST.available_sources).get(source) - - self.gateway.updateStateOnServer('currentAudioSource', value=source) - self.gateway.updateStateOnServer('currentAudioSourceName', value=sourceName) - - try: - self.gateway.updateStateOnServer('nowPlaying', value=message['State_Update']['nowPlaying']) - except KeyError: - self.gateway.updateStateOnServer('nowPlaying', value='Unknown') - - self.add_to_renderers_list(message['Device'], 'Audio') - - # Remove device from Video Renderers list if it is on there - if message['Device'] in self.gateway.states['VideoRenderers']: - self.remove_from_renderers_list(message['Device'], 'Video') - - except KeyError: - pass - - # If source is N.Music then control accordingly - if message['State_Update']['source'] == str(self.itunes_source) and self.itunes_control: - self.iTunes_transport_control(message) - - # If new source is an video source then update the gateway accordingly - elif message['State_Update']['source'] in CONST.source_type_dict.get('Video Sources'): - try: - # Keep track of which devices are playing video sources - if message['Device'] not in self.gateway.states['VideoRenderers'] and \ - message['State_Update']['state'] not in ['Standby', 'Unknown', 'None']: - self.add_to_renderers_list(message['Device'], 'Video') - - # Remove device from Audio Renderers list if it is on there - if message['Device'] in self.gateway.states['AudioRenderers']: - self.remove_from_renderers_list(message['Device'], 'Audio') - - except KeyError: - pass - except KeyError: - pass - - def dev_update(self, message): - # Update device states - try: - for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'): - if node.name == message['Device']: - # Handle Standby state - if message['State_Update']['state'] == "Standby": - # Update states to standby values - node.updateStatesOnServer(CONST.standby_state) - node.updateStateImageOnServer(indigo.kStateImageSel.PowerOff) - - # Remove the device from active renderers list - self.remove_from_renderers_list(message['Device'], 'All') - - # Post state update in Apple Notification Centre - if self.notifymode: - self.iTunes.notify(node.name + " now in Standby", "Device State Update") - return - - # If device not in standby then update its state information - # get current states as list # Index - last_state = [ - node.states['playState'], # 0 - node.states['source'], # 1 - node.states['nowPlaying'], # 2 - node.states['channelTrack'], # 3 - node.states['volume'], # 4 - node.states['mute'], # 5 - node.states['onOffState'] # 6 - ] - - # Initialise new state list - the device is not in standby so it must be on - new_state = last_state[:] - new_state[5] = False - new_state[6] = True - - # Update device states with values from message - if 'state' in message['State_Update']: - if message['State_Update']['state'] not in ['None', 'Standby', '', None]: - if last_state[0] == 'Standby' and message['State_Update']['state'] == 'Unknown': - new_state[0] = 'Play' - elif last_state[0] != 'Standby' and message['State_Update']['state'] == 'Unknown': - pass - else: - new_state[0] = message['State_Update']['state'] - - if 'sourceName' in message['State_Update'] and message['State_Update']['sourceName'] != 'Unknown': - # Sanitise source name to avoid indigo key errors (remove whitespace) - source_name = message['State_Update']['sourceName'].strip().replace(" ", "_") - new_state[1] = source_name - - if 'nowPlaying' in message['State_Update']: - # Update now playing information unless the state value is empty or unknown - if message['State_Update']['nowPlaying'] not in ['', 'Unknown']: - new_state[2] = message['State_Update']['nowPlaying'] - # If the state value is empty/unknown and the source has not changed then no update required - elif new_state[1] != last_state[1]: - # If the state has changed and the value is unknown, then set as "Unknown" - new_state[2] = 'Unknown' - - if 'nowPlayingDetails' in message['State_Update'] and \ - 'channel_track' in message['State_Update']['nowPlayingDetails']: - new_state[3] = message['State_Update']['nowPlayingDetails']['channel_track'] - elif new_state[1] != last_state[1]: - # If the state has changed and the value is unknown, then set as "Unknown" - new_state[2] = 0 - - if 'volume' in message['State_Update']: - new_state[4] = message['State_Update']['volume'] - - if new_state != last_state: - # Update states on server - key_value_list = [ - {'key': 'playState', 'value': new_state[0]}, - {'key': 'source', 'value': new_state[1]}, - {'key': 'nowPlaying', 'value': new_state[2]}, - {'key': 'channelTrack', 'value': new_state[3]}, - {'key': 'volume', 'value': new_state[4]}, - {'key': 'mute', 'value': new_state[5]}, - {'key': 'onOffState', 'value': new_state[6]}, - ] - node.updateStatesOnServer(key_value_list) - - # Post notifications Notifications - if self.notifymode: - self.notifications(node.name, last_state, new_state) - - # Update state image on server - if new_state[0] == "Stopped": - node.updateStateImageOnServer(indigo.kStateImageSel.AvPaused) - elif new_state[0] not in ['None', 'Unknown', 'Standby', '', None]: - node.updateStateImageOnServer(indigo.kStateImageSel.AvPlaying) - - # If audio source active, update any other active audio renderers accordingly - try: - if new_state[0] not in ['None', 'Unknown', 'Standby', '', None] and \ - message['State_Update']['source'] in CONST.source_type_dict.get('Audio Sources'): - self.all_audio_nodes_update(new_state, node.name, message['State_Update']['source']) - except KeyError: - pass - - break - except KeyError: - pass - - def all_audio_nodes_update(self, new_state, dev, source): - # Loop over all active audio renderers to update them with the latest audio state - for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'): - if node.name in self.gateway.states['AudioRenderers'] and node.name != dev: - # Get current state of this node - last_state = [ - node.states['playState'], - node.states['source'], - node.states['nowPlaying'], - node.states['channelTrack'], - node.states['volume'], - node.states['mute'], - ] - - if last_state[:4] != new_state[:4]: - # Update the play state for active Audio renderers if new values are different from current ones - key_value_list = [ - {'key': 'onOffState', 'value': True}, - {'key': 'playState', 'value': new_state[0]}, - {'key': 'source', 'value': new_state[1]}, - {'key': 'nowPlaying', 'value': new_state[2]}, - {'key': 'channelTrack', 'value': new_state[3]}, - {'key': 'mute', 'value': False}, - ] - node.updateStatesOnServer(key_value_list) - node.updateStateImageOnServer(indigo.kStateImageSel.AvPlaying) - - # Update the gateway - if self.gateway.states['currentAudioSourceName'] != new_state[1]: - # If the source has changed, update both source and nowPlaying - sourceName = new_state[1] - - key_value_list = [ - {'key': 'currentAudioSource', 'value': source}, - {'key': 'currentAudioSourceName', 'value': sourceName}, - {'key': 'nowPlaying', 'value': new_state[2]}, - ] - self.gateway.updateStatesOnServer(key_value_list) - - elif self.gateway.states['nowPlaying'] != new_state[2] and new_state[2] not in ['', 'Unknown']: - # If the source has not changed, and nowPlaying is not Unknown, update nowPlaying - self.gateway.updateStateOnServer('nowPlaying', value=new_state[2]) - - # #### Active renderer list maintenance - def add_to_renderers_list(self, dev, av): - if av == "Audio": - renderer_list = 'AudioRenderers' - renderer_count = 'nAudioRenderers' - else: - renderer_list = 'VideoRenderers' - renderer_count = 'nVideoRenderers' - - # Retrieve the renderers and convert from string to list - renderers = self.gateway.states[renderer_list].split(', ') - - # Sanitise the list for stray blanks - if '' in renderers: - renderers.remove('') - - # Add device to list if not already on there - if dev not in renderers: - renderers.append(dev) - self.gateway.updateStateOnServer(renderer_list, value=', '.join(renderers)) - self.gateway.updateStateOnServer(renderer_count, value=len(renderers)) - - def remove_from_renderers_list(self, dev, av): - # Remove devices from renderers lists when the enter standby mode - if av in ['Audio', 'All']: - if dev in self.gateway.states['AudioRenderers']: - renderers = self.gateway.states['AudioRenderers'].split(', ') - renderers.remove(dev) - - self.gateway.updateStateOnServer('AudioRenderers', value=', '.join(renderers)) - self.gateway.updateStateOnServer('nAudioRenderers', value=len(renderers)) - - if av in ['Video', 'All']: - if dev in self.gateway.states['VideoRenderers']: - renderers = self.gateway.states['VideoRenderers'].split(', ') - renderers.remove(dev) - - self.gateway.updateStateOnServer('VideoRenderers', value=', '.join(renderers)) - self.gateway.updateStateOnServer('nVideoRenderers', value=len(renderers)) - - # If no audio sources are playing then update the gateway states - if self.gateway.states['AudioRenderers'] == '': - key_value_list = [ - {'key': 'AudioRenderers', 'value': ''}, - {'key': 'nAudioRenderers', 'value': 0}, - {'key': 'currentAudioSource', 'value': 'Unknown'}, - {'key': 'currentAudioSourceName', 'value': 'Unknown'}, - {'key': 'nowPlaying', 'value': 'Unknown'}, - ] - self.gateway.updateStatesOnServer(key_value_list) - - # If no AV renderers are playing N.Music, stop iTunes playback - if self.itunes_control: - self.iTunes.stop() - - # #### Helper functions - @staticmethod - def find_source_name(source, sources): - # Get the sourceName for source - for source_name in sources: - if sources[source_name]['source'] == str(source): - return str(sources[source_name]).split()[0] - - # if source list exhausted and no valid name found return Unknown - return 'Unknown' - - @staticmethod - # Get the source ID for sourceName - def get_source(sourceName, sources): - for source_name in sources: - if source_name == sourceName: - return str(sources[source_name]['source']) - - # if source list exhausted and no valid name found return Unknown - return 'Unknown' - - # ######################################################################################## - # Apple Music Control and feedback - def iTunes_transport_control(self, message): - # Transport controls for iTunes - try: # If N.MUSIC command, trigger appropriate self.iTunes control - if message['State_Update']['state'] not in ["", "Standby"]: - self.iTunes.play() - except KeyError: - pass - - try: # If N.MUSIC selected and Beo4 command received then run appropriate transport commands - if message['State_Update']['command'] == "Go/Play": - self.iTunes.play() - - elif message['State_Update']['command'] == "Stop": - self.iTunes.pause() - - elif message['State_Update']['command'] == "Exit": - self.iTunes.stop() - - elif message['State_Update']['command'] == "Step Up": - self.iTunes.next_track() - - elif message['State_Update']['command'] == "Step Down": - self.iTunes.previous_track() - - elif message['State_Update']['command'] == "Wind": - self.iTunes.wind(15) - - elif message['State_Update']['command'] == "Rewind": - self.iTunes.rewind(-15) - - elif message['State_Update']['command'] == "Shift-1/Random": - self.iTunes.shuffle() - - # If 'Info' pressed - update track info - elif message['State_Update']['command'] == "Info": - track_info = self.iTunes.get_current_track_info() - if track_info[0] not in [None, 'None']: - indigo.server.log( - "\n\t----------------------------------------------------------------------------" - "\n\tiTUNES CURRENT TRACK INFO:" - "\n\t============================================================================" - "\n\tNow playing: '" + track_info[0] + "'" - "\n\t by " + track_info[2] + - "\n\t from the album '" + track_info[1] + "'" - "\n\t----------------------------------------------------------------------------" - "\n\tACTIVE AUDIO RENDERERS: " + str(self.gateway.states['AudioRenderers']) + "\n\n", - level=logging.DEBUG - ) - - self.iTunes.notify( - "Now playing: '" + track_info[0] + - "' by " + track_info[2] + - "from the album '" + track_info[1] + "'", - "Apple Music Track Info:" - ) - - # If 'Guide' pressed - print instructions to indigo log - elif message['State_Update']['command'] == "Guide": - indigo.server.log( - "\n\t----------------------------------------------------------------------------" - "\n\tBeo4/BeoRemote One Control of Apple Music" - "\n\tKey mapping guide: [Key : Action]" - "\n\t============================================================================" - "\n\n\t** BASIC TRANSPORT CONTROLS **" - "\n\tGO/PLAY : Play" - "\n\tSTOP/Pause : Pause" - "\n\tEXIT : Stop" - "\n\tStep Up/P+ : Next Track" - "\n\tStep Down/P- : Previous Track" - "\n\tWind : Scan Forwards 15 Seconds" - "\n\tRewind : Scan Backwards 15 Seconds" - "\n\n\t** FUNCTIONS **" - "\n\tShift-1/Random : Toggle Shuffle" - "\n\tINFO : Display Track Info for Current Track" - "\n\tGUIDE : This Guide" - "\n\n\t** ADVANCED CONTROLS **" - "\n\tGreen : Shuffle Playlist 'Recently Played'" - "\n\tYellow : Play Digital Radio Stations from Playlist Radio" - "\n\tRed : More of the Same" - "\n\tBlue : Play the Album that the Current Track Resides On\n\n", - level=logging.DEBUG - ) - - # If colour key pressed, execute the appropriate applescript - elif message['State_Update']['command'] == "Green": - # Play a specific playlist - defaults to Recently Played - script = ASBridge.__file__[:-12] + '/Scripts/green.scpt' - self.iTunes.run_script(script, self.debug) - - elif message['State_Update']['command'] == "Yellow": - # Play a specific playlist - defaults to URL Radio stations - script = ASBridge.__file__[:-12] + '/Scripts/yellow.scpt' - self.iTunes.run_script(script, self.debug) - - elif message['State_Update']['command'] == "Blue": - # Play the current album - script = ASBridge.__file__[:-12] + '/Scripts/blue.scpt' - self.iTunes.run_script(script, self.debug) - - elif message['State_Update']['command'] in ["0xf2", "Red", "MOTS"]: - # More of the same (start a playlist with just current track and let autoplay find similar tunes) - script = ASBridge.__file__[:-12] + '/Scripts/red.scpt' - self.iTunes.run_script(script, self.debug) - except KeyError: - pass - - def _get_itunes_track_info(self, message): - track_info = self.iTunes.get_current_track_info() - if track_info[0] not in [None, 'None']: - # Construct track info string - track_info_ = "'" + track_info[0] + "' by " + track_info[2] + " from the album '" + track_info[1] + "'" - - # Add now playing info to the message block - if 'Type' in message and message['Type'] == "AV RENDERER" and 'source' in message['State_Update'] \ - and message['State_Update']['source'] == str(self.itunes_source) and \ - 'nowPlaying' in message['State_Update']: - message['State_Update']['nowPlaying'] = track_info_ - message['State_Update']['nowPlayingDetails']['channel_track'] = int(track_info[3]) - - # Print track info to log if trackmode is set to true (via config UI) - src = dict(CONST.available_sources).get(self.itunes_source) - if self.gateway.states['currentAudioSource'] == str(self.itunes_source) and \ - track_info_ != self.gateway.states['nowPlaying'] and self.trackmode: - indigo.server.log("\n\t----------------------------------------------------------------------------" - "\n\tiTUNES CURRENT TRACK INFO:" - "\n\t============================================================================" - "\n\tNow playing: '" + track_info[0] + "'" - "\n\t by " + track_info[2] + - "\n\t from the album '" + track_info[1] + "'" - "\n\t----------------------------------------------------------------------------" - "\n\tACTIVE AUDIO RENDERERS: " + str(self.gateway.states['AudioRenderers']) + "\n\n") - - if self.notifymode: - # Post track information to Apple Notification Centre - self.iTunes.notify(track_info_ + " from source " + src, - "Now Playing:") - - # Update nowPlaying on the gateway device - if track_info_ != self.gateway.states['nowPlaying'] and \ - self.gateway.states['currentAudioSource'] == str(self.itunes_source): - self.gateway.updateStateOnServer('nowPlaying', value=track_info_) - - # Update info on active Audio Renderers - for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'): - if node.name in self.gateway.states['AudioRenderers']: - key_value_list = [ - {'key': 'onOffState', 'value': True}, - {'key': 'playState', 'value': 'Play'}, - {'key': 'source', 'value': src}, - {'key': 'nowPlaying', 'value': track_info_}, - {'key': 'channelTrack', 'value': int(track_info[3])}, - ] - node.updateStatesOnServer(key_value_list) - node.updateStateImageOnServer(indigo.kStateImageSel.AvPlaying) - - # ######################################################################################## - # Message Reporting - @staticmethod - def message_log(name, header, payload, message): - # Set reporting level for message logging - try: # CLOCK messages are filtered except in debug mode - if message['payload_type'] == 'CLOCK': - debug_level = logging.DEBUG - else: # Everything else is for INFO - debug_level = logging.INFO - except KeyError: - debug_level = logging.INFO - - # Pretty formatting - convert to JSON format then remove braces - message = json.dumps(message, indent=4) - for r in (('"', ''), (',', ''), ('{', ''), ('}', '')): - message = str(message).replace(*r) - - # Print message data - if len(payload) + 9 < 73: - indigo.server.log("\n\t----------------------------------------------------------------------------" + - "\n\t" + name + ": <--DATA-RECEIVED!-<< " + - datetime.now().strftime("on %d/%m/%y at %H:%M:%S") + - "\n\t============================================================================" + - "\n\tHeader: " + header + - "\n\tPayload: " + payload + - "\n\t----------------------------------------------------------------------------" + - message, level=debug_level) - elif 73 < len(payload) + 9 < 137: - indigo.server.log("\n\t----------------------------------------------------------------------------" + - "\n\t" + name + ": <--DATA-RECEIVED!-<< " + - datetime.now().strftime("on %d/%m/%y at %H:%M:%S") + - "\n\t============================================================================" + - "\n\tHeader: " + header + - "\n\tPayload: " + payload[:66] + "\n\t\t" + payload[66:137] + - "\n\t----------------------------------------------------------------------------" + - message, level=debug_level) - else: - indigo.server.log("\n\t----------------------------------------------------------------------------" + - "\n\t" + name + ": <--DATA-RECEIVED!-<< " + - datetime.now().strftime("on %d/%m/%y at %H:%M:%S") + - "\n\t============================================================================" + - "\n\tHeader: " + header + - "\n\tPayload: " + payload[:66] + "\n\t\t" + payload[66:137] + "\n\t\t" + payload[137:] + - "\n\t----------------------------------------------------------------------------" + - message, level=debug_level) - - def notifications(self, name, last_state, new_state): - # Post state information to the Apple Notification Centre - # Information index: - # node.states['playState'], # 0 - # node.states['source'], # 1 - # node.states['nowPlaying'], # 2 - # node.states['channelTrack'], # 3 - # node.states['volume'], # 4 - # node.states['mute'], # 5 - # node.states['onOffState'] # 6 - - # Don't post notification if nothing has changed - if last_state == new_state: - return - - # Source status information - if last_state[0] != new_state[0] and new_state[0] == "Standby": # Power off - self.iTunes.notify( - name + " now in Standby", - "Device State Update" - ) - return - elif last_state[1] != new_state[1] and new_state[0] != "Standby": # Source Update - self.iTunes.notify( - name + " now playing from source " + new_state[1], - "Device State Update" - ) - return - elif last_state[0] != new_state[0] and new_state[0] == "Play": # Power on - self.iTunes.notify( - name + " Active", - "Device State Update" - ) - return - - # Channel/Track information - if new_state[2] not in [None, 'None', '', 0, '0', 'Unknown']: # Now Playing Update - self.iTunes.notify( - new_state[2] + " from source " + new_state[1], - name + " Now Playing:" - ) - elif last_state[3] != new_state[3] and new_state[3] not in [0, 255, '0', '255']: # Channel/Track Update - self.iTunes.notify( - name + " now playing channel/track " + new_state[3] + " from source " + new_state[1], - "Device Channel/Track Information" - ) - - # ######################################################################################## - # Indigo Server Methods - def startup(self): - indigo.server.log(u"Startup called") - - # Download the config file from the gateway and initialise the devices - config = MLCONFIG.MLConfig(self.host, self.user, self.pwd, self.debug) - self.gateway = indigo.devices['Bang and Olufsen Gateway'] - - # Create MLGW Protocol and ML_CLI Protocol clients (basic command listening) - indigo.server.log('Creating MLGW Protocol Client...', level=logging.WARNING) - self.mlgw = MLGW.MLGWClient(self.host, self.port[0], self.user, self.pwd, 'MLGW protocol', self.debug, self.cb) - asyncore.loop(count=10, timeout=0.2) - - indigo.server.log('Creating ML Command Line Protocol Client...', level=logging.WARNING) - self.mlcli = MLCLI.MLCLIClient(self.host, self.port[2], self.user, self.pwd, - 'ML command line interface', self.debug, self.cb) - # Log onto the MLCLI client and ascertain the gateway model - asyncore.loop(count=10, timeout=0.2) - - # Now MLGW and MasterLink Command Line Client are set up, retrieve MasterLink IDs of products - config.get_masterlink_id(self.mlgw, self.mlcli) - - # If the gateway is a BLGW use the BLHIP protocol, else use the legacy MLHIP protocol - if self.mlcli.isBLGW: - indigo.server.log('Creating BLGW Home Integration Protocol Client...', level=logging.WARNING) - self.blgw = BLHIP.BLHIPClient(self.host, self.port[1], self.user, self.pwd, - 'BLGW Home Integration Protocol', self.debug, self.cb) - self.mltn = None - else: - indigo.server.log('Creating MLGW Home Integration Protocol Client...', level=logging.WARNING) - self.mltn = MLtn.MLtnClient(self.host, self.port[2], self.user, self.pwd, 'ML telnet client', - self.debug, self.cb) - self.blgw = None - - # Connection polling - def check_connection(self, client): - last = round(time.time() - client.last_received_at, 2) - # Reconnect if socket has disconnected, or if no response received to last ping - if not client.is_connected or last > 60: - indigo.server.log("\t" + client.name + ": Reconnecting!", level=logging.WARNING) - client.handle_close() - self.sleep(0.5) - client.client_connect() - - # Indigo main program loop - def runConcurrentThread(self): - try: - while True: - # Ping all connections every 10 minutes to prompt messages on the network - asyncore.loop(count=self.pollinterval, timeout=1) - if self.mlgw.is_connected: - self.mlgw.ping() - if self.mlcli.is_connected: - self.mlcli.ping() - if self.mlcli.isBLGW: - if self.blgw.is_connected: - self.blgw.ping() - else: - if self.mltn.is_connected: - self.mltn.ping() - - # Check the connections approximately every 10 minutes to keep sockets open - asyncore.loop(count=5, timeout=1) - self.check_connection(self.mlgw) - self.check_connection(self.mlcli) - if self.mlcli.isBLGW: - self.check_connection(self.blgw) - else: - self.check_connection(self.mltn) - - self.sleep(0.5) - - except self.StopThread: - raise asyncore.ExitNow('Server is quitting!') - - # Tidy up on shutdown - def shutdown(self): - indigo.server.log("Shutdown plugin") - del self.mlgw - del self.mlcli - if self.mlcli.isBLGW: - del self.blgw - else: - del self.mltn - del self.iTunes - raise asyncore.ExitNow('Server is quitting!')