Add files via upload

This commit is contained in:
LukeSpad 2021-12-17 18:45:05 +00:00 committed by GitHub
parent d58e5a0c77
commit fded45c031
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 65 additions and 22 deletions

View file

@ -1,13 +1,14 @@
#!/usr/bin/python #!/usr/bin/python
import os import os
import unicodedata
from Foundation import NSAppleScript from Foundation import NSAppleScript
from ScriptingBridge import SBApplication from ScriptingBridge import SBApplication
''' Module defining a MusicController class for Apple Music, enables: ''' Module defining a MusicController class for Apple Music, enables:
basic transport control of the player, basic transport control of the player,
query of player status, query of player status,
playing existing playlists, and playing existing playlists,
running external appleScripts for more complex control running external appleScripts for more complex control, and
reporting messages in the notification centre''' reporting messages in the notification centre'''
PLAYSTATE = dict([ PLAYSTATE = dict([
@ -26,9 +27,14 @@ class MusicController(object):
# Player information # Player information
def get_current_track_info(self): def get_current_track_info(self):
name = str(self.app.currentTrack().name()) name = self.app.currentTrack().name()
album = str(self.app.currentTrack().album()) album = self.app.currentTrack().album()
artist = str(self.app.currentTrack().artist()) artist = self.app.currentTrack().artist()
if name:
# Deal with tracks with non-ascii characters such as accents
name = unicodedata.normalize('NFD', name).encode('ascii', 'ignore')
album = unicodedata.normalize('NFD', album).encode('ascii', 'ignore')
artist = unicodedata.normalize('NFD', artist).encode('ascii', 'ignore')
return [name, album, artist] return [name, album, artist]
def get_current_play_state(self): def get_current_play_state(self):
@ -48,6 +54,7 @@ class MusicController(object):
elif PLAYSTATE.get(self.app.playerState()) == 'Pause': elif PLAYSTATE.get(self.app.playerState()) == 'Pause':
self.app.playpause() self.app.playpause()
elif PLAYSTATE.get(self.app.playerState()) == 'Stop': elif PLAYSTATE.get(self.app.playerState()) == 'Stop':
self. app.setValue_forKey_('true', 'shuffleEnabled')
playlist = self.app.sources().objectWithName_("Library") playlist = self.app.sources().objectWithName_("Library")
playlist.playOnce_(None) playlist.playOnce_(None)
@ -79,6 +86,12 @@ class MusicController(object):
# ######################################################################################## # ########################################################################################
# More complex playback control functions # 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'): def set_current_track_position(self, time, mode='Relative'):
if mode == 'Relative': if mode == 'Relative':
# Set playback position in seconds relative to current position # Set playback position in seconds relative to current position
@ -102,10 +115,10 @@ class MusicController(object):
s.executeAndReturnError_(None) s.executeAndReturnError_(None)
@staticmethod @staticmethod
def notify(message): def notify(message, subtitle):
# Post message in notification center # Post message in notification center
path = os.path.dirname(os.path.abspath(__file__)) path = os.path.dirname(os.path.abspath(__file__))
script = 'tell application "' + path + '/Notify.app" to notify("BeoGateway", "' + \ script = 'tell application "' + path + '/Notify.app" to notify("BeoGateway", "' + \
message + '")' message + '", "' + subtitle + '")'
s = NSAppleScript.alloc().initWithSource_(script) s = NSAppleScript.alloc().initWithSource_(script)
s.executeAndReturnError_(None) s.executeAndReturnError_(None)

View file

@ -1,7 +1,14 @@
# Constants for B&O telegram protocols # Constants for B&O telegram protocols
# ######################################################################################## # ########################################################################################
# Config data (set on initialisation) # Config data (set on initialisation)
gateway = dict([("Current Audio Source", ''), ("Current Video Source", '')]) gateway = dict(
[
("Current Audio Source", 'Unknown'),
("Current Video Source", 'Unknown'),
("Now Playing", 'Unknown'),
("N.MUSIC Renderers", [])
]
)
rooms = [] rooms = []
devices = [] devices = []
available_sources = [] available_sources = []
@ -13,7 +20,7 @@ source_type_dict = dict(
[ [
("Video Sources", ("TV", "V.AUX/DTV2", "MEDIA", "V.TAPE/V.MEM/DVD2", "DVD", "CAMERA", ("Video Sources", ("TV", "V.AUX/DTV2", "MEDIA", "V.TAPE/V.MEM/DVD2", "DVD", "CAMERA",
"SAT/DTV", "PC", "WEB", "DOORCAM", "PHOTO", "USB2", "WEBMEDIA", "AV.IN", "SAT/DTV", "PC", "WEB", "DOORCAM", "PHOTO", "USB2", "WEBMEDIA", "AV.IN",
"HOMEMEDIA", "DNLA", "RECORDINGS", "CAMERA", "USB", "DNLA-DMR", "YOUTUBE", "HOMEMEDIA", "DVB_RADIO", "DNLA", "RECORDINGS", "CAMERA", "USB", "DNLA-DMR", "YOUTUBE",
"HOME.APP", "HDMI_1", "HDMI_2", "HDMI_3", "HDMI_4", "HDMI_5", "HDMI_6", "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", "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_6", "MATRIX_7", "MATRIX_8", "MATRIX_9", "MATRIX_10", "MATRIX_11",
@ -262,9 +269,9 @@ ml_command_type_dict = dict(
(0x3C, "TIMER"), (0x3C, "TIMER"),
(0x40, "CLOCK"), (0x40, "CLOCK"),
(0x44, "TRACK_INFO"), (0x44, "TRACK_INFO"),
# LOCKmANAGER_COMMAND: Lock to Determine what device issues source commands
# reference: https://tidsskrift.dk/daimipb/article/download/7043/6004/0
(0x45, "GOTO_SOURCE"), (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"), (0x5C, "LOCK_MANAGER_COMMAND"),
(0x6C, "DISTRIBUTION_REQUEST"), (0x6C, "DISTRIBUTION_REQUEST"),
(0x82, "TRACK_INFO_LONG"), (0x82, "TRACK_INFO_LONG"),
@ -301,7 +308,8 @@ ml_command_type_dict = dict(
# On power up all devices send out a request key telegram. If # 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 # 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 # Master in older implementations) then asserts a NEW_LOCKmANAGER telegram and assumes responsibility
# for LOCKmANAGER_COMMAND telegrams until a key transfer occurs. # for LOCKMANAGER_COMMAND telegrams until a key transfer occurs.
# reference: https://tidsskrift.dk/daimipb/article/download/7043/6004/0
(0x12, "KEY_LOST"), # ? (0x12, "KEY_LOST"), # ?
# Unknown command with payload of length 1. # Unknown command with payload of length 1.
# bit 0: unknown # bit 0: unknown
@ -344,9 +352,11 @@ ml_device_dict = dict(
(0x83, "ALL LINK DEVICES"), (0x83, "ALL LINK DEVICES"),
(0x80, "ALL"), (0x80, "ALL"),
(0xF0, "MLGW"), (0xF0, "MLGW"),
(0x21, "BLC NL/ML"),
# Power Master exists in older (pre 1996?) ML implementations. Later revisions enforced the Video Master # 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 # 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. # you may see this device type on the network.
# reference: https://tidsskrift.dk/daimipb/article/download/7043/6004/0
(0xFF, "POWER MASTER"), # ? (0xFF, "POWER MASTER"), # ?
] ]
) )
@ -433,7 +443,7 @@ destselectordict = dict(
[ [
(0x00, "Video Source"), (0x00, "Video Source"),
(0x01, "Audio Source"), (0x01, "Audio Source"),
(0x05, "V.TAPE/V.MEM"), (0x05, "Secondary Video Source (V.TAPE/V.MEM)"),
(0x0F, "All Products"), (0x0F, "All Products"),
(0x1B, "MLGW"), (0x1B, "MLGW"),
] ]
@ -451,9 +461,9 @@ mlgw_soundstatusdict = dict([(0x00, "Not muted"), (0x01, "Muted")])
mlgw_speakermodedict = dict( mlgw_speakermodedict = dict(
[ [
(0x01, "Center channel"), (0x01, "Center channel"),
(0x02, "2ch stereo"), (0x02, "2 channel stereo"),
(0x03, "Front surround"), (0x03, "Front surround"),
(0x04, "4ch stereo"), (0x04, "4 channel stereo"),
(0x05, "Full surround"), (0x05, "Full surround"),
(0xFD, "<all>"), # Dummy for 'Listen for all modes' (0xFD, "<all>"), # Dummy for 'Listen for all modes'
] ]

View file

@ -193,7 +193,10 @@ class MLCLIClient(asynchat.async_chat):
# Decode header # Decode header
message = OrderedDict() message = OrderedDict()
self._get_device_info(message, telegram) self._get_device_info(message, telegram)
message["from_device"] = self._get_device_name(self._dictsanitize(CONST.ml_device_dict, telegram[1])) if 'Device' not in message:
# If ML telegram has been matched to a Masterlink node in the devices list then the 'from_device'
# key is redundant - it will always be identical to the 'Device' key
message["from_device"] = self._dictsanitize(CONST.ml_device_dict, telegram[1])
message["from_source"] = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[5]) message["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_device"] = self._get_device_name(self._dictsanitize(CONST.ml_device_dict, telegram[0]))
message["to_source"] = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[4]) message["to_source"] = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[4])
@ -252,6 +255,10 @@ class MLCLIClient(asynchat.async_chat):
message["State_Update"]["sourceID"] = telegram[10] message["State_Update"]["sourceID"] = telegram[10]
message["State_Update"]["source_type"] = self._get_type(CONST.ml_selectedsource_type_dict, telegram[10]) message["State_Update"]["source_type"] = self._get_type(CONST.ml_selectedsource_type_dict, telegram[10])
message["State_Update"]["command"] = self._dictsanitize(CONST.beo4_commanddict, telegram[11]) message["State_Update"]["command"] = self._dictsanitize(CONST.beo4_commanddict, telegram[11])
if message["State_Update"]["command"] == 'Go/Play':
message["State_Update"]["state"] = 'Play'
elif message["State_Update"]["command"] in ['Standby', 'Stop', 'Wind', 'Rewind']:
message["State_Update"]["state"] = message["State_Update"]["command"]
# audio track info long # audio track info long
if message.get("payload_type") == "TRACK_INFO_LONG": if message.get("payload_type") == "TRACK_INFO_LONG":
@ -304,6 +311,8 @@ class MLCLIClient(asynchat.async_chat):
message["State_Update"]["source"] = source message["State_Update"]["source"] = source
message["State_Update"]["sourceID"] = telegram[11] message["State_Update"]["sourceID"] = telegram[11]
message["State_Update"]["source_type"] = self._get_type(CONST.ml_selectedsource_type_dict, telegram[11]) message["State_Update"]["source_type"] = self._get_type(CONST.ml_selectedsource_type_dict, telegram[11])
# This device is playing
message["State_Update"]["state"] = 'Play'
else: else:
message["State_Update"]["subtype"] = "Undefined: " + self._hexbyte(telegram[9]) message["State_Update"]["subtype"] = "Undefined: " + self._hexbyte(telegram[9])
@ -319,6 +328,8 @@ class MLCLIClient(asynchat.async_chat):
message["State_Update"]["source"] = source message["State_Update"]["source"] = source
message["State_Update"]["sourceID"] = telegram[11] message["State_Update"]["sourceID"] = telegram[11]
self._get_channel_track(telegram, message) self._get_channel_track(telegram, message)
# Device sending goto source command is playing
message["State_Update"]["state"] = 'Play'
# remote request # remote request
if message.get("payload_type") == "MLGW_REMOTE_BEO4": if message.get("payload_type") == "MLGW_REMOTE_BEO4":

View file

@ -103,8 +103,11 @@ class MLConfig:
for selectCmd in source["selectCmds"]: for selectCmd in source["selectCmds"]:
if gateway_type == 'blgw': if gateway_type == 'blgw':
# get source information from the BLGW config file # get source information from the BLGW config file
source_id = str(source['sourceId'].split(':')[0]) if str(source['sourceId']) == '':
source_id = self._srcdictsanitize(CONST.blgw_srcdict, source_id).upper() source_id = self._srcdictsanitize(CONST.beo4_commanddict, source['selectID']).upper()
else:
source_id = str(source['sourceId'].split(':')[0])
source_id = self._srcdictsanitize(CONST.blgw_srcdict, source_id).upper()
device['Sources'][str(source["name"])]['source'] = source_id device['Sources'][str(source["name"])]['source'] = source_id
device['Sources'][str(source["name"])]['uniqueID'] = str(source['sourceId']) device['Sources'][str(source["name"])]['uniqueID'] = str(source['sourceId'])
else: else:
@ -142,7 +145,7 @@ class MLConfig:
self.log.info('\tFound ' + str(len(CONST.available_sources)) + ' Available Sources [Name, Type]:') self.log.info('\tFound ' + str(len(CONST.available_sources)) + ' Available Sources [Name, Type]:')
for i in range(len(CONST.available_sources)): for i in range(len(CONST.available_sources)):
self.log.info('\t\t' + str(list(CONST.available_sources[i]))) self.log.info('\t\t' + str(list(CONST.available_sources[i])))
self.log.info('Done!\n') self.log.info('\tDone!\n')
self.log.debug(json.dumps(CONST.gateway, indent=4)) self.log.debug(json.dumps(CONST.gateway, indent=4))
self.log.debug(json.dumps(CONST.rooms, indent=4)) self.log.debug(json.dumps(CONST.rooms, indent=4))
@ -169,6 +172,7 @@ class MLConfig:
mlcli.last_message['State_Update']['command'] == "Light Timeout": mlcli.last_message['State_Update']['command'] == "Light Timeout":
device['ML_ID'] = mlcli.last_message.get('to_device') device['ML_ID'] = mlcli.last_message.get('to_device')
device['Serial_num'] = 'NA'
self.log.info("\tMasterLink ID of product " + self.log.info("\tMasterLink ID of product " +
device.get('Device') + " is " + device.get('ML_ID') + ".\n") device.get('Device') + " is " + device.get('ML_ID') + ".\n")
test = False test = False
@ -177,9 +181,14 @@ class MLConfig:
else: else:
# If this is a NetLink product then it has a serial number and no ML_ID # If this is a NetLink product then it has a serial number and no ML_ID
device['ML_ID'] = 'NA' if device['Device'] == 'BeoMaster 7000':
self.log.info("\tNetworkLink ID of product " + device.get('Device') + " is " + device['ML_ID'] = "AUDIO MASTER"
device.get('Serial_num') + ". No MasterLink ID assigned.\n") self.log.info("\tNetworkLink ID of product " + device.get('Device') + " is " +
device.get('Serial_num') + ". MasterLink ID manually assigned to AUDIO MASTER.\n")
else:
device['ML_ID'] = 'NA'
self.log.info("\tNetworkLink ID of product " + device.get('Device') + " is " +
device.get('Serial_num') + ". No MasterLink ID assigned.\n")
self.log.debug(json.dumps(device, indent=4)) self.log.debug(json.dumps(device, indent=4))