From 3adc2125e030763082aef1edaaa0e3ec205302fe Mon Sep 17 00:00:00 2001 From: LukeSpad <64772822+LukeSpad@users.noreply.github.com> Date: Sat, 5 Feb 2022 20:34:14 +0000 Subject: [PATCH] Delete Server Plugin directory --- Server Plugin/Actions.xml | 203 --- Server Plugin/Devices.xml | 157 -- Server Plugin/Events.xml | 66 - Server Plugin/MenuItems.xml | 15 - Server Plugin/PluginConfig.xml | 93 -- Server Plugin/Resources/ASBridge.py | 140 -- Server Plugin/Resources/ASBridge.pyc | Bin 8027 -> 0 bytes Server Plugin/Resources/BLHIP_CLIENT.py | 282 ---- Server Plugin/Resources/BLHIP_CLIENT.pyc | Bin 11893 -> 0 bytes Server Plugin/Resources/CONSTANTS.py | 683 --------- Server Plugin/Resources/CONSTANTS.pyc | Bin 21752 -> 0 bytes Server Plugin/Resources/MLCLI_CLIENT.py | 462 ------ Server Plugin/Resources/MLCLI_CLIENT.pyc | Bin 16403 -> 0 bytes Server Plugin/Resources/MLCONFIG.py | 298 ---- Server Plugin/Resources/MLCONFIG.pyc | Bin 9245 -> 0 bytes Server Plugin/Resources/MLGW_CLIENT.py | 431 ------ Server Plugin/Resources/MLGW_CLIENT.pyc | Bin 17589 -> 0 bytes Server Plugin/Resources/MLtn_CLIENT.py | 370 ----- Server Plugin/Resources/MLtn_CLIENT.pyc | Bin 15209 -> 0 bytes .../Resources/Notify.app/Contents/Info.plist | 78 - .../Notify.app/Contents/MacOS/applet | Bin 99064 -> 0 bytes .../Resources/Notify.app/Contents/PkgInfo | 1 - .../Contents/Resources/Scripts/main.scpt | Bin 1116 -> 0 bytes .../Notify.app/Contents/Resources/applet.icns | Bin 64857 -> 0 bytes .../Notify.app/Contents/Resources/applet.rsrc | Bin 388 -> 0 bytes .../Resources/description.rtfd/TXT.rtf | 5 - Server Plugin/Resources/Scripts/blue.scpt | Bin 6180 -> 0 bytes Server Plugin/Resources/Scripts/green.scpt | Bin 1570 -> 0 bytes Server Plugin/Resources/Scripts/red.scpt | Bin 5048 -> 0 bytes Server Plugin/Resources/Scripts/yellow.scpt | Bin 1802 -> 0 bytes Server Plugin/Resources/__init__.py | 1 - Server Plugin/Resources/__init__.pyc | Bin 228 -> 0 bytes Server Plugin/plugin.py | 1277 ----------------- 33 files changed, 4562 deletions(-) delete mode 100644 Server Plugin/Actions.xml delete mode 100644 Server Plugin/Devices.xml delete mode 100644 Server Plugin/Events.xml delete mode 100644 Server Plugin/MenuItems.xml delete mode 100644 Server Plugin/PluginConfig.xml delete mode 100644 Server Plugin/Resources/ASBridge.py delete mode 100644 Server Plugin/Resources/ASBridge.pyc delete mode 100644 Server Plugin/Resources/BLHIP_CLIENT.py delete mode 100644 Server Plugin/Resources/BLHIP_CLIENT.pyc delete mode 100644 Server Plugin/Resources/CONSTANTS.py delete mode 100644 Server Plugin/Resources/CONSTANTS.pyc delete mode 100644 Server Plugin/Resources/MLCLI_CLIENT.py delete mode 100644 Server Plugin/Resources/MLCLI_CLIENT.pyc delete mode 100644 Server Plugin/Resources/MLCONFIG.py delete mode 100644 Server Plugin/Resources/MLCONFIG.pyc delete mode 100644 Server Plugin/Resources/MLGW_CLIENT.py delete mode 100644 Server Plugin/Resources/MLGW_CLIENT.pyc delete mode 100644 Server Plugin/Resources/MLtn_CLIENT.py delete mode 100644 Server Plugin/Resources/MLtn_CLIENT.pyc delete mode 100644 Server Plugin/Resources/Notify.app/Contents/Info.plist delete mode 100644 Server Plugin/Resources/Notify.app/Contents/MacOS/applet delete mode 100644 Server Plugin/Resources/Notify.app/Contents/PkgInfo delete mode 100644 Server Plugin/Resources/Notify.app/Contents/Resources/Scripts/main.scpt delete mode 100644 Server Plugin/Resources/Notify.app/Contents/Resources/applet.icns delete mode 100644 Server Plugin/Resources/Notify.app/Contents/Resources/applet.rsrc delete mode 100644 Server Plugin/Resources/Notify.app/Contents/Resources/description.rtfd/TXT.rtf delete mode 100644 Server Plugin/Resources/Scripts/blue.scpt delete mode 100644 Server Plugin/Resources/Scripts/green.scpt delete mode 100644 Server Plugin/Resources/Scripts/red.scpt delete mode 100644 Server Plugin/Resources/Scripts/yellow.scpt delete mode 100644 Server Plugin/Resources/__init__.py delete mode 100644 Server Plugin/Resources/__init__.pyc delete mode 100644 Server Plugin/plugin.py 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 0fa8ae44be6542956efea2c509f7ce227279b007..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8027 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 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 d7ec4e85572622f1a69266780ef91fe9bf4acf3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11893 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 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 358305200847381094b8a8feaa012391cf6c47bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21752 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 V^K+QRAI*O-0^yu-?QyJ|{sTVQZ%qII 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 b9b8d8d2f759cb47635896cd4298c705a94bdf96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16403 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 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 d8c3b2575b98ec125dc4191f038e20dad465cc40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9245 zcmd5?+ix7#c|WsDF1ah-#fw%KdnilxMwXY7tjIP)JLXL?6LKjJDKTZB4u(6!Q(f}l-`q9{=0p+Er_`2+e;pvY7E+J`>np=h6q7HEIJ@9grT z$aQLgILj_809`_BkX>`Uj zYBSU6XVqr5)9+H7UDD5}pj$O^YBQ%AJ!-Q@8HAWsL9c4$)n;Bl{lzOhzZ*BGyfwfW(kn#T;|0<;ny6FI#PuABTF8DPsNoQN>PTJodnsXAr%8`=6 zmFLi`#;zB8?t))U(_TEUEYC;bwqH9Yx*LOY_$Di8JdoQV5r=yqF{8c*nm}Kch>^H1 z>2xda7+Wx=M;>#WdXC3lWogWl0>EwswP)oZsyac+;~cl)hgJ6YB~kPECU?*rcBw<9 zY;+9ipv_1JZB{zc*1O0ea&%2dt&|8!fVw)H5NUs3EgcOQGx6BJ<9zJGaYsrXB{Znja=pb6)ho zNxf&zLD`oDdSVKKOT92nrq;apsTZ5=VarRR7N93nknL*a{?dbT^FU;6+kWV$woQ!@ zIVdBa$s71&s#Flto5NSQ|4VcZK3Azybwkx5c<4b!rCCK@8-@Y-Lo)SlH30>fI5Je4 zQ}rIo2`Y=@IGLBWpq^*A&k-QsB|p2!~ZyI;?gJD*k%~Y(qnRhx^2ODoGi{Ewlyhhc&Y*HqeT-kf;IXb{sX# z4&TdV(VM_~>IDf6a3@Wh(^FH)dGx0mQ0FEz>ZuQ|m+J}SLRLC3JpYft*wP4|z#iDG z$WOXl4<)1m1yL;}Q2avRy1y}NRhB9bEbbEF#ok^E_MOO%nwMIXn8j@t?O2RENu9Kn z*wx7OEQ~2ZL-3(~QoFV4rrv%kTHm4Dvq?ir+kET zF4_zx2yE}M+8{K!4`6LDGdQApKPC<1U|wJi&>CrPSU0HZLvmyED7RO+dF2+A3uD7g zf}2B(#T``c5IkkiQMcL%UpqFDf1`_10aS(Y)qx&sy-ns+y5-beV0^m;YpY~(mVw_E^7>~h}-$4vG;S4)CJxd zNAOg7#UYsXnwmL;aNs!LpQbaU43nCwdQrKr^BC7uMJ}FHGheV^NH4gmX2LfV0Tu<| zld}K`gG-d!|Eq4E^9J$$)9$0ZN;Pf;#RDKojlm%VdJPz2;5tsg({(lDzNy%IOU)cy z=qv~0fG3AbYGxz6bj<00%#hPx>r4p`kLkUU{Q`1GFJS%e^kTt{7wBC%3y?6l1V*Cy zs>Q-#@IAdqaq!`7HM5^)Naam3WG8cUPIkz}LBi^nxU_y*x$h{q%-iLtPo3UhJnRFE zo0&swnIa4`bJQ<_(2{L-5d%!#(u(n>h^MEEch(irr`KfqHl}g+Keq9_4Ji$Go858b%ouT(SLp5I1U* z57iM|(6l-lKE>?E-G>Fl>PW!nm*|LRy89Zsi@N(Zx;NoqRWGj< z+$P#PUK4ik`MO*cZ>suRnnOF8{c_v$*H3Pxiw3rn8J+v+3}fgUf7Rj2T+azt?w@ew zfrN!;IMe0;VA>_+enQ|jJQ0q#LHjb;ds`iislzcPDcpy9zslinV{a$3K^?j!;cl~IJZo=%P+8K|5I5B5GR88km11zgI8ofsF5IH!mX$YS~~_x8m3l8 zx+TNdYxSOrA6B_Fz1_A1ARstAd=Uqn@a~Jt7M|QwM-!*e0HT0(w+-uXLd-?rojL!% zPM>de;umcx5{s`Pz^X{ld6MEJw=me+Emxy*(AxD9yQ#CF^2w629xBg1j=hlCY@B=z zn;ymy!taFPd^>D^NmySlmkZV)R!b<`jH5KFMuEi1q0{grFr0G$!U3^A(ssf%$gT^NFqGfbUkJR=5`dDCzzWW^XY>54ze}RNj%ey z%rHu^Jn&M_#A?t4L?*dp(e+c3(}o3Xo5&b^FA=bg`_u`mIEsmAVn$(bFe#a;UcP?N z^p@O|A@AI3n<1KLVd#>ZT*uUJ#A(S_wxa;NmOQ1M1^ZG|V3NRmK4sXPgmNGd@}0m2 zVa!i}cils|fv6y#O~I))AY>1zgBik@TYj*ibEHJN9FwGkofS?phxsfDkpwX*3R{gW z9P_W}ieRiLYSos6+NBVB7ij0cj7DQf>=&USPU~<^Y16P*q zr3KN^wKh?$hLBpB6|@Obs9e)YxAfZyxZq(*YYPYz6Tpc12GTI*YhxlfnLRXW@;Wxn)9)Td=84&u1Dx`Gx-~=`)d0BP8+r7VTfpHPdXoN1it1X)l1kJcf`sLR4uuN{i}g{%%ew5kU}0kJZqH+#aBo^ z|H`a_-M9Ju<-GcTb0gIJrz`}B9&YHh0@ot&tjcHRNECB zO{A~gQDT)iNzAHLT2BOv>IjUAe!~aeGV3&(KvgWp4^xZxg~h6j#R85s%7*E_tkuVe zwd_K=`i9^Uy1-^#;lhh(Vy_uERZkC750cdoUZPXhNzz!X@Y~*j)Kie{!r@YHyfqYR zypz@B9ZGd;>*6J9&uq9zJ4v@JvQkJ&P(svx(q-kA8vq{n5dA?Lk8}uqJBh1Yk~nY( z{%4*HZh6sNRE5NA9PdXZ63k&~lkU5-PpUdjM^d&n)kNx>a_Yk3+@l9lV1^?P-K_)Q zNxrjSa*DR4hEwy7xj}W=6`Eyy&C`{nhtV@HUftfd=zuI*SjwxpP0sOdmaX;0DLfV# z>7!B;Q*!>YpQNktf)7492l^y;9^S9!xk6$idg`r5_w^knp3fC>Xx_S{7NC?$m07CX zU)59GEkgl-mq?&4hH({bQ#3^Dp$-2*4Z)R%06{jfCl>{-Ml!kW=j;?JZ#^ck4KyOZ zW*qS%vbH$-Esn~C5La!60;k+IZVvq<_QOL7SyTTR3eazALdFH`^qF03q;@t4ji zv_9n#bcn>!#_4c_yqrV3V_{t9)3jdMQr>0Y_BOuBW#Vd#7~{rRHiv@V#Y{1iHTv+$ z899vb)1T=_p)V`_tZ@#XF?Q+ z(B79F#p<`v`-br*pk6la>T)Li1@&{D9Qzk=j5)uTZjVKcZ@h#~dG@XOXG_ z&XAPzQSd}55~Vv99{$ouN0kX{Hsm-sG89GP!$+E z<^@ww6jxEx=u+6YvG+NuaT8Ma{3#|3cutm*#+5FPqEz@?WelwP4!#?EADv?J#TSnP zgC#Pxg+)Amc& zyBs8#rgI?g>gY`6hGT+YTZ=2J>x=f>;_6*lCHH_Wk(y^MW1BwFukEDfy5I1kR%$&W zNIJhX>Uck1?fEa_lbO){n|`9>oN_^XS%gKY&WGrLQw_ZKqaX6$-+eQ=M35CPeHO*L zzs`Fzp`6NQC3-c^>$_Zz*QiSF zT77>bAhLC2&~Pn>a}pN$=m|&WR~FXng~hdXi|OY<>AoJ<1>+g z!-wM@f|I|3-+p{1yV#evQd9y4FXG0_=+MQYm1>TDy$ZA6?N#wL4q}9^N?yCdsdZkB z6N-q``V;oH(TI9Ubl^&>b7H*eZUwJ2v#kioU&noxP@NPm);{pEB=*b^TOuf1XWzEn zK%5nLwr%|kbJhSGF0~j;i0U$y^s9)ub|TmM3r_KX6Lpi`tM_)_qDX0a@>V~W8O!HK z^Zzh-LnAJ9GKsSOj7^cvI2-08;tD$Nmdku)EpLwH?Xvh^l*d>xV7*JHBa$(7{*OUf s(w5oE`kHnbOk((C(TfOLuRQa_6U}JV_z0?`66R41%k*a));O5`AEtagH2?qr 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 d6c86c83dffb4e44c889f349f52c53af9e2a5de0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17589 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 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 7be73e356338e1c7243d645d2f2d5ad2ac46ca7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15209 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 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 792791262270b07af372dbea68d4be2bb805af53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99064 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 UH~-N5eZBked)pWOe9^<=-_hmt&;S4c 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 c478d35fc04038aeb4ea0260d1a41cc1a2a6454b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1116 zcmaJ>*-lhJ5IuKcV7lo$Ga$0c`c8&V@Iez3-^9c>!x|?JV3;Awo1fq(sPFy@i|jBC z2(s@YvU#i?Mp2k>lRDjXZ*_ImIhFgcBh~%%{*%VG*0#8{0DvDM;lPFG$OcCC&P#c3 zAK!&9Uy2Yi0&XJr(nB#yjDYtj=Yzix=7p9;#S!WsNGE!SA;-dE3fc%b1NqQ%&2t2C zsMm?~i|6S?I@x1GwOA>P5%?Q=rg;VvNBAZVOwpuxj&dKm-&yB0?j4enTDZq27^=5L+h!D_quG`EQ~Wmv1RFkOak1enknF zxQGTcGONY?cP0Za!S(qCO`d=kph<3Lh6|k6%-m2*bK$z~n)8L!apguSyg|gB<1A-1 z=L%$-y_s#MP_`%Y3D*BB507~p@d z{LcyWzdi>H0Pwl--*U!3@;{0IO#d_Me_!r@y8r9-e;b2<0|Ne^$Bz;K%zu*r0YE`O z004h<0FVU)BqS96ug8xG04V^Vu%oG=v#AMzkq3diy{Vm)f~YuwtEr=teCHMc)dfXg*CaD;TneD1b&`6Ii5N1*R1NqzS`I;m&Mtg@mx;D+gZs z> z&zi`F{dNGIYUDc=8|q;y5UlVPLB|)R4!I~qg~lF$j6+PL3joM)4&)yb76sY!;gcl2 z0}73IGEjhvo5aeS9go3q@Abl${&b<@Dza(87ndr8Pw(Rv0UNBOsshlu~O6kyf5 z5+y-2ZI<|b(nCY}m*YS)ONyKWdC1dvx;^<>1c|S5afcwOx2v%t9l01H9G5BOOSyZ& zyHb#IR3pL5>Ou1GYrJnPgCz?KlLXS=HSCo)63dkAKxmra{k9baR}ItsKngn>L`>>6 zGyMTZdOj&{(+Q+>v|PoxlWp@iuT})%qeZB`{%%#;*C;xPq>N2|z%D4vs<^VSPi(AA zbCm{c2EV;gbpn*bfFMDF{9p>|hLJzGkF}v+i#v$JxmM$&>E62^yTKSXiqmtoJA8Fl zP7TE{ulR70LXR+H-RGyc;Flo)(lw+8&WR7}W0AMfCmFjPp|m=r^=x*yw`%spy^L$; zeen}IQJtDEOY9hsTyP@r=ufv<>wQMF5n7&1w}OAjejlE-DgOL!((fkyK1u(tJW2k$ zsqM49N~gzFeSLh4+O2k0it`=bqCuA};Q8y2oD!p0ylj(j`utp7TO650ft28SmP~Qn zUFrXx;<@`QqwJ@ff>jHN)liaO^ySLvHiA&V!^@Q)%GDZc`yoyzi8o)_%8w&?KFJ2-q3T)}&b=%iW_pk=~0 zcH(Anlp5=uiQ(hNuVuS(pRYL51{|<<^}P3W#&lk(?E=~(^-g?WpOm&!UpX*4_a#3b4Q_+@(88gM^>!3K9oP1}57XG4DgNv5^v@<17D zZ;sxE`U$MwweNM=QpC$NduQ7xO--MxSMyG2Zsoi~F5G@n)&`0O1>d-G3B?e{ST(qs z2!$0*!9HEn(e^dYK~lXZEg^;X{RECz(w=8b31a~9r6p$#?f2E!BU4xzSFSRVq?{MJek2QLF9Z7khtBzNR`VRet!;u#Mu3QP+%X!^+K+9 zarSCa`vbP!z*bkOg(K`~4pA#zLG=UUI8=`H{8SJ%Eb$MA&MlxyXWkV*##D0Hb3N6c z);6c{^@XfVRBfhc{Lcy|;5+e=&(RHvEDC1I!TI~i_IW_cR(gidiTA#Xwl;x>6}aDW z4DJJ&XN^3KkEUm@Mhlv>>yq(n1Zt)044*|>5^hnB0xIOuR2Dv>7Z?*8Pe+GA0S<9)!7 z3rtgB@d63l#!XcI4|`JhbcD5sjEMDRDjIOO&D|Ci9aI8^DV2J}eTI=#MsV zYZH2x*%l2kV=0Ux?ebg3W&w`>D6KCRba)aR-rsv18a~<$5{sP){iYmvfFLlIlMz5WzRot$Y z1Ur=Y0(U|doGNh?0|=)`?@Ck>vLkw1+&r|x%vExLMUpv>0tFkF@JP4|zFutaXGD^;w9nF(NO4Wj&~?s*teFMp2V)R&gGP}q1~KReBix|D4)<25zDcEhQsK^q=9Y05 zsU@ude(|1^opChBT1QZvrc1i5BXcP5NBcJ1ScrPixqI1n`nL7&8D9UyDKRLW2$OfD z>)&C)!?$C&;SQt?*iZN$v}k{Kznl49d-8Hh0A^sdx6I_KwQu`C4O_-*h8&5=3zOFK zw=6UgSrTsq6^_2aLT60(vwX;G3OehZZ{E}(LB>Ix4a~>KT$uCO zJl);0KOmu#$~(Bs=r)VBCk?I;eWwq`ykbEYqz`n9oX$zP)y2K{c>u*{L`Hu0f03*` zCJdLb7VG(#*2G!Kd)KEi#=F0!;bJG@^=kgz`z^gB04~XWPJn^q2oKHMiW^>vT zWXy7KK5Xh#nU2tY@orV$V>Rr?$V?oOY(vsR-C$|!^KvAxxLlWNv{Po5?^l^gH_BX5 znh{|5v0II6*F=$5JSwpeZv1)qk^D?21xy7{Ro4(W$8J~M4G%You9gJ{DO$^y#iqbE zrz-mP2aK#8yP3`fVS0C|3maXChc-SVk!Pt0qow~*-r;O>VH@}P#jyAHRuAX!uTyvxN5IXZzX4i$pCk7T#)O z=78*0H>w4>y`n`^j*tQkjc$!b7z-=*mlJbp z{j-nrn0PJ2e+aqzhJ^%=*$O6!8Sb_}a0711c#F)8bgiA(`1B*8qO3FM_Vp%$6fcpU z->~Ucm+v!i<&@wIA(F($K3HC1rG>72=SF0@ZBk)Rds+)QY6G37k6QHFmkttwn{l|s z?OkwcIA+|%=s2SE+dDDyUiyVH5X5|bI?d-!2^Kwlv6`&1U7(M)BDdDL+Ss8EwA8`` zd|MjQa(pa3xBm?H6e=sRn=CjVwZqDqKA6yz~+GOwoy-BXr`yHO*VQ$*=O zB7sC$>`hWMA+|lOkPER^-C z+HqT{g_^0pt%*sdhxruix-m=m6bKCXdal(+vzkT<*mcIZ1SoRX`BK5P^%AjS%Nv3E zA&A%7LG{BJ>_yG^spg&tnpeIHPM|e_2_r9A*rK+hb=sesZ+L!;oh*__`>bBg|Mcu-2yla#y!ET4vH;8#rMEe z(r3ZVf^iK35|&agW9r=(lL|!|Doo2x$C!B=Wqcv;sj6Qgs{yDhP=kO)_751Buj!+&Vnyijhxh_>dYL$;;;3Y zns5E&EhWph!FN0;B4!3%n(Q~^;PWlJqpIr^5)*n1YxxD(S6G~9OKm}-8l>O4Y2-JwCh!z4 z3sDDhOgvNM#ni^rR?L$_PvI*WKhfYcjf;DxI7oA0#qh)1aSy~6dIyVPX2G7DEtRew z7G$q*8b=0m*8BM%^0)|KP9B;N`&z6O!WoZC37%U1$dtJ;U`>~hl7do@n59h5L$Z`) z&hW?#0=7Ld1~m<>%r=Kw2{cx5VzVgDD#K^-97UNtTKAio>fiTz14WYL`)*uNn)C2K z9U6AF?G|?KGK$Y;IisO~G4p))SCQ26QCeGCVn(F7c%G0?WGQ~NrxLiiMw|ZdzGUFQ zs0IwOkV(YOnZll`5ZQy02mq(#_&b;;MR1_!#nZ7o9ha$VL2M^)t-J@F&5wQr;#k}2 zzEpHC+ScdkY52PgFd9#fAq(T))lDwLEeCTf4Sn4`;h_LAaRPKWpLy14t0xsSH{eMy z<&@h-Dd@rRIM3Rd_+Xq%t)Cp^J0ZkU;s1ej(=9LD+XjYD3{a_`3NdJF%rA@V&kP)kpAS&m>#JksWzPoei5PBywXsy3Davn{)1-<5Pi<7wdSAsF1Db2)56wC z`lPEX0Nu;49f&EZhISeC&f0@d;YWb?obu*S1xuqqtUX3^}gk(P=4M=229q&o?w`|<< zTj7DMTaGP5(*hV@@y}c&L!Tek?Ja5MS1xgp9*|V--}Bz|*)6-^ESLRYjA897y-SRy zOQfL}+QxSlJCuJ+NT5WrB($l^wERb5DyeS-fU19ISkk7uts0Rz$aw}BKQjUD`=;aE zFF$FCb`_iXEtF^O-OIy!ldbr;rW+B;p!{s0M7rI>Vb92I&mGdZ?c8r#ZNvI+f9#1Q@&%jlLIz3o?~@Ys|G?4wJ(ib| zcNdP?7`~rr9gcQ%yfvNz5ju|`j|&c87OW|M!~CXOiOBnmB7n1X=G8eW0~Xqm;Vkm4 z?N#%(09l7et>2-+=gqlkn;iHkouWh~|IrDn|Ds3 zsJ@x zyj~@K-)rCRC_g_b_iwC#j~DJ&XYr4Bw3`UA=tJQZi&rQ#fUv)Bko}fOv|Ta7PmB1O zc&L?#QC4uW!BRq7k43t0>kU99K@iO~JP;k*^Ldx59tW~aOU0hJrM%{zXcF+S^jmT# zc5!Yc%NBmL0dj}pS|@42sEXB?bJKY(*1g%u#+8fd{tFR;$=9qpU1=B%ao+OOLQu?! z5V!$uIbskzu9{?d14ND0Jbn-i?~$1rsPAx&#ro$&=`B)D2}f=eCw@yMgeqSQebQqe z@iQmbl>P@D&0=cT*d8DZBV_>cB|E0M;;8qWR_VR18Bd$dSnKljAlZPchEdk@7`x_L zgdDeVz_(^J8|q3{mi&K99r)}vrD21(drl!GMY@XJV6BbGH-qcX_hLz59~@u%ULpMD zS#zQ9d=Gk&wAH?A<(kWN2%4}xpL*?d>&gf)O}Wgk$<#99TmPwkH4to3o!uu-RN{wTX=qK0vZCkN6iO zv=nZO2~m1p4{^j$kUh}r+3%JBH#~NqQ`PAow&|TGH~L+|*hLtA_7EPM>zm_M?D0R? zc_D=#kLL~ydpBiX8mn0?itW~-l+HaL_ZS1Ix$2v*7$P8)sX@x89WziG_p-Y(!LNfpmgkm)bu*oBu<&Gr= zcMh%R=YD#tUDYqoX}^{X+|Xhf&MFUhWkqx(EUTXbbJQ3%od(8c^oq=Nvz?B8up8w@ ziBBi!MSg=-9exg|muRT+d8K%75GWaqK6K$TYvv;K3F9UmKkpeG1hp~k9t#&v8v-4| z0fecmc%CYMamRKF0|GhL`P zfSrjGngj;+dEKouq82TMx@P-f*uoI9B;7VL-fS5l>LIn8c0i)S>n0_jVpc&nQ+*>L zdk6mD`QE6Z{)EMTx1}dTv^?xM87*^RGITZL&?~NMnCoO|`;mvzgtxZU(w&us`0F0u z3qzBPCj{m5EZUk0cd}*6E7mri*ur9Dj4%u05@(;|_5N<3@yz(|OW3H`k`}3ve-Wuf zYl9X9CnwcbqZ_i7uSFJ?((Z-X9yj5tp}L?lcBnW52&J@wA_z-F@#M$v?$0rONf$k9 zF5%<+v1y1f5N$_&4bcU3PCCpKiWY--9FeTL0Z?X8dS}HwiXiY6cz3i zX%CIgT^)qjl`ABnvyZlIUbkuY>8m7a+JJgd_jnX$#(}UbVIY;Z6<7W+%sf4WLik%d zyyF5>%r9nA+Ya_RmG86O#HOLWj%c)jrF5zkyqLwGU(Kyh>C{PFm*cMIzi9`~)RfX* z4$K8*s;wT|u5HW10Map&@o;~$*3|%t2Bc2?Q!LOkH*;85##Jc70TKN`EYY)dECN#N zhCZsLE)%{*nTqi$KC_{^`N0W2BCbGD^&s?Thku!(fdp-Q2ES3=gnx@6ZSrjvWF1aH%#{HV zDAyT<31+hE#7qnD3rS1dhemKCJE=M9U0<;{0r9ZuwL3OU(ZP>J6Ii>%o4%5adWi=5 zG%NfE(cW|zUpYH_1Ul^ry%O^ouFYu(j25Hthpti9k4J^UhR~yti&n)PJUe_>D%SDM zx0;s&IHlceG+f0X0Beo#xKuK}Xi?-Vw#RA-T}MMa&^SZpS*iEa{Yh{2vV;tTg4T6h%|(S zJZUv<1x%#_WZIi#aBRBEiAlT8+cVIJx2rUPyz zzqyq9ol$H74YmRAeS(@{%_q`wyePl5=+UM)jvC28KwuHysazJ&F93kD{_a>wd5a3s3v82%P zrQY{*QhNQx%f;6)odVO~SSnt9A)*~qtFi!K=+ihjwAkRH{h18_Z!cK5s=H9BOlUes z+_DgZYo2&!Z!7k6>F!Y2yrX^l*U!f!x8PWi)n$IgF5%idQKR~dGj!ejVj-i0Yz3vg zZ1*cmD$8#W+djfGGh(6k=&_lbg_T_`CGxEkjQYQdk3a!ucaU0p8Av7E2Xrz)wamlB zkY6cgEBU;7c49FNw&O^$EM%(`+;WGlt@4CW|B_AT30o@unXs!?cXi{qdTUAeqK$ew z)DKvyKu*2|4REM7hInPfmX9`Zy?WhB=bd5KQ63CiHn!s>#Peq5d$^K65LDZhx?R>q zq89Wjn*JT}He4|Y?K3yK-(T?e--A@@gnZ|7*44^Yjf88Sb%3n8EC#xk+d}4W7S*c_ ziIK6qkzO`!fynY(&d8JmvNdz?U)DuV@w(F*B;a8gf0qm4(OzKa5(gr4=F<3+rHC|E z4=RoB9YHXyVIR%XAI^Q!2$6vIVnm4fb;@1t_veC$sUMTAP`a118tTxO@vK82{M`Q{EKcY=)z~i zj-`|ug$S*Q550{zJ335M26*kdM@u7W4&687W=z{-ffCma(;Xo#SadUx9PMj818eDp zkj+P&y```HGT)}s+V-2z>su;vh22*sf2!}_RIjMUiUimS* zJ`LQSZyLg<(YuM=Ge*HrBZU%b45gW*HyYhKFUxxf4MmIxaMgzged#nv44=4KLIuqK zkUbB5*I#tX-G-1eQOv=P-Q$UaqqY>mwyft779y#e{%d;1rE-0VbAlFY?^0q_zeV1# zm22Tt%X}hBCVnPWF8#XHG>y0;pbXEfvznCcIsxoXF8d%)-*x&xr_EEpTj8&vogQ{h zm2&4UZUQ?1s4tI@9M-1}pffK8s?TJ=lCV7CELC?qE2;78&SQLazRLKrWSIIL5_qm-g61VS20g;h3c{s~9O zuK7#5uj_;rf$s(2)?qMAwi2R zQi)#5co@W~*NA_u;{joyGDO)e&+U;5juD%B{~10{Amq(P`y{lNTfgn*;) z>ORZWBMXx(@!LRSMRbz{G%}yOl)Bw|$(WTvT;`Sv4TwR~)q2&HJ-+jxdaZ=SOH8yy zlFr;%sa@5&j=Af1Z%K@&?a!RU^?{DHfslw-hT40kkUch$M10`^yrvO
s1!LEFHY_z24&=in|^D;_;8Zgvk?Zy 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 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!')