BeoGateway/Server Plugin/plugin.py

1278 lines
60 KiB
Python
Raw Normal View History

import indigo
import asyncore
import json
import time
import logging
from datetime import datetime
import Resources.CONSTANTS as CONST
import Resources.MLCONFIG as MLCONFIG
import Resources.MLGW_CLIENT as MLGW
import Resources.MLCLI_CLIENT as MLCLI
import Resources.BLHIP_CLIENT as BLHIP
import Resources.MLtn_CLIENT as MLtn
import Resources.ASBridge as ASBridge
class Plugin(indigo.PluginBase):
def __init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs):
indigo.PluginBase.__init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs)
self.pollinterval = 595 # ping sent out at 9:55, response evaluated at 10:00
self.trackmode = self.pluginPrefs.get("trackMode")
self.verbose = self.pluginPrefs.get("verboseMode")
self.notifymode = self.pluginPrefs.get("notifyMode")
self.debug = self.pluginPrefs.get("debugMode")
self.default_audio_source = self.pluginPrefs.get("defaultAudio")
self.itunes_control = self.pluginPrefs.get("iTunesControl")
self.itunes_source = self.pluginPrefs.get("iTunesSource")
self.goto_flag = datetime(1982, 04, 01, 13, 30, 00, 342380)
self.triggers = []
self.host = str(self.pluginPrefs.get('address')).encode('ascii')
self.port = [int(self.pluginPrefs.get('mlgw_port')),
int(self.pluginPrefs.get('hip_port')),
23] # Telnet port - 23
self.user = str(self.pluginPrefs.get('userID')).encode('ascii')
self.pwd = str(self.pluginPrefs.get('password')).encode('ascii')
# Instantiate an AppleScriptBridge MusicController for N.MUSIC control of apple Music
self.iTunes = ASBridge.MusicController()
def triggerStartProcessing(self, trigger):
self.triggers.append(trigger)
def getDeviceStateList(self, dev):
stateList = indigo.PluginBase.getDeviceStateList(self, dev)
if stateList is not None:
if dev.deviceTypeId in self.devicesTypeDict and dev.deviceTypeId == u"AVrenderer":
# Add dynamic states onto stateList for devices of type AV renderer
try:
sources = dev.pluginProps['sources']
for source_name in sources:
stateList.append(
{
"Disabled": False,
"Key": "source." + source_name,
"StateKey": sources[source_name]['source'],
"StateLabel": "Source is " + source_name,
"TriggerLabel": "Source is " + source_name,
"Type": 50
}
)
except KeyError:
indigo.server.log("Device " + dev.name + " does not have state key 'sources'\n",
level=logging.WARNING)
pass
return stateList
def deviceStartComm(self, dev):
dev.stateListOrDisplayStateIdChanged()
def __del__(self):
indigo.PluginBase.__del__(self)
# ########################################################################################
# ##### Indigo UI menu constructors
@staticmethod
def zonelistgenerator(filter="", valuesDict=None, typeId="", targetId=0):
myarray = [("*", "All"), ("Main", "Main")]
for room in CONST.rooms:
if (room['Zone'], room['Zone']) not in myarray:
myarray.append((room['Zone'], room['Zone']))
return myarray
@staticmethod
def roomlistgenerator(filter="", valuesDict=None, typeId="", targetId=0):
myarray = [("99", "Any")]
for room in CONST.rooms:
myarray.append((room['Room_Number'], room['Room_Name']))
return myarray
@staticmethod
def roomlistgenerator2(filter="", valuesDict=None, typeId="", targetId=0):
myarray = [("*", "All"), ("global", "global")]
for room in CONST.rooms:
myarray.append((room['Room_Name'], room['Room_Name']))
return myarray
@staticmethod
def keylistgenerator(filter="", valuesDict=None, typeId="", targetId=0):
myarray = []
for item in CONST.beo4_commanddict.items():
myarray.append(item)
return myarray
@staticmethod
def keylistgenerator2(filter="", valuesDict=None, typeId="", targetId=0):
myarray = []
for item in CONST.beoremoteone_keydict.items():
myarray.append(item)
return myarray
@staticmethod
def beo4sourcelistgenerator(filter="", valuesDict=None, typeId="", targetId=0):
myarray = []
for item in CONST.beo4_srcdict.items():
myarray.append(item)
return myarray
@staticmethod
def beo4sourcelistgenerator2(filter="", valuesDict=None, typeId="", targetId=0):
myarray = [('Any Source', 'Any Source'), ('Any Audio', 'Any Audio'), ('Any Video', 'Any Video')]
for item in CONST.beo4_srcdict.items():
myarray.append(item)
return myarray
@staticmethod
def br1sourcelistgenerator(filter="", valuesDict=None, typeId="", targetId=0):
myarray = []
for item in CONST.available_sources:
myarray.append(item)
return myarray
@staticmethod
def destinationlistgenerator(filter="", valuesDict=None, typeId="", targetId=0):
myarray = []
for item in CONST.destselectordict.items():
myarray.append(item)
return myarray
@staticmethod
def srcactivitylistgenerator(filter="", valuesDict=None, typeId="", targetId=0):
myarray = [(0x00, "Unknown")]
for item in CONST.sourceactivitydict.items():
if item not in myarray:
myarray.append(item)
return myarray
@staticmethod
def hiptypelistgenerator(filter="", valuesDict=None, typeId="", targetId=0):
myarray = []
for item in CONST.blgw_devtypes.items():
myarray.append(item)
return myarray
# ########################################################################################
# ##### Indigo UI Prefs
def set_login(self, ui):
# If LogIn data is updated in config, update the values in the plugin
self.user = str(ui.get('userID')).encode('ascii')
self.pwd = str(ui.get('password')).encode('ascii')
indigo.server.log("BeoGateway device login details updated!", level=logging.DEBUG)
def set_gateway(self, ui):
# If gateway network address data is updated in config, update the values in the plugin
self.host = str(ui.get('address')).encode('ascii')
self.port = [int(ui.get('mlgw_port')),
int(ui.get('hip_port')),
23] # Telnet port - 23
indigo.server.log("BeoGateway device network address details updated!", level=logging.DEBUG)
def set_trackmode(self, ui):
# If Track Mode setting is updated in config, update the value in the plugin
self.trackmode = ui.get("trackMode")
indigo.server.log("Track reporting set to " + str(self.trackmode), level=logging.DEBUG)
def set_verbose(self, ui):
# If Verbose Mode setting is updated in config, update the value in the plugin
self.verbose = ui.get("verboseMode")
indigo.server.log("Verbose Mode set to " + str(self.verbose), level=logging.DEBUG)
def set_notifymode(self, ui):
# If Notify Mode setting is updated in config, update the value in the plugin
self.notifymode = ui.get("notifyMode")
indigo.server.log("Verbose Mode set to " + str(self.notifymode), level=logging.DEBUG)
def set_debug(self, ui):
# If Debug Mode setting is updated in config, update the value in the plugin
self.debug = ui.get("debugMode")
# Set the debug flag for the clients
self.mlcli.debug = self.debug
self.mlgw.debug = self.debug
if self.mlcli.isBLGW:
self.blgw.debug = self.debug
else:
self.mltn.debug = self.debug
# Report the debug flag change
indigo.server.log("Debug Mode set to " + str(self.debug), level=logging.DEBUG)
def set_music_control(self, ui):
# If Apple Music Control setting is updated in config, update the value in the plugin
self.itunes_control = ui.get("iTunesControl")
self.itunes_source = ui.get("iTunesSource")
if self.itunes_control:
indigo.server.log("Apple Music Control enabled on source: " + str(self.itunes_source), level=logging.DEBUG)
else:
indigo.server.log("Apple Music Control set to " + str(self.itunes_control), level=logging.DEBUG)
def set_default_audio(self, ui):
# Define default audio source for AVrenderers
self.default_audio_source = ui.get("defaultAudio")
indigo.server.log("Default Audio Source set to " + str(self.default_audio_source), level=logging.DEBUG)
# ########################################################################################
# ##### Indigo UI Actions
def send_beo_4key(self, action, device):
device_id = int(device.address)
key_code = int(action.props.get("keyCode", 0))
destination = int(action.props.get("destination", 0))
link = int(action.props.get("linkcmd", 0))
if destination == 0x06:
self.mlgw.send_beo4_cmd(device_id, destination, key_code, 0x01, link)
else:
self.mlgw.send_beo4_cmd(device_id, destination, key_code, 0x00, link)
def send_beo4_src(self, action, device):
device_id = int(device.address)
key_code = int(action.props.get("keyCode", 0))
destination = int(action.props.get("destination", 0))
link = int(action.props.get("linkcmd", 0))
if destination == 0x06:
self.mlgw.send_beo4_cmd(device_id, destination, key_code, 0x01, link)
else:
self.mlgw.send_beo4_cmd(device_id, destination, key_code, 0x00, link)
def send_br1_key(self, action, device):
device_id = int(device.address)
key_code = int(action.props.get("keyCode", 0))
network_bit = int(action.props.get("netBit", 0))
self.mlgw.send_beoremoteone_cmd(device_id, key_code, network_bit)
def send_br1_src(self, action, device):
device_id = int(device.address)
key_code = str(action.props.get("keyCode", 0))
network_bit = int(action.props.get("netBit", 0))
try:
key_code = CONST.beoremoteone_commanddict.get(key_code)
self.mlgw.send_beoremoteone_select_source(device_id, key_code[0], key_code[1], network_bit)
except KeyError:
pass
def send_hip_cmd(self, action):
zone = str(action.props.get("zone", 0))
room = str(action.props.get("room", 0))
device_type = str(action.props.get("devType", 0))
device_id = str(action.props.get("deviceID", 0))
hip_command = str(action.props.get("hip_cmd", 0))
telegram = "c " + zone + "/" + room + "/" + device_type + "/" + device_id + "/" + hip_command
if self.mlcli.isBLGW:
self.blgw.send_cmd(telegram)
def send_hip_cmd2(self, action):
hip_command = str(action.props.get("hip_cmd", 0))
if self.mlcli.isBLGW:
self.blgw.send_cmd(hip_command)
def send_hip_query(self, action):
zone = str(action.props.get("zone", 0))
room = str(action.props.get("room", 0))
device_type = str(action.props.get("devType", 0))
device_id = str(action.props.get("deviceID", 0))
if self.mlcli.isBLGW:
self.blgw.query(zone, room, device_type, device_id)
def request_state_update(self, action, device):
action_id = str(action.props.get("id", 0))
zone = str(device.pluginProps['zone'])
room = str(device.pluginProps['room'])
device_type = "AV renderer"
device_id = str(device.name)
if self.mlcli.isBLGW:
self.blgw.query(zone, room, device_type, device_id)
def send_virtual_button(self, action):
button_id = int(action.props.get("buttonID", 0))
button_action = int(action.props.get("action", 0))
self.mlgw.send_virtualbutton(button_id, button_action)
def post_notification(self, action):
title = str(action.props.get("title", 0))
body = str(action.props.get("body", 0))
self.iTunes.notify(body, title)
def all_standby(self, action):
self.mlgw.send_beo4_cmd(1, CONST.CMDS_DEST.get("ALL PRODUCTS"), CONST.BEO4_CMDS.get("STANDBY"))
def request_serial_number(self):
self.mlgw.get_serial()
def request_device_update(self):
if self.mlcli.isBLGW:
self.blgw.query(dev_type="AV renderer")
def reset_clients(self):
self.check_connection(self.mlgw)
self.check_connection(self.mlcli)
if self.mlcli.isBLGW:
self.check_connection(self.blgw)
else:
self.check_connection(self.mltn)
# ########################################################################################
# ##### Indigo UI Events
def light_key(self, message):
room = message['room_number']
key_code = CONST.BEO4_CMDS.get(message['command'].upper())
for trigger in self.triggers:
props = trigger.globalProps["uk.co.lukes_plugins.BeoGateway.plugin"]
if trigger.pluginTypeId == "lightKey" and \
(props["room"] == str(99) or props["room"] == str(room)) and \
props["keyCode"] == str(key_code):
indigo.trigger.execute(trigger)
break
def control_key(self, message):
room = message['room_number']
key_code = CONST.BEO4_CMDS.get(message['command'].upper())
for trigger in self.triggers:
props = trigger.globalProps["uk.co.lukes_plugins.BeoGateway.plugin"]
if trigger.pluginTypeId == "controlkey" and \
(props["room"] == str(99) or props["room"] == str(room)) and \
props["keyCode"] == str(key_code):
indigo.trigger.execute(trigger)
break
def beo4_key(self, message):
source = message['State_Update']['source']
source_type = message['State_Update']['source']
key_code = CONST.BEO4_CMDS.get(message['State_Update']['command'].upper())
for trigger in self.triggers:
props = trigger.globalProps["uk.co.lukes_plugins.BeoGateway.plugin"]
if trigger.pluginTypeId == "beo4Key" and props["keyCode"] == str(key_code):
if props["sourceType"] == "Any Source":
indigo.trigger.execute(trigger)
break
elif props["sourceType"] == "Any Audio" and "AUDIO" in source_type:
indigo.trigger.execute(trigger)
break
elif props["sourceType"] == "Any Audio" and "AUDIO" in source_type:
indigo.trigger.execute(trigger)
break
elif props["sourceType"] == source:
indigo.trigger.execute(trigger)
break
def virtual_button(self, message):
button_id = message['button']
action = message['action']
for trigger in self.triggers:
props = trigger.globalProps["uk.co.lukes_pugins.mlgw.plugin"]
if trigger.pluginTypeId == "virtualButton" and \
props["buttonID"] == str(button_id) and \
props["action"] == str(action):
indigo.trigger.execute(trigger)
break
# ########################################################################################
# ##### Indigo UI Device Controls
def actionControlDevice(self, action, node):
""" Callback Method to Control a Relay Device. """
if action.deviceAction == indigo.kDeviceAction.TurnOn:
self._dev_on(node)
elif action.deviceAction == indigo.kDeviceAction.TurnOff:
self._dev_off(node)
elif action.deviceAction == indigo.kDeviceAction.Toggle:
if node.states["onOffState"]:
self._dev_off(node)
else:
self._dev_on(node)
elif action.deviceAction == indigo.kDeviceAction.RequestStatus:
self._status_request(node)
def _dev_on(self, node):
indigo.server.log(node.name + " turned On")
# Get a local copy of the gateway states from server
active_renderers = self.gateway.states['AudioRenderers']
active_source = self.gateway.states['currentAudioSource']
active_sourceName = self.gateway.states['currentAudioSourceName']
if self.debug:
indigo.server.log('Active renderers: ' + active_renderers, level=logging.DEBUG)
indigo.server.log('Active Audio Source: ' + active_source, level=logging.DEBUG)
# Join if music already playing
if active_renderers != '' and active_source != 'Unknown':
# Send Beo4 command
source = active_source
self.mlgw.send_beo4_cmd(
int(node.address),
int(CONST.CMDS_DEST.get("AUDIO SOURCE")),
int(CONST.BEO4_CMDS.get(source))
)
# Update device states
sourceName = active_sourceName
key_value_list = [
{'key': 'onOffState', 'value': True},
{'key': 'playState', 'value': 'Play'},
{'key': 'source', 'value': sourceName},
{'key': 'mute', 'value': False},
]
if self.debug:
indigo.server.log(
node.name + " joining current audio experience " + sourceName + " (" + source +
"). Joining active renderer(s): " + active_renderers,
level=logging.DEBUG
)
# Otherwise start default music source
else:
# Send Beo4 command
source = self.default_audio_source
self.mlgw.send_beo4_cmd(
int(node.address),
int(CONST.CMDS_DEST.get("AUDIO SOURCE")),
int(CONST.BEO4_CMDS.get(source))
)
# Update device states
sourceName = dict(CONST.available_sources).get(self.default_audio_source)
key_value_list = [
{'key': 'onOffState', 'value': True},
{'key': 'playState', 'value': 'Play'},
{'key': 'source', 'value': sourceName},
{'key': 'mute', 'value': False},
]
if self.debug:
indigo.server.log(
node.name + " starting audio experience " + sourceName + " (" + source + ").",
level=logging.DEBUG
)
# Update states on server
node.updateStatesOnServer(key_value_list)
node.updateStateImageOnServer(indigo.kStateImageSel.AvPlaying)
# Add device to active renderers lists and update gateway
self.add_to_renderers_list(node.name, 'Audio')
key_value_list = [
{'key': 'currentAudioSource', 'value': source},
{'key': 'currentAudioSourceName', 'value': sourceName},
]
self.gateway.updateStatesOnServer(key_value_list)
def _dev_off(self, node):
indigo.server.log(node.name + " turned Off")
# Send Beo4 command
self.mlgw.send_beo4_cmd(
int(node.address),
int(CONST.CMDS_DEST.get("AUDIO SOURCE")),
int(CONST.BEO4_CMDS.get('STANDBY'))
)
# Update states to standby values
node.updateStatesOnServer(CONST.standby_state)
node.updateStateImageOnServer(indigo.kStateImageSel.PowerOff)
# Remove device from active renderers lists
self.remove_from_renderers_list(node.name, 'All')
def _status_request(self, node):
if node.pluginProps['serial_no'] == 'NA': # Check if this is a netlink device
indigo.server.log(node.name + " does not support status requests")
else: # If netlink, request a status update
self.blgw.query(dev_type="AV renderer", device=node.name)
# ########################################################################################
# Define callback function for message return from B&O Gateway
def cb(self, name, header, payload, message):
# ########################################################################################
# Message handler
# Handle Beo4 Command Events
try:
if message['payload_type'] == "BEO4_KEY":
self.beo4_key(message)
except KeyError:
pass
# Handle Light and Command Events
try:
if message['Type'] == "LIGHT COMMAND":
self.light_key(message)
elif message['Type'] == "CONTROL COMMAND":
self.control_key(message)
except KeyError:
pass
# Handle Virtual Button Events
try:
if message['payload_type'] == "MLGW virtual button event":
self.virtual_button(message)
except KeyError:
pass
# Handle all standby events
try:
if message["command"] == "All Standby":
for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
node.updateStatesOnServer(CONST.standby_state)
node.updateStateImageOnServer(indigo.kStateImageSel.PowerOff)
indigo.devices['Bang and Olufsen Gateway'].updateStatesOnServer(CONST.gw_all_stb)
except KeyError:
pass
# Handle AV Events
try:
# Use regular incoming messages to sync nowPlaying data
if self.gateway.states['AudioRenderers'] != '' and \
self.gateway.states['currentAudioSource'] == str(self.itunes_source) and \
self.itunes_control:
self._get_itunes_track_info(message)
# For messages of type AV RENDERER, scan keys to update device states
if message["Type"] == "AV RENDERER":
# Tidy up messages
self.av_sanitise(message)
# Filter messages that don't constitute meaningful state updates
actionable = self.filter_messages(message)
if self.debug:
indigo.server.log("Process message: " + str(actionable), level=logging.WARNING)
if actionable:
# Keep track of what sources are playing on the network
self.src_tracking(message)
# Update individual devices based on updates
self.dev_update(message)
except KeyError:
pass
# Report message content to log
if self.verbose:
if 'State_Update' in message and 'CONNECTION' in message['State_Update']:
# Don't print pong responses from regular client ping to check sockets are open -
# approx every 600 seconds
pass
elif 'Device' in message and message['Device'] == 'Clock':
# Don't print the Clock sync telegrams from the HIP -
# approx every 60 seconds
pass
elif 'payload_type' in message and message['payload_type'] == '0x14':
# Don't print the Clock sync telegrams from the ML -
# approx every 6 seconds
pass
elif 'payload_type' in message and message['payload_type'] == 'CLOCK' and not self.debug:
# Don't print the Clock message telegrams from the ML
pass
else:
self.message_log(name, header, payload, message)
# ########################################################################################
# AV Handler Functions
# #### Message Conditioning
def av_sanitise(self, message):
# Sanitise AV messages
try: # Check for missing source information
if message['State_Update']['source'] in [None, 'None', '']:
message['State_Update']['source'] = 'Unknown'
message['State_Update']['sourceName'] = 'Unknown'
# Update for standby condition
message['State_Update']['state'] = "Standby"
except KeyError:
pass
try: # Check for unknown state
if message['State_Update']['state'] in [None, 'None', '']:
message['State_Update']['state'] = 'Unknown'
except KeyError:
pass
try: # Sanitise unknown Channel/Tracks
if message['State_Update']['nowPlayingDetails']['channel_track'] in [0, 255, '0', '255']:
del message['State_Update']['nowPlayingDetails']['channel_track']
except KeyError:
pass
try: # Add sourceName if not in message block
if 'sourceName' not in message['State_Update']:
# Find the sourceName from the source list for this device
if 'Device' in message: # If device known use local source data
self.find_source_name(message['State_Update']['source'],
indigo.devices[message['Device']].pluginProps['sources'])
else: # If device not known use global source data
message['State_Update']['sourceName'] = \
dict(CONST.available_sources).get(message['State_Update']['source'])
except KeyError:
pass
try: # Catch GOTO_SOURCE commands and set the goto_flag
if message['payload_type'] == 'GOTO_SOURCE':
self.goto_flag = datetime.now()
if self.debug:
indigo.server.log("GOTO_SOURCE command received - goto_flag set", level=logging.WARNING)
except KeyError:
pass
def filter_messages(self, message):
# Filter state updates that are:
# 1. Standby states that are received between source changes:
# If device is changing source the state changes as follows [Old Source -> Standby -> New Source].
# If the standby condition is processed, the New Source state will be filtered by the condition below
#
# 2. Play states that received <1.5 seconds after standby state set:
# Some messages come in on the ML and HIP protocols relating to previous state etc.
# These can be ignored to avoid false states for the indigo devices
try:
for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
if node.name == message['Device']:
# Get time since last state update for this device
time_delta1 = datetime.now() - node.lastChanged
time_delta1 = time_delta1.total_seconds()
# Get time since last GOTO_SOURCE command
time_delta2 = datetime.now() - self.goto_flag
time_delta2 = time_delta2.total_seconds()
# If standby command received <2.0 seconds after GOTO_SOURCE command , ignore
if 'state' in message['State_Update'] and message['State_Update']['state'] == "Standby" \
and time_delta2 < 2.0: # Condition 1
if self.debug:
indigo.server.log(message['Device'] + " ignoring Standby: " + str(round(time_delta2, 2)) +
" seconds elapsed since GOTO_STATE command - ignoring message!",
level=logging.DEBUG)
return False
# If message received <1.5 seconds after standby state, ignore
elif node.states['playState'] == "Standby" and time_delta1 < 1.5: # Condition 2
if self.debug:
indigo.server.log(message['Device'] + " in Standby: " + str(round(time_delta1, 2)) +
" seconds elapsed since last state update - ignoring message!",
level=logging.DEBUG)
return False
else:
return True
except KeyError:
return False
# #### State tracking
def src_tracking(self, message):
# Track active renderers via gateway device
try:
# If new source is an audio source then update the gateway accordingly
if message['State_Update']['source'] in CONST.source_type_dict.get('Audio Sources'):
try:
# Keep track of which devices are playing audio sources
if message['Device'] not in self.gateway.states['AudioRenderers'] and \
message['State_Update']['state'] not in ['Standby', 'Unknown', 'None']:
# Log current audio source (MasterLink allows a single audio source for distribution)
source = message['State_Update']['source']
sourceName = dict(CONST.available_sources).get(source)
self.gateway.updateStateOnServer('currentAudioSource', value=source)
self.gateway.updateStateOnServer('currentAudioSourceName', value=sourceName)
try:
self.gateway.updateStateOnServer('nowPlaying', value=message['State_Update']['nowPlaying'])
except KeyError:
self.gateway.updateStateOnServer('nowPlaying', value='Unknown')
self.add_to_renderers_list(message['Device'], 'Audio')
# Remove device from Video Renderers list if it is on there
if message['Device'] in self.gateway.states['VideoRenderers']:
self.remove_from_renderers_list(message['Device'], 'Video')
except KeyError:
pass
# If source is N.Music then control accordingly
if message['State_Update']['source'] == str(self.itunes_source) and self.itunes_control:
self.iTunes_transport_control(message)
# If new source is an video source then update the gateway accordingly
elif message['State_Update']['source'] in CONST.source_type_dict.get('Video Sources'):
try:
# Keep track of which devices are playing video sources
if message['Device'] not in self.gateway.states['VideoRenderers'] and \
message['State_Update']['state'] not in ['Standby', 'Unknown', 'None']:
self.add_to_renderers_list(message['Device'], 'Video')
# Remove device from Audio Renderers list if it is on there
if message['Device'] in self.gateway.states['AudioRenderers']:
self.remove_from_renderers_list(message['Device'], 'Audio')
except KeyError:
pass
except KeyError:
pass
def dev_update(self, message):
# Update device states
try:
for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
if node.name == message['Device']:
# Handle Standby state
if message['State_Update']['state'] == "Standby":
# Update states to standby values
node.updateStatesOnServer(CONST.standby_state)
node.updateStateImageOnServer(indigo.kStateImageSel.PowerOff)
# Remove the device from active renderers list
self.remove_from_renderers_list(message['Device'], 'All')
# Post state update in Apple Notification Centre
if self.notifymode:
self.iTunes.notify(node.name + " now in Standby", "Device State Update")
return
# If device not in standby then update its state information
# get current states as list # Index
last_state = [
node.states['playState'], # 0
node.states['source'], # 1
node.states['nowPlaying'], # 2
node.states['channelTrack'], # 3
node.states['volume'], # 4
node.states['mute'], # 5
node.states['onOffState'] # 6
]
# Initialise new state list - the device is not in standby so it must be on
new_state = last_state[:]
new_state[5] = False
new_state[6] = True
# Update device states with values from message
if 'state' in message['State_Update']:
if message['State_Update']['state'] not in ['None', 'Standby', '', None]:
if last_state[0] == 'Standby' and message['State_Update']['state'] == 'Unknown':
new_state[0] = 'Play'
elif last_state[0] != 'Standby' and message['State_Update']['state'] == 'Unknown':
pass
else:
new_state[0] = message['State_Update']['state']
if 'sourceName' in message['State_Update'] and message['State_Update']['sourceName'] != 'Unknown':
# Sanitise source name to avoid indigo key errors (remove whitespace)
source_name = message['State_Update']['sourceName'].strip().replace(" ", "_")
new_state[1] = source_name
if 'nowPlaying' in message['State_Update']:
# Update now playing information unless the state value is empty or unknown
if message['State_Update']['nowPlaying'] not in ['', 'Unknown']:
new_state[2] = message['State_Update']['nowPlaying']
# If the state value is empty/unknown and the source has not changed then no update required
elif new_state[1] != last_state[1]:
# If the state has changed and the value is unknown, then set as "Unknown"
new_state[2] = 'Unknown'
if 'nowPlayingDetails' in message['State_Update'] and \
'channel_track' in message['State_Update']['nowPlayingDetails']:
new_state[3] = message['State_Update']['nowPlayingDetails']['channel_track']
elif new_state[1] != last_state[1]:
# If the state has changed and the value is unknown, then set as "Unknown"
new_state[2] = 0
if 'volume' in message['State_Update']:
new_state[4] = message['State_Update']['volume']
if new_state != last_state:
# Update states on server
key_value_list = [
{'key': 'playState', 'value': new_state[0]},
{'key': 'source', 'value': new_state[1]},
{'key': 'nowPlaying', 'value': new_state[2]},
{'key': 'channelTrack', 'value': new_state[3]},
{'key': 'volume', 'value': new_state[4]},
{'key': 'mute', 'value': new_state[5]},
{'key': 'onOffState', 'value': new_state[6]},
]
node.updateStatesOnServer(key_value_list)
# Post notifications Notifications
if self.notifymode:
self.notifications(node.name, last_state, new_state)
# Update state image on server
if new_state[0] == "Stopped":
node.updateStateImageOnServer(indigo.kStateImageSel.AvPaused)
elif new_state[0] not in ['None', 'Unknown', 'Standby', '', None]:
node.updateStateImageOnServer(indigo.kStateImageSel.AvPlaying)
# If audio source active, update any other active audio renderers accordingly
try:
if new_state[0] not in ['None', 'Unknown', 'Standby', '', None] and \
message['State_Update']['source'] in CONST.source_type_dict.get('Audio Sources'):
self.all_audio_nodes_update(new_state, node.name, message['State_Update']['source'])
except KeyError:
pass
break
except KeyError:
pass
def all_audio_nodes_update(self, new_state, dev, source):
# Loop over all active audio renderers to update them with the latest audio state
for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
if node.name in self.gateway.states['AudioRenderers'] and node.name != dev:
# Get current state of this node
last_state = [
node.states['playState'],
node.states['source'],
node.states['nowPlaying'],
node.states['channelTrack'],
node.states['volume'],
node.states['mute'],
]
if last_state[:4] != new_state[:4]:
# Update the play state for active Audio renderers if new values are different from current ones
key_value_list = [
{'key': 'onOffState', 'value': True},
{'key': 'playState', 'value': new_state[0]},
{'key': 'source', 'value': new_state[1]},
{'key': 'nowPlaying', 'value': new_state[2]},
{'key': 'channelTrack', 'value': new_state[3]},
{'key': 'mute', 'value': False},
]
node.updateStatesOnServer(key_value_list)
node.updateStateImageOnServer(indigo.kStateImageSel.AvPlaying)
# Update the gateway
if self.gateway.states['currentAudioSourceName'] != new_state[1]:
# If the source has changed, update both source and nowPlaying
sourceName = new_state[1]
key_value_list = [
{'key': 'currentAudioSource', 'value': source},
{'key': 'currentAudioSourceName', 'value': sourceName},
{'key': 'nowPlaying', 'value': new_state[2]},
]
self.gateway.updateStatesOnServer(key_value_list)
elif self.gateway.states['nowPlaying'] != new_state[2] and new_state[2] not in ['', 'Unknown']:
# If the source has not changed, and nowPlaying is not Unknown, update nowPlaying
self.gateway.updateStateOnServer('nowPlaying', value=new_state[2])
# #### Active renderer list maintenance
def add_to_renderers_list(self, dev, av):
if av == "Audio":
renderer_list = 'AudioRenderers'
renderer_count = 'nAudioRenderers'
else:
renderer_list = 'VideoRenderers'
renderer_count = 'nVideoRenderers'
# Retrieve the renderers and convert from string to list
renderers = self.gateway.states[renderer_list].split(', ')
# Sanitise the list for stray blanks
if '' in renderers:
renderers.remove('')
# Add device to list if not already on there
if dev not in renderers:
renderers.append(dev)
self.gateway.updateStateOnServer(renderer_list, value=', '.join(renderers))
self.gateway.updateStateOnServer(renderer_count, value=len(renderers))
def remove_from_renderers_list(self, dev, av):
# Remove devices from renderers lists when the enter standby mode
if av in ['Audio', 'All']:
if dev in self.gateway.states['AudioRenderers']:
renderers = self.gateway.states['AudioRenderers'].split(', ')
renderers.remove(dev)
self.gateway.updateStateOnServer('AudioRenderers', value=', '.join(renderers))
self.gateway.updateStateOnServer('nAudioRenderers', value=len(renderers))
if av in ['Video', 'All']:
if dev in self.gateway.states['VideoRenderers']:
renderers = self.gateway.states['VideoRenderers'].split(', ')
renderers.remove(dev)
self.gateway.updateStateOnServer('VideoRenderers', value=', '.join(renderers))
self.gateway.updateStateOnServer('nVideoRenderers', value=len(renderers))
# If no audio sources are playing then update the gateway states
if self.gateway.states['AudioRenderers'] == '':
key_value_list = [
{'key': 'AudioRenderers', 'value': ''},
{'key': 'nAudioRenderers', 'value': 0},
{'key': 'currentAudioSource', 'value': 'Unknown'},
{'key': 'currentAudioSourceName', 'value': 'Unknown'},
{'key': 'nowPlaying', 'value': 'Unknown'},
]
self.gateway.updateStatesOnServer(key_value_list)
# If no AV renderers are playing N.Music, stop iTunes playback
if self.itunes_control:
self.iTunes.stop()
# #### Helper functions
@staticmethod
def find_source_name(source, sources):
# Get the sourceName for source
for source_name in sources:
if sources[source_name]['source'] == str(source):
return str(sources[source_name]).split()[0]
# if source list exhausted and no valid name found return Unknown
return 'Unknown'
@staticmethod
# Get the source ID for sourceName
def get_source(sourceName, sources):
for source_name in sources:
if source_name == sourceName:
return str(sources[source_name]['source'])
# if source list exhausted and no valid name found return Unknown
return 'Unknown'
# ########################################################################################
# Apple Music Control and feedback
def iTunes_transport_control(self, message):
# Transport controls for iTunes
try: # If N.MUSIC command, trigger appropriate self.iTunes control
if message['State_Update']['state'] not in ["", "Standby"]:
self.iTunes.play()
except KeyError:
pass
try: # If N.MUSIC selected and Beo4 command received then run appropriate transport commands
if message['State_Update']['command'] == "Go/Play":
self.iTunes.play()
elif message['State_Update']['command'] == "Stop":
self.iTunes.pause()
elif message['State_Update']['command'] == "Exit":
self.iTunes.stop()
elif message['State_Update']['command'] == "Step Up":
self.iTunes.next_track()
elif message['State_Update']['command'] == "Step Down":
self.iTunes.previous_track()
elif message['State_Update']['command'] == "Wind":
self.iTunes.wind(15)
elif message['State_Update']['command'] == "Rewind":
self.iTunes.rewind(-15)
elif message['State_Update']['command'] == "Shift-1/Random":
self.iTunes.shuffle()
# If 'Info' pressed - update track info
elif message['State_Update']['command'] == "Info":
track_info = self.iTunes.get_current_track_info()
if track_info[0] not in [None, 'None']:
indigo.server.log(
"\n\t----------------------------------------------------------------------------"
"\n\tiTUNES CURRENT TRACK INFO:"
"\n\t============================================================================"
"\n\tNow playing: '" + track_info[0] + "'"
"\n\t by " + track_info[2] +
"\n\t from the album '" + track_info[1] + "'"
"\n\t----------------------------------------------------------------------------"
"\n\tACTIVE AUDIO RENDERERS: " + str(self.gateway.states['AudioRenderers']) + "\n\n",
level=logging.DEBUG
)
self.iTunes.notify(
"Now playing: '" + track_info[0] +
"' by " + track_info[2] +
"from the album '" + track_info[1] + "'",
"Apple Music Track Info:"
)
# If 'Guide' pressed - print instructions to indigo log
elif message['State_Update']['command'] == "Guide":
indigo.server.log(
"\n\t----------------------------------------------------------------------------"
"\n\tBeo4/BeoRemote One Control of Apple Music"
"\n\tKey mapping guide: [Key : Action]"
"\n\t============================================================================"
"\n\n\t** BASIC TRANSPORT CONTROLS **"
"\n\tGO/PLAY : Play"
"\n\tSTOP/Pause : Pause"
"\n\tEXIT : Stop"
"\n\tStep Up/P+ : Next Track"
"\n\tStep Down/P- : Previous Track"
"\n\tWind : Scan Forwards 15 Seconds"
"\n\tRewind : Scan Backwards 15 Seconds"
"\n\n\t** FUNCTIONS **"
"\n\tShift-1/Random : Toggle Shuffle"
"\n\tINFO : Display Track Info for Current Track"
"\n\tGUIDE : This Guide"
"\n\n\t** ADVANCED CONTROLS **"
"\n\tGreen : Shuffle Playlist 'Recently Played'"
"\n\tYellow : Play Digital Radio Stations from Playlist Radio"
"\n\tRed : More of the Same"
"\n\tBlue : Play the Album that the Current Track Resides On\n\n",
level=logging.DEBUG
)
# If colour key pressed, execute the appropriate applescript
elif message['State_Update']['command'] == "Green":
# Play a specific playlist - defaults to Recently Played
script = ASBridge.__file__[:-12] + '/Scripts/green.scpt'
self.iTunes.run_script(script, self.debug)
elif message['State_Update']['command'] == "Yellow":
# Play a specific playlist - defaults to URL Radio stations
script = ASBridge.__file__[:-12] + '/Scripts/yellow.scpt'
self.iTunes.run_script(script, self.debug)
elif message['State_Update']['command'] == "Blue":
# Play the current album
script = ASBridge.__file__[:-12] + '/Scripts/blue.scpt'
self.iTunes.run_script(script, self.debug)
elif message['State_Update']['command'] in ["0xf2", "Red", "MOTS"]:
# More of the same (start a playlist with just current track and let autoplay find similar tunes)
script = ASBridge.__file__[:-12] + '/Scripts/red.scpt'
self.iTunes.run_script(script, self.debug)
except KeyError:
pass
def _get_itunes_track_info(self, message):
track_info = self.iTunes.get_current_track_info()
if track_info[0] not in [None, 'None']:
# Construct track info string
track_info_ = "'" + track_info[0] + "' by " + track_info[2] + " from the album '" + track_info[1] + "'"
# Add now playing info to the message block
if 'Type' in message and message['Type'] == "AV RENDERER" and 'source' in message['State_Update'] \
and message['State_Update']['source'] == str(self.itunes_source) and \
'nowPlaying' in message['State_Update']:
message['State_Update']['nowPlaying'] = track_info_
message['State_Update']['nowPlayingDetails']['channel_track'] = int(track_info[3])
# Print track info to log if trackmode is set to true (via config UI)
src = dict(CONST.available_sources).get(self.itunes_source)
if self.gateway.states['currentAudioSource'] == str(self.itunes_source) and \
track_info_ != self.gateway.states['nowPlaying'] and self.trackmode:
indigo.server.log("\n\t----------------------------------------------------------------------------"
"\n\tiTUNES CURRENT TRACK INFO:"
"\n\t============================================================================"
"\n\tNow playing: '" + track_info[0] + "'"
"\n\t by " + track_info[2] +
"\n\t from the album '" + track_info[1] + "'"
"\n\t----------------------------------------------------------------------------"
"\n\tACTIVE AUDIO RENDERERS: " + str(self.gateway.states['AudioRenderers']) + "\n\n")
if self.notifymode:
# Post track information to Apple Notification Centre
self.iTunes.notify(track_info_ + " from source " + src,
"Now Playing:")
# Update nowPlaying on the gateway device
if track_info_ != self.gateway.states['nowPlaying'] and \
self.gateway.states['currentAudioSource'] == str(self.itunes_source):
self.gateway.updateStateOnServer('nowPlaying', value=track_info_)
# Update info on active Audio Renderers
for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
if node.name in self.gateway.states['AudioRenderers']:
key_value_list = [
{'key': 'onOffState', 'value': True},
{'key': 'playState', 'value': 'Play'},
{'key': 'source', 'value': src},
{'key': 'nowPlaying', 'value': track_info_},
{'key': 'channelTrack', 'value': int(track_info[3])},
]
node.updateStatesOnServer(key_value_list)
node.updateStateImageOnServer(indigo.kStateImageSel.AvPlaying)
# ########################################################################################
# Message Reporting
@staticmethod
def message_log(name, header, payload, message):
# Set reporting level for message logging
try: # CLOCK messages are filtered except in debug mode
if message['payload_type'] == 'CLOCK':
debug_level = logging.DEBUG
else: # Everything else is for INFO
debug_level = logging.INFO
except KeyError:
debug_level = logging.INFO
# Pretty formatting - convert to JSON format then remove braces
message = json.dumps(message, indent=4)
for r in (('"', ''), (',', ''), ('{', ''), ('}', '')):
message = str(message).replace(*r)
# Print message data
if len(payload) + 9 < 73:
indigo.server.log("\n\t----------------------------------------------------------------------------" +
"\n\t" + name + ": <--DATA-RECEIVED!-<< " +
datetime.now().strftime("on %d/%m/%y at %H:%M:%S") +
"\n\t============================================================================" +
"\n\tHeader: " + header +
"\n\tPayload: " + payload +
"\n\t----------------------------------------------------------------------------" +
message, level=debug_level)
elif 73 < len(payload) + 9 < 137:
indigo.server.log("\n\t----------------------------------------------------------------------------" +
"\n\t" + name + ": <--DATA-RECEIVED!-<< " +
datetime.now().strftime("on %d/%m/%y at %H:%M:%S") +
"\n\t============================================================================" +
"\n\tHeader: " + header +
"\n\tPayload: " + payload[:66] + "\n\t\t" + payload[66:137] +
"\n\t----------------------------------------------------------------------------" +
message, level=debug_level)
else:
indigo.server.log("\n\t----------------------------------------------------------------------------" +
"\n\t" + name + ": <--DATA-RECEIVED!-<< " +
datetime.now().strftime("on %d/%m/%y at %H:%M:%S") +
"\n\t============================================================================" +
"\n\tHeader: " + header +
"\n\tPayload: " + payload[:66] + "\n\t\t" + payload[66:137] + "\n\t\t" + payload[137:] +
"\n\t----------------------------------------------------------------------------" +
message, level=debug_level)
def notifications(self, name, last_state, new_state):
# Post state information to the Apple Notification Centre
# Information index:
# node.states['playState'], # 0
# node.states['source'], # 1
# node.states['nowPlaying'], # 2
# node.states['channelTrack'], # 3
# node.states['volume'], # 4
# node.states['mute'], # 5
# node.states['onOffState'] # 6
# Don't post notification if nothing has changed
if last_state == new_state:
return
# Source status information
if last_state[0] != new_state[0] and new_state[0] == "Standby": # Power off
self.iTunes.notify(
name + " now in Standby",
"Device State Update"
)
return
elif last_state[1] != new_state[1] and new_state[0] != "Standby": # Source Update
self.iTunes.notify(
name + " now playing from source " + new_state[1],
"Device State Update"
)
return
elif last_state[0] != new_state[0] and new_state[0] == "Play": # Power on
self.iTunes.notify(
name + " Active",
"Device State Update"
)
return
# Channel/Track information
if new_state[2] not in [None, 'None', '', 0, '0', 'Unknown']: # Now Playing Update
self.iTunes.notify(
new_state[2] + " from source " + new_state[1],
name + " Now Playing:"
)
elif last_state[3] != new_state[3] and new_state[3] not in [0, 255, '0', '255']: # Channel/Track Update
self.iTunes.notify(
name + " now playing channel/track " + new_state[3] + " from source " + new_state[1],
"Device Channel/Track Information"
)
# ########################################################################################
# Indigo Server Methods
def startup(self):
indigo.server.log(u"Startup called")
# Download the config file from the gateway and initialise the devices
config = MLCONFIG.MLConfig(self.host, self.user, self.pwd, self.debug)
self.gateway = indigo.devices['Bang and Olufsen Gateway']
# Create MLGW Protocol and ML_CLI Protocol clients (basic command listening)
indigo.server.log('Creating MLGW Protocol Client...', level=logging.WARNING)
self.mlgw = MLGW.MLGWClient(self.host, self.port[0], self.user, self.pwd, 'MLGW protocol', self.debug, self.cb)
asyncore.loop(count=10, timeout=0.2)
indigo.server.log('Creating ML Command Line Protocol Client...', level=logging.WARNING)
self.mlcli = MLCLI.MLCLIClient(self.host, self.port[2], self.user, self.pwd,
'ML command line interface', self.debug, self.cb)
# Log onto the MLCLI client and ascertain the gateway model
asyncore.loop(count=10, timeout=0.2)
# Now MLGW and MasterLink Command Line Client are set up, retrieve MasterLink IDs of products
config.get_masterlink_id(self.mlgw, self.mlcli)
# If the gateway is a BLGW use the BLHIP protocol, else use the legacy MLHIP protocol
if self.mlcli.isBLGW:
indigo.server.log('Creating BLGW Home Integration Protocol Client...', level=logging.WARNING)
self.blgw = BLHIP.BLHIPClient(self.host, self.port[1], self.user, self.pwd,
'BLGW Home Integration Protocol', self.debug, self.cb)
self.mltn = None
else:
indigo.server.log('Creating MLGW Home Integration Protocol Client...', level=logging.WARNING)
self.mltn = MLtn.MLtnClient(self.host, self.port[2], self.user, self.pwd, 'ML telnet client',
self.debug, self.cb)
self.blgw = None
# Connection polling
def check_connection(self, client):
last = round(time.time() - client.last_received_at, 2)
# Reconnect if socket has disconnected, or if no response received to last ping
if not client.is_connected or last > 60:
indigo.server.log("\t" + client.name + ": Reconnecting!", level=logging.WARNING)
client.handle_close()
self.sleep(0.5)
client.client_connect()
# Indigo main program loop
def runConcurrentThread(self):
try:
while True:
# Ping all connections every 10 minutes to prompt messages on the network
asyncore.loop(count=self.pollinterval, timeout=1)
if self.mlgw.is_connected:
self.mlgw.ping()
if self.mlcli.is_connected:
self.mlcli.ping()
if self.mlcli.isBLGW:
if self.blgw.is_connected:
self.blgw.ping()
else:
if self.mltn.is_connected:
self.mltn.ping()
# Check the connections approximately every 10 minutes to keep sockets open
asyncore.loop(count=5, timeout=1)
self.check_connection(self.mlgw)
self.check_connection(self.mlcli)
if self.mlcli.isBLGW:
self.check_connection(self.blgw)
else:
self.check_connection(self.mltn)
self.sleep(0.5)
except self.StopThread:
raise asyncore.ExitNow('Server is quitting!')
# Tidy up on shutdown
def shutdown(self):
indigo.server.log("Shutdown plugin")
del self.mlgw
del self.mlcli
if self.mlcli.isBLGW:
del self.blgw
else:
del self.mltn
del self.iTunes
raise asyncore.ExitNow('Server is quitting!')