BeoGateway/BeoGateway.indigoPlugin/Contents/Server Plugin/plugin.py
2023-11-13 19:59:51 +00:00

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!')