mirror of
https://github.com/LukeSpad/BeoGateway.git
synced 2024-12-23 21:51:51 +00:00
1415 lines
66 KiB
Python
1415 lines
66 KiB
Python
try:
|
|
import indigo
|
|
except ImportError:
|
|
pass
|
|
import asyncore
|
|
import json
|
|
import time
|
|
import logging
|
|
import requests
|
|
import threading
|
|
import os
|
|
from datetime import datetime
|
|
|
|
import Resources.CONSTANTS as CONST
|
|
import Resources.MLCONFIG as MLCONFIG
|
|
import Resources.MLGW_CLIENT as MLGW
|
|
import Resources.MLCLI_CLIENT as MLCLI
|
|
import Resources.BLHIP_CLIENT as BLHIP
|
|
import Resources.MLtn_CLIENT as MLtn
|
|
import Resources.ASBridge as ASBridge
|
|
|
|
|
|
class Plugin(indigo.PluginBase):
|
|
|
|
def __init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs):
|
|
indigo.PluginBase.__init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs)
|
|
|
|
# Instantiate an AppleScriptBridge MusicController for N.MUSIC control of apple Music
|
|
self.iTunes = ASBridge.MusicController()
|
|
|
|
def startup(self):
|
|
# Get path to plugin config file
|
|
self.path = os.path.abspath(os.getcwd())[:-54] + \
|
|
"Preferences/Plugins/uk.co.lukes_plugins.BeoGateway.plugin.indiPref"
|
|
|
|
# If plugin is configured, initialise all clients and set config flag to True
|
|
if os.path.exists(self.path):
|
|
self.configure_clients()
|
|
self.configured = True
|
|
else:
|
|
self.configured = False
|
|
|
|
def triggerStartProcessing(self, trigger):
|
|
self.triggers.append(trigger)
|
|
|
|
def getDeviceStateList(self, dev):
|
|
stateList = indigo.PluginBase.getDeviceStateList(self, dev)
|
|
|
|
if stateList is not None:
|
|
if dev.deviceTypeId in self.devicesTypeDict and dev.deviceTypeId == u"AVrenderer":
|
|
# Add dynamic states onto stateList for devices of type AV renderer
|
|
# This allows devices to have unique source lists, rather than sharing a common source list
|
|
try:
|
|
sources = dev.pluginProps['sources']
|
|
for source_name in sources:
|
|
stateList.append(
|
|
{
|
|
"Disabled": False,
|
|
"Key": "source." + source_name,
|
|
"StateKey": sources[source_name]['source'],
|
|
"StateLabel": "Source is " + source_name,
|
|
"TriggerLabel": "Source is " + source_name,
|
|
"Type": 50
|
|
}
|
|
)
|
|
except KeyError:
|
|
indigo.server.log("Device " + dev.name + " does not have state key 'sources'\n",
|
|
level=logging.WARNING)
|
|
pass
|
|
return stateList
|
|
|
|
def deviceStartComm(self, dev):
|
|
dev.stateListOrDisplayStateIdChanged()
|
|
|
|
def __del__(self):
|
|
indigo.PluginBase.__del__(self)
|
|
|
|
# ########################################################################################
|
|
# ##### Indigo UI menu constructors
|
|
@staticmethod
|
|
def zonelistgenerator(filter="", valuesDict=None, typeId="", targetId=0):
|
|
myarray = [("*", "All"), ("Main", "Main")]
|
|
for room in CONST.rooms:
|
|
if (room['Zone'], room['Zone']) not in myarray:
|
|
myarray.append((room['Zone'], room['Zone']))
|
|
return myarray
|
|
|
|
@staticmethod
|
|
def roomlistgenerator(filter="", valuesDict=None, typeId="", targetId=0):
|
|
myarray = [("99", "Any")]
|
|
for room in CONST.rooms:
|
|
myarray.append((room['Room_Number'], room['Room_Name']))
|
|
return myarray
|
|
|
|
@staticmethod
|
|
def roomlistgenerator2(filter="", valuesDict=None, typeId="", targetId=0):
|
|
myarray = [("*", "All"), ("global", "global")]
|
|
for room in CONST.rooms:
|
|
myarray.append((room['Room_Name'], room['Room_Name']))
|
|
return myarray
|
|
|
|
@staticmethod
|
|
def keylistgenerator(filter="", valuesDict=None, typeId="", targetId=0):
|
|
myarray = []
|
|
for item in CONST.beo4_commanddict.items():
|
|
myarray.append(item)
|
|
return myarray
|
|
|
|
@staticmethod
|
|
def keylistgenerator2(filter="", valuesDict=None, typeId="", targetId=0):
|
|
myarray = []
|
|
for item in CONST.beoremoteone_keydict.items():
|
|
myarray.append(item)
|
|
return myarray
|
|
|
|
@staticmethod
|
|
def beo4sourcelistgenerator(filter="", valuesDict=None, typeId="", targetId=0):
|
|
myarray = []
|
|
for item in CONST.beo4_srcdict.items():
|
|
myarray.append(item)
|
|
return myarray
|
|
|
|
@staticmethod
|
|
def beo4sourcelistgenerator2(filter="", valuesDict=None, typeId="", targetId=0):
|
|
myarray = [('Any Source', 'Any Source'), ('Any Audio', 'Any Audio'), ('Any Video', 'Any Video')]
|
|
for item in CONST.beo4_srcdict.items():
|
|
myarray.append(item)
|
|
return myarray
|
|
|
|
@staticmethod
|
|
def br1sourcelistgenerator(filter="", valuesDict=None, typeId="", targetId=0):
|
|
myarray = []
|
|
for item in CONST.available_sources:
|
|
myarray.append(item)
|
|
return myarray
|
|
|
|
@staticmethod
|
|
def destinationlistgenerator(filter="", valuesDict=None, typeId="", targetId=0):
|
|
myarray = []
|
|
for item in CONST.destselectordict.items():
|
|
myarray.append(item)
|
|
return myarray
|
|
|
|
@staticmethod
|
|
def srcactivitylistgenerator(filter="", valuesDict=None, typeId="", targetId=0):
|
|
myarray = [(0x00, "Unknown")]
|
|
for item in CONST.sourceactivitydict.items():
|
|
if item not in myarray:
|
|
myarray.append(item)
|
|
return myarray
|
|
|
|
@staticmethod
|
|
def hiptypelistgenerator(filter="", valuesDict=None, typeId="", targetId=0):
|
|
myarray = []
|
|
for item in CONST.blgw_devtypes.items():
|
|
myarray.append(item)
|
|
return myarray
|
|
|
|
def playlistgenerator(self, filter="", valuesDict=None, typeId="", targetId=0):
|
|
return self.iTunes.get_playlist_names()
|
|
|
|
# ########################################################################################
|
|
# ##### Indigo UI Prefs
|
|
def set_login(self, ui):
|
|
# If LogIn data is updated in config, update the values in the plugin
|
|
self.user = ui.get('userID')
|
|
self.pwd = ui.get('password')
|
|
indigo.server.log("BeoGateway device login details updated!", level=logging.DEBUG)
|
|
|
|
def set_gateway(self, ui):
|
|
# If gateway network address data is updated in config, update the values in the plugin
|
|
self.host = ui.get('address')
|
|
self.port = [int(ui.get('mlgw_port')),
|
|
int(ui.get('hip_port')),
|
|
23] # Telnet port - 23
|
|
indigo.server.log("BeoGateway device network address details updated!", level=logging.DEBUG)
|
|
|
|
def set_trackmode(self, ui):
|
|
# If Track Mode setting is updated in config, update the value in the plugin
|
|
self.trackmode = ui.get("trackMode")
|
|
indigo.server.log("Track reporting set to " + str(self.trackmode), level=logging.DEBUG)
|
|
|
|
def set_verbose(self, ui):
|
|
# If Verbose Mode setting is updated in config, update the value in the plugin
|
|
self.verbose = ui.get("verboseMode")
|
|
indigo.server.log("Verbose Mode set to " + str(self.verbose), level=logging.DEBUG)
|
|
|
|
def set_notifymode(self, ui):
|
|
# If Notify Mode setting is updated in config, update the value in the plugin
|
|
self.notifymode = ui.get("notifyMode")
|
|
indigo.server.log("Notification Mode set to " + str(self.notifymode), level=logging.DEBUG)
|
|
|
|
def set_debug(self, ui):
|
|
# If Debug Mode setting is updated in config, update the value in the plugin
|
|
self.debug = ui.get("debugMode")
|
|
|
|
# Set the debug flag for the clients
|
|
self.mlcli.debug = self.debug
|
|
self.mlgw.debug = self.debug
|
|
if self.mlcli.isBLGW:
|
|
self.blgw.debug = self.debug
|
|
else:
|
|
self.mltn.debug = self.debug
|
|
|
|
# Report the debug flag change
|
|
indigo.server.log("Debug Mode set to " + str(self.debug), level=logging.DEBUG)
|
|
|
|
def set_music_control(self, ui):
|
|
# If Apple Music Control setting is updated in config, update the value in the plugin
|
|
self.itunes_control = ui.get("iTunesControl")
|
|
self.itunes_source = ui.get("iTunesSource")
|
|
|
|
if self.itunes_control:
|
|
indigo.server.log("Apple Music Control enabled on source: " + str(self.itunes_source), level=logging.DEBUG)
|
|
else:
|
|
indigo.server.log("Apple Music Control set to " + str(self.itunes_control), level=logging.DEBUG)
|
|
|
|
def set_default_audio(self, ui):
|
|
# Define default audio source for AVrenderers
|
|
self.default_audio_source = ui.get("defaultAudio")
|
|
indigo.server.log("Default Audio Source set to " + str(self.default_audio_source), level=logging.DEBUG)
|
|
|
|
def set_playlist_default(self, ui):
|
|
self.playlist_default = ui.get("playlist_default")
|
|
indigo.server.log("Default Apple Music Playlist set to " + str(self.playlist_default), level=logging.DEBUG)
|
|
|
|
def set_playlist_green(self, ui):
|
|
self.playlist_green = ui.get("playlist_green")
|
|
indigo.server.log("Green Key Apple Music Playlist set to " + str(self.playlist_green), level=logging.DEBUG)
|
|
|
|
def set_playlist_yellow(self, ui):
|
|
self.playlist_yellow = ui.get("playlist_yellow")
|
|
indigo.server.log("Yellow Key Music Playlist set to " + str(self.playlist_yellow), level=logging.DEBUG)
|
|
|
|
def set_playlist_red(self, ui):
|
|
self.playlist_red = ui.get("playlist_red")
|
|
indigo.server.log("Red Key Music Playlist set to " + str(self.playlist_red), level=logging.DEBUG)
|
|
|
|
def set_playlist_blue(self, ui):
|
|
self.playlist_blue = ui.get("playlist_blue")
|
|
indigo.server.log("Blue Key Music Playlist set to " + str(self.playlist_blue), level=logging.DEBUG)
|
|
|
|
# ########################################################################################
|
|
# ##### Indigo UI Actions
|
|
def send_beo4_key(self, action, device):
|
|
device_id = int(device.address)
|
|
key_code = int(action.props.get("keyCode", 0))
|
|
destination = int(action.props.get("destination", 0))
|
|
link = int(action.props.get("linkcmd", 0))
|
|
if destination == 0x06:
|
|
self.mlgw.send_beo4_cmd(device_id, destination, key_code, 0x01, link)
|
|
else:
|
|
self.mlgw.send_beo4_cmd(device_id, destination, key_code, 0x00, link)
|
|
|
|
def send_beo4_src(self, action, device):
|
|
device_id = int(device.address)
|
|
key_code = int(action.props.get("keyCode", 0))
|
|
destination = int(action.props.get("destination", 0))
|
|
link = int(action.props.get("linkcmd", 0))
|
|
if destination == 0x06:
|
|
self.mlgw.send_beo4_cmd(device_id, destination, key_code, 0x01, link)
|
|
else:
|
|
self.mlgw.send_beo4_cmd(device_id, destination, key_code, 0x00, link)
|
|
|
|
def send_br1_key(self, action, device):
|
|
device_id = int(device.address)
|
|
key_code = int(action.props.get("keyCode", 0))
|
|
network_bit = int(action.props.get("netBit", 0))
|
|
self.mlgw.send_beoremoteone_cmd(device_id, key_code, network_bit)
|
|
|
|
def send_br1_src(self, action, device):
|
|
device_id = int(device.address)
|
|
key_code = str(action.props.get("keyCode", 0))
|
|
network_bit = int(action.props.get("netBit", 0))
|
|
try:
|
|
key_code = CONST.beoremoteone_commanddict.get(key_code)
|
|
self.mlgw.send_beoremoteone_select_source(device_id, key_code[0], key_code[1], network_bit)
|
|
except KeyError:
|
|
pass
|
|
|
|
def send_hip_cmd(self, action):
|
|
zone = str(action.props.get("zone", 0))
|
|
room = str(action.props.get("room", 0))
|
|
device_type = str(action.props.get("devType", 0))
|
|
device_id = str(action.props.get("deviceID", 0))
|
|
hip_command = str(action.props.get("hip_cmd", 0))
|
|
telegram = "c " + zone + "/" + room + "/" + device_type + "/" + device_id + "/" + hip_command
|
|
if self.mlcli.isBLGW:
|
|
self.blgw.send_cmd(telegram)
|
|
|
|
def send_hip_cmd2(self, action):
|
|
hip_command = str(action.props.get("hip_cmd", 0))
|
|
if self.mlcli.isBLGW:
|
|
self.blgw.send_cmd(hip_command)
|
|
|
|
def send_hip_query(self, action):
|
|
zone = str(action.props.get("zone", 0))
|
|
room = str(action.props.get("room", 0))
|
|
device_type = str(action.props.get("devType", 0))
|
|
device_id = str(action.props.get("deviceID", 0))
|
|
if self.mlcli.isBLGW:
|
|
self.blgw.query(zone, room, device_type, device_id)
|
|
|
|
def send_bnr(self, action):
|
|
cmd_type = str(action.props.get("cmd_type", 0))
|
|
command = str(action.props.get("bnr_cmd", 0))
|
|
cmd_data = str(action.props.get("cmd_data", 0))
|
|
header = '' # {'Content-Type': 'application/json'}
|
|
|
|
try:
|
|
if cmd_type == "GET":
|
|
response = requests.get(url=command, headers=header, timeout=1)
|
|
elif cmd_type == "POST":
|
|
response = requests.post(url=command, headers=header, data=cmd_data, timeout=1)
|
|
if cmd_type == "PUT":
|
|
response = requests.put(url=command, headers=header, data=cmd_data, timeout=1)
|
|
else:
|
|
response = ''
|
|
|
|
if response.content:
|
|
response = json.loads(response.content)
|
|
indigo.server.log(json.dumps(response, indent=4), level=logging.DEBUG)
|
|
except requests.ConnectionError as e:
|
|
indigo.server.log("Unable to process BeoNetRemote Command - " + str(e), level=logging.ERROR)
|
|
|
|
def request_state_update(self, action, device):
|
|
action_id = str(action.props.get("id", 0))
|
|
zone = str(device.pluginProps['zone'])
|
|
room = str(device.pluginProps['room'])
|
|
device_type = "AV renderer"
|
|
device_id = str(device.name)
|
|
if self.mlcli.isBLGW:
|
|
self.blgw.query(zone, room, device_type, device_id)
|
|
|
|
def send_virtual_button(self, action):
|
|
button_id = int(action.props.get("buttonID", 0))
|
|
button_action = int(action.props.get("action", 0))
|
|
self.mlgw.send_virtualbutton(button_id, button_action)
|
|
|
|
def post_notification(self, action):
|
|
title = str(action.props.get("title", 0))
|
|
body = str(action.props.get("body", 0))
|
|
self.iTunes.notify(body, title)
|
|
|
|
def all_standby(self, action):
|
|
self.mlgw.send_beo4_cmd(1, CONST.CMDS_DEST.get("ALL PRODUCTS"), CONST.BEO4_CMDS.get("STANDBY"))
|
|
|
|
def request_serial_number(self):
|
|
self.mlgw.get_serial()
|
|
|
|
def request_device_update(self):
|
|
if self.mlcli.isBLGW:
|
|
self.blgw.query(dev_type="AV renderer")
|
|
|
|
def reset_clients(self):
|
|
self.check_connection(self.mlgw)
|
|
self.check_connection(self.mlcli)
|
|
|
|
if self.mlcli.isBLGW:
|
|
self.check_connection(self.blgw)
|
|
else:
|
|
self.check_connection(self.mltn)
|
|
|
|
# ########################################################################################
|
|
# ##### Indigo UI Events
|
|
def light_key(self, message):
|
|
room = message['room_number']
|
|
key_code = CONST.BEO4_CMDS.get(message['command'].upper())
|
|
|
|
for trigger in self.triggers:
|
|
props = trigger.globalProps["uk.co.lukes_plugins.BeoGateway.plugin"]
|
|
if trigger.pluginTypeId == "lightKey" and \
|
|
(props["room"] == str(99) or props["room"] == str(room)) and \
|
|
props["keyCode"] == str(key_code):
|
|
indigo.server.log("Executing Trigger: " + trigger.name, level=logging.DEBUG)
|
|
indigo.trigger.execute(trigger)
|
|
break
|
|
|
|
def control_key(self, message):
|
|
room = message['room_number']
|
|
key_code = CONST.BEO4_CMDS.get(message['command'].upper())
|
|
|
|
for trigger in self.triggers:
|
|
props = trigger.globalProps["uk.co.lukes_plugins.BeoGateway.plugin"]
|
|
if trigger.pluginTypeId == "controlKey" and \
|
|
(props["room"] == str(99) or props["room"] == str(room)) and \
|
|
props["keyCode"] == str(key_code):
|
|
indigo.server.log("Executing Trigger: " + trigger.name, level=logging.DEBUG)
|
|
indigo.trigger.execute(trigger)
|
|
break
|
|
|
|
def beo4_key(self, message):
|
|
source = message['State_Update']['source']
|
|
source_type = message['State_Update']['source']
|
|
key_code = CONST.BEO4_CMDS.get(message['State_Update']['command'].upper())
|
|
|
|
for trigger in self.triggers:
|
|
props = trigger.globalProps["uk.co.lukes_plugins.BeoGateway.plugin"]
|
|
if trigger.pluginTypeId == "beo4Key" and props["keyCode"] == str(key_code):
|
|
if props["sourceType"] == "Any Source":
|
|
indigo.trigger.execute(trigger)
|
|
break
|
|
elif props["sourceType"] == "Any Audio" and "AUDIO" in source_type:
|
|
indigo.trigger.execute(trigger)
|
|
break
|
|
elif props["sourceType"] == "Any Audio" and "AUDIO" in source_type:
|
|
indigo.trigger.execute(trigger)
|
|
break
|
|
elif props["sourceType"] == source:
|
|
indigo.trigger.execute(trigger)
|
|
break
|
|
|
|
def virtual_button(self, message):
|
|
button_id = message['button']
|
|
action = message['action']
|
|
for trigger in self.triggers:
|
|
props = trigger.globalProps["uk.co.lukes_pugins.mlgw.plugin"]
|
|
if trigger.pluginTypeId == "virtualButton" and \
|
|
props["buttonID"] == str(button_id) and \
|
|
props["action"] == str(action):
|
|
indigo.trigger.execute(trigger)
|
|
break
|
|
|
|
# ########################################################################################
|
|
# ##### Indigo UI Device Controls
|
|
def actionControlDevice(self, action, node):
|
|
""" Callback Method to Control a Relay Device. """
|
|
if node.deviceTypeId == u"AVrenderer":
|
|
if action.deviceAction == indigo.kDeviceAction.TurnOn:
|
|
self._dev_on(node)
|
|
elif action.deviceAction == indigo.kDeviceAction.TurnOff:
|
|
self._dev_off(node)
|
|
elif action.deviceAction == indigo.kDeviceAction.Toggle:
|
|
if node.states["onOffState"]:
|
|
self._dev_off(node)
|
|
else:
|
|
self._dev_on(node)
|
|
elif action.deviceAction == indigo.kDeviceAction.RequestStatus:
|
|
self._status_request(node)
|
|
|
|
def _dev_on(self, node):
|
|
indigo.server.log(node.name + " turned On")
|
|
|
|
# Get a local copy of the gateway states from server
|
|
active_renderers = self.gateway.states['AudioRenderers']
|
|
active_source = self.gateway.states['currentAudioSource']
|
|
active_sourceName = self.gateway.states['currentAudioSourceName']
|
|
|
|
if self.debug:
|
|
indigo.server.log('Active renderers: ' + active_renderers, level=logging.DEBUG)
|
|
indigo.server.log('Active Audio Source: ' + active_source, level=logging.DEBUG)
|
|
|
|
# Join if music already playing
|
|
if active_renderers != '' and active_source != 'Unknown':
|
|
# Send Beo4 command
|
|
source = active_source
|
|
self.mlgw.send_beo4_cmd(
|
|
int(node.address),
|
|
int(CONST.CMDS_DEST.get("AUDIO SOURCE")),
|
|
int(CONST.BEO4_CMDS.get(source))
|
|
)
|
|
|
|
# Update device states
|
|
sourceName = active_sourceName
|
|
key_value_list = [
|
|
{'key': 'onOffState', 'value': True},
|
|
{'key': 'playState', 'value': 'Play'},
|
|
{'key': 'source', 'value': sourceName},
|
|
{'key': 'mute', 'value': False},
|
|
]
|
|
|
|
if self.debug:
|
|
indigo.server.log(
|
|
node.name + " joining current audio experience " + sourceName + " (" + source +
|
|
"). Joining active renderer(s): " + active_renderers,
|
|
level=logging.DEBUG
|
|
)
|
|
|
|
# Otherwise start default music source
|
|
else:
|
|
# Send Beo4 command
|
|
source = self.default_audio_source
|
|
self.mlgw.send_beo4_cmd(
|
|
int(node.address),
|
|
int(CONST.CMDS_DEST.get("AUDIO SOURCE")),
|
|
int(CONST.BEO4_CMDS.get(source))
|
|
)
|
|
|
|
# Update device states
|
|
sourceName = dict(CONST.available_sources).get(self.default_audio_source)
|
|
key_value_list = [
|
|
{'key': 'onOffState', 'value': True},
|
|
{'key': 'playState', 'value': 'Play'},
|
|
{'key': 'source', 'value': sourceName},
|
|
{'key': 'mute', 'value': False},
|
|
]
|
|
|
|
if self.debug:
|
|
indigo.server.log(
|
|
node.name + " starting audio experience " + sourceName + " (" + source + ").",
|
|
level=logging.DEBUG
|
|
)
|
|
|
|
# Update states on server
|
|
node.updateStatesOnServer(key_value_list)
|
|
node.updateStateImageOnServer(indigo.kStateImageSel.AvPlaying)
|
|
|
|
# Add device to active renderers lists and update gateway
|
|
self.add_to_renderers_list(node.name, 'Audio')
|
|
key_value_list = [
|
|
{'key': 'currentAudioSource', 'value': source},
|
|
{'key': 'currentAudioSourceName', 'value': sourceName},
|
|
]
|
|
self.gateway.updateStatesOnServer(key_value_list)
|
|
|
|
def _dev_off(self, node):
|
|
indigo.server.log(node.name + " turned Off")
|
|
|
|
# Send Beo4 command
|
|
self.mlgw.send_beo4_cmd(
|
|
int(node.address),
|
|
int(CONST.CMDS_DEST.get("AUDIO SOURCE")),
|
|
int(CONST.BEO4_CMDS.get('STANDBY'))
|
|
)
|
|
|
|
# Update states to standby values
|
|
node.updateStatesOnServer(CONST.standby_state)
|
|
node.updateStateImageOnServer(indigo.kStateImageSel.PowerOff)
|
|
|
|
# Remove device from active renderers lists
|
|
self.remove_from_renderers_list(node.name, 'All')
|
|
|
|
def _status_request(self, node):
|
|
if node.pluginProps['serial_no'] == 'NA': # Check if this is a NetLink device: if no serial number then false
|
|
# Potentially send a status update command but these may only work over 2 way IR, not the MasterLink
|
|
self.mlgw.send_beo4_cmd(
|
|
int(node.address),
|
|
int(CONST.CMDS_DEST.get("AUDIO SOURCE")),
|
|
int(CONST.BEO4_CMDS.get("STATUS"))
|
|
)
|
|
|
|
indigo.server.log(node.name + " does not support status requests")
|
|
else: # If netlink, request a status update
|
|
self.blgw.query(dev_type="AV renderer", device=node.name)
|
|
|
|
# ########################################################################################
|
|
# Define callback function for message return from B&O Gateway
|
|
def cb(self, name, header, payload, message):
|
|
# ########################################################################################
|
|
# Message handler
|
|
# Handle Beo4 Command Events
|
|
try:
|
|
if message['payload_type'] == "BEO4_KEY":
|
|
self.beo4_key(message)
|
|
except KeyError:
|
|
pass
|
|
|
|
# Handle Light and Command Events
|
|
try:
|
|
if message['Type'] == "LIGHT COMMAND":
|
|
self.light_key(message)
|
|
elif message['Type'] == "CONTROL COMMAND":
|
|
self.control_key(message)
|
|
except KeyError:
|
|
pass
|
|
|
|
# Handle Virtual Button Events
|
|
try:
|
|
if message['payload_type'] == "MLGW virtual button event":
|
|
self.virtual_button(message)
|
|
except KeyError:
|
|
pass
|
|
|
|
# Handle all standby events
|
|
try:
|
|
if message["command"] == "All Standby":
|
|
for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
|
|
node.updateStatesOnServer(CONST.standby_state)
|
|
node.updateStateImageOnServer(indigo.kStateImageSel.PowerOff)
|
|
|
|
indigo.devices['Bang and Olufsen Gateway'].updateStatesOnServer(CONST.gw_all_stb)
|
|
except KeyError:
|
|
pass
|
|
|
|
# Handle AV Events
|
|
try:
|
|
# Use regular incoming messages to sync nowPlaying data
|
|
if self.gateway.states['AudioRenderers'] != '' and \
|
|
self.gateway.states['currentAudioSource'] == str(self.itunes_source) and \
|
|
self.itunes_control:
|
|
self._get_itunes_track_info(message)
|
|
|
|
# For messages of type AV RENDERER, scan keys to update device states
|
|
if message["Type"] in ["AV RENDERER", "RESPONSE"]:
|
|
# Tidy up messages
|
|
self.av_sanitise(message)
|
|
# Filter messages that don't constitute meaningful state updates
|
|
actionable = self.filter_messages(message)
|
|
if self.debug:
|
|
indigo.server.log("Process message: " + str(actionable), level=logging.WARNING)
|
|
if actionable:
|
|
# Keep track of what sources are playing on the network
|
|
self.src_tracking(message)
|
|
# Update individual devices based on updates
|
|
self.dev_update(message)
|
|
except KeyError:
|
|
pass
|
|
|
|
# Report message content to log
|
|
if self.verbose:
|
|
if 'State_Update' in message and 'CONNECTION' in message['State_Update']:
|
|
# Don't print pong responses from regular client ping to check sockets are open -
|
|
# approx every 600 seconds
|
|
pass
|
|
elif 'Device' in message and message['Device'] == 'Clock':
|
|
# Don't print the Clock sync telegrams from the HIP -
|
|
# approx every 60 seconds
|
|
pass
|
|
elif 'payload_type' in message and message['payload_type'] == '0x14':
|
|
# Don't print the Clock sync telegrams from the ML -
|
|
# approx every 6 seconds
|
|
pass
|
|
elif 'payload_type' in message and message['payload_type'] == 'CLOCK' and not self.debug:
|
|
# Don't print the Clock message telegrams from the ML
|
|
pass
|
|
else:
|
|
self.message_log(name, header, payload, message)
|
|
|
|
# Report Thread Count
|
|
if self.debug:
|
|
thread_count = int(threading.active_count())
|
|
if thread_count > 1:
|
|
indigo.server.log("Current Thread Count = " + str(thread_count), level=logging.DEBUG)
|
|
|
|
# ########################################################################################
|
|
# AV Handler Functions
|
|
|
|
# #### Message Conditioning
|
|
def av_sanitise(self, message):
|
|
# Sanitise AV messages
|
|
try: # Check for missing source information
|
|
if message['State_Update']['source'] in [None, 'None', '']:
|
|
message['State_Update']['source'] = 'Unknown'
|
|
message['State_Update']['sourceName'] = 'Unknown'
|
|
|
|
# Update for standby condition
|
|
message['State_Update']['state'] = "Standby"
|
|
except KeyError:
|
|
pass
|
|
|
|
try: # Check for unknown state
|
|
if message['State_Update']['state'] in [None, 'None', '']:
|
|
message['State_Update']['state'] = 'Unknown'
|
|
except KeyError:
|
|
pass
|
|
|
|
try: # Sanitise unknown Channel/Tracks
|
|
if message['State_Update']['nowPlayingDetails']['channel_track'] in [0, 255, '0', '255']:
|
|
del message['State_Update']['nowPlayingDetails']['channel_track']
|
|
except KeyError:
|
|
pass
|
|
|
|
try: # Add sourceName if not in message block
|
|
if 'sourceName' not in message['State_Update']:
|
|
# Find the sourceName from the source list for this device
|
|
if 'Device' in message: # If device known use local source data
|
|
self.find_source_name(message['State_Update']['source'],
|
|
indigo.devices[message['Device']].pluginProps['sources'])
|
|
else: # If device not known use global source data
|
|
message['State_Update']['sourceName'] = \
|
|
dict(CONST.available_sources).get(message['State_Update']['source'])
|
|
except KeyError:
|
|
pass
|
|
|
|
try:
|
|
if message['State_Update']['source'] == str(self.itunes_source) and \
|
|
message['State_Update']['nowPlaying'] in [0, '0', '', 'Unknown']:
|
|
# If the source is iTunes and current track unknown then set to currently playing track
|
|
indigo.server.log('Telegram current track "unknown" - setting to iTunes currently playing',
|
|
level=logging.DEBUG)
|
|
message['State_Update']['nowPlaying'] = self.gateway.states['nowPlaying']
|
|
|
|
except KeyError:
|
|
pass
|
|
|
|
try: # Catch GOTO_SOURCE commands and set the goto_flag
|
|
if message['payload_type'] == 'GOTO_SOURCE':
|
|
self.goto_flag = datetime.now()
|
|
if self.debug:
|
|
indigo.server.log("GOTO_SOURCE command received - goto_flag set", level=logging.WARNING)
|
|
except KeyError:
|
|
pass
|
|
|
|
def filter_messages(self, message):
|
|
# Filter state updates that are:
|
|
# 1. Standby states that are received between source changes:
|
|
# If device is changing source the state changes as follows [Old Source -> Standby -> New Source].
|
|
# If the standby condition is processed, the New Source state will be filtered by the condition below
|
|
#
|
|
# 2. Play states that received <2 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 <2 seconds after standby state, ignore
|
|
elif node.states['playState'] == "Standby" and time_delta1 < 2.0: # 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[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 [0, '0', '', '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']
|
|
|
|
try: # Check for mute state
|
|
if message['State_Update']['sound_status']['mute_status'] == 'Muted':
|
|
new_state[5] = True
|
|
else:
|
|
new_state[5] = False
|
|
except KeyError:
|
|
new_state[5] = False
|
|
|
|
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(self.playlist_default)
|
|
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(self.playlist_default)
|
|
|
|
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()
|
|
|
|
elif "Digit-" in message['State_Update']['command']:
|
|
rating = int(message['State_Update']['command'][-1:]) * 100/9
|
|
self.iTunes.set_rating(int(rating))
|
|
|
|
# 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
|
|
)
|
|
s = 'say ' + track_info[0] + ', by ' + track_info[2] + ', from the album, ' + track_info[1]
|
|
os.system(s)
|
|
|
|
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** DIGITS **"
|
|
"\n\tDigit 0 : Rate Track at 0 (0%), dislike it, and disable it from playback"
|
|
"\n\tDigits 1 to 8 : Rate Track from 1 (10%) to 8 (90%)"
|
|
"\n\tDigit 9 : Rate Track at 9 (100%) and 'love'' it"
|
|
"\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 '" + self.playlist_green + "'"
|
|
"\n\tYellow : Shuffle Playlist '" + self.playlist_yellow + "'"
|
|
"\n\tRed : Shuffle Playlist '" + self.playlist_red + "'"
|
|
"\n\tBlue : Shuffle Playlist '" + self.playlist_blue + "'\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)
|
|
if self.playlist_green != 'None':
|
|
self.iTunes.play_playlist(self.playlist_green)
|
|
|
|
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)
|
|
if self.playlist_yellow != 'None':
|
|
self.iTunes.play_playlist(self.playlist_yellow)
|
|
|
|
elif message['State_Update']['command'] == "Blue":
|
|
# Play the current album
|
|
# script = ASBridge.__file__[:-12] + '/Scripts/blue.scpt'
|
|
# self.iTunes.run_script(script, self.debug)
|
|
if self.playlist_blue != 'None':
|
|
self.iTunes.play_playlist(self.playlist_blue)
|
|
|
|
# elif message['State_Update']['command'] in ["0xf2", "Red", "MOTS"]:
|
|
elif message['State_Update']['command'] == "Red":
|
|
# 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)
|
|
if self.playlist_red != 'None':
|
|
self.iTunes.play_playlist(self.playlist_red)
|
|
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].decode('utf8') + "' by " + track_info[2].decode('utf8') + \
|
|
" from the album '" + track_info[1].decode('utf8') + "'"
|
|
|
|
# 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].decode('utf8') + "'"
|
|
"\n\t by " + track_info[2].decode('utf8') +
|
|
"\n\t from the album '" + track_info[1].decode('utf8') + "'"
|
|
"\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 configure_clients(self):
|
|
# Get Parameters from UI
|
|
# Plugin polling and reporting parameters
|
|
self.pollinterval = 595 # ping sent out at 9:55, response evaluated at 10:00
|
|
self.trackmode = self.pluginPrefs.get("trackMode")
|
|
self.verbose = self.pluginPrefs.get("verboseMode")
|
|
self.notifymode = self.pluginPrefs.get("notifyMode")
|
|
self.debug = self.pluginPrefs.get("debugMode")
|
|
self.triggers = []
|
|
|
|
# Plugin audio source control parameters
|
|
self.default_audio_source = self.pluginPrefs.get("defaultAudio")
|
|
self.goto_flag = datetime(1982, 4, 1, 13, 30, 00, 342380)
|
|
|
|
# Plugin Apple Music control parameters
|
|
self.itunes_control = self.pluginPrefs.get("iTunesControl")
|
|
self.playlist_default = self.pluginPrefs.get("playlist_default")
|
|
self.playlist_green = self.pluginPrefs.get("playlist_green")
|
|
self.playlist_yellow = self.pluginPrefs.get("playlist_yellow")
|
|
self.playlist_red = self.pluginPrefs.get("playlist_red")
|
|
self.playlist_blue = self.pluginPrefs.get("playlist_blue")
|
|
self.itunes_source = self.pluginPrefs.get("iTunesSource")
|
|
|
|
# Gateway network address and login parameters
|
|
self.host = str(self.pluginPrefs.get('address'))
|
|
self.port = [int(self.pluginPrefs.get('mlgw_port')),
|
|
int(self.pluginPrefs.get('hip_port')),
|
|
23] # Telnet port - 23
|
|
self.user = str(self.pluginPrefs.get('userID'))
|
|
self.pwd = str(self.pluginPrefs.get('password'))
|
|
|
|
###
|
|
|
|
# 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:
|
|
# If not configured yet, stall the programme flow until the user fills out the config UI
|
|
while not os.path.exists(self.path):
|
|
indigo.server.log("Plugin not yet configured: Retrying in 10 seconds...", level=logging.WARNING)
|
|
self.sleep(10.0)
|
|
|
|
# If configuration file exists, set up clients accordingly
|
|
if not self.configured:
|
|
self.configure_clients()
|
|
self.configured = 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!')
|