2023-11-13 20:00:48 +00:00
|
|
|
try:
|
|
|
|
import indigo
|
|
|
|
except ImportError:
|
|
|
|
pass
|
2022-02-05 20:38:57 +00:00
|
|
|
import asynchat
|
|
|
|
import socket
|
|
|
|
import time
|
|
|
|
import urllib
|
|
|
|
import logging
|
|
|
|
from collections import OrderedDict
|
|
|
|
|
|
|
|
import Resources.CONSTANTS as CONST
|
|
|
|
|
|
|
|
|
|
|
|
class BLHIPClient(asynchat.async_chat):
|
|
|
|
"""Client to interact with a Beolink Gateway via the Home Integration Protocol
|
|
|
|
https://manualzz.com/download/14415327
|
|
|
|
Full documentation of states, commands and events can be found in the driver development guide
|
|
|
|
https://vdocument.in//blgw-driver-development-guide-blgw-driver-development-guide-7-2016-10-10"""
|
|
|
|
def __init__(self, host_address='blgw.local', port=9100, user='admin', pwd='admin', name='BLGW_HIP',
|
|
|
|
debug=False, cb=None):
|
|
|
|
asynchat.async_chat.__init__(self)
|
|
|
|
|
|
|
|
self.debug = debug
|
|
|
|
|
|
|
|
self._host = host_address
|
|
|
|
self._port = int(port)
|
|
|
|
self._user = user
|
|
|
|
self._pwd = pwd
|
|
|
|
self.name = name
|
|
|
|
self.is_connected = False
|
|
|
|
|
|
|
|
self._received_data = ''
|
|
|
|
self.last_sent = ''
|
|
|
|
self.last_sent_at = time.time()
|
|
|
|
self.last_received = ''
|
|
|
|
self.last_received_at = time.time()
|
|
|
|
self.last_message = {}
|
|
|
|
|
|
|
|
# Optional callback function
|
|
|
|
if cb:
|
|
|
|
self.messageCallBack = cb
|
|
|
|
else:
|
|
|
|
self.messageCallBack = None
|
|
|
|
|
|
|
|
# ########################################################################################
|
|
|
|
# ##### Open Socket and connect to B&O Gateway
|
|
|
|
self.client_connect()
|
|
|
|
|
|
|
|
# ########################################################################################
|
|
|
|
# ##### Client functions
|
|
|
|
def collect_incoming_data(self, data):
|
|
|
|
self.is_connected = True
|
2023-11-13 20:00:48 +00:00
|
|
|
self._received_data += str(data)
|
2022-02-05 20:38:57 +00:00
|
|
|
|
|
|
|
def found_terminator(self):
|
2023-11-13 20:00:48 +00:00
|
|
|
if self.debug:
|
|
|
|
indigo.server.log("Raw Data: " + self._received_data)
|
2022-02-05 20:38:57 +00:00
|
|
|
self.last_received = self._received_data
|
|
|
|
self.last_received_at = time.time()
|
|
|
|
|
|
|
|
if self._received_data == 'error':
|
|
|
|
self.handle_close()
|
|
|
|
|
|
|
|
if self._received_data == 'e OK f%20%2A/%2A/%2A/%2A':
|
|
|
|
indigo.server.log('\tAuthentication Successful!', level=logging.DEBUG)
|
|
|
|
self.query(dev_type="AV renderer")
|
|
|
|
|
2023-11-13 20:00:48 +00:00
|
|
|
self._received_data = urllib.parse.unquote(self._received_data)
|
2022-02-05 20:38:57 +00:00
|
|
|
telegram = self._received_data.replace("%201", "")
|
|
|
|
telegram = telegram.split('/')
|
|
|
|
header = telegram[0:4]
|
|
|
|
|
|
|
|
self._decode(header, telegram)
|
|
|
|
|
|
|
|
def _decode(self, header, telegram):
|
|
|
|
e_string = str(header[0])
|
|
|
|
if e_string[0] == 'e':
|
|
|
|
if e_string[2:4] == 'OK' and self.debug:
|
2023-11-13 20:00:48 +00:00
|
|
|
indigo.server.log('Command Successfully Processed: ' + str(self._received_data), level=logging.DEBUG)
|
2022-02-05 20:38:57 +00:00
|
|
|
|
|
|
|
elif e_string[2:5] == 'CMD':
|
2023-11-13 20:00:48 +00:00
|
|
|
indigo.server.log('Wrong or Unrecognised Command: ' + str(self._received_data), level=logging.WARNING)
|
2022-02-05 20:38:57 +00:00
|
|
|
|
|
|
|
elif e_string[2:5] == 'SYN':
|
2023-11-13 20:00:48 +00:00
|
|
|
indigo.server.log('Bad Syntax, or Wrong Character Encoding: ' + str(self._received_data),
|
|
|
|
level=logging.WARNING)
|
2022-02-05 20:38:57 +00:00
|
|
|
|
|
|
|
elif e_string[2:5] == 'ACC':
|
2023-11-13 20:00:48 +00:00
|
|
|
indigo.server.log('Zone Access Violation: ' + str(self._received_data), level=logging.WARNING)
|
2022-02-05 20:38:57 +00:00
|
|
|
|
|
|
|
elif e_string[2:5] == 'LEN':
|
2023-11-13 20:00:48 +00:00
|
|
|
indigo.server.log('Received Message Too Long: ' + str(self._received_data), level=logging.WARNING)
|
2022-02-05 20:38:57 +00:00
|
|
|
|
|
|
|
self._received_data = ""
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
self._received_data = ""
|
|
|
|
|
|
|
|
if len(telegram) > 4:
|
|
|
|
state = telegram[4].replace('?', '&')
|
|
|
|
state = state.split('&')[1:]
|
|
|
|
|
|
|
|
message = OrderedDict()
|
|
|
|
message['Zone'] = telegram[0][2:].upper()
|
|
|
|
message['Room'] = telegram[1].upper()
|
|
|
|
message['Type'] = telegram[2].upper()
|
|
|
|
message['Device'] = telegram[3]
|
|
|
|
message['State_Update'] = OrderedDict()
|
|
|
|
|
|
|
|
for s in state:
|
|
|
|
if s.split('=')[0] == "nowPlayingDetails":
|
|
|
|
play_details = s.split('=')
|
|
|
|
if len(play_details[1]) > 0:
|
|
|
|
play_details = play_details[1].split('; ')
|
|
|
|
message['State_Update']["nowPlayingDetails"] = OrderedDict()
|
|
|
|
for p in play_details:
|
|
|
|
if p.split(': ')[0] in ['track number', 'channel number']:
|
|
|
|
message['State_Update']["nowPlayingDetails"]['channel_track'] = p.split(': ')[1]
|
|
|
|
else:
|
|
|
|
message['State_Update']["nowPlayingDetails"][p.split(': ')[0]] = p.split(': ')[1]
|
|
|
|
|
|
|
|
elif s.split('=')[0] == "sourceUniqueId":
|
|
|
|
src = s.split('=')[1].split(':')[0].upper()
|
|
|
|
message['State_Update']['source'] = self._srcdictsanitize(CONST.blgw_srcdict, src)
|
|
|
|
message['State_Update'][s.split('=')[0]] = s.split('=')[1]
|
|
|
|
else:
|
|
|
|
message['State_Update'][s.split('=')[0]] = s.split('=')[1]
|
|
|
|
|
|
|
|
# call function to find channel details if type = Legacy
|
|
|
|
try:
|
|
|
|
if 'nowPlayingDetails' in message['State_Update'] \
|
|
|
|
and message['State_Update']['nowPlayingDetails']['type'] == 'Legacy':
|
|
|
|
self._get_channel_track(message)
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
if message.get('Type') == 'BUTTON':
|
|
|
|
if message['State_Update'].get('STATE') == '0':
|
|
|
|
message['State_Update']['Status'] = 'Off'
|
|
|
|
else:
|
|
|
|
message['State_Update']['Status'] = 'On'
|
|
|
|
|
|
|
|
if message.get('Type') == 'DIMMER':
|
|
|
|
if message['State_Update'].get('LEVEL') == '0':
|
|
|
|
message['State_Update']['Status'] = 'Off'
|
|
|
|
else:
|
|
|
|
message['State_Update']['Status'] = 'On'
|
|
|
|
|
|
|
|
self._report(header, state, message)
|
|
|
|
|
|
|
|
def _report(self, header, payload, message):
|
|
|
|
self.last_message = message
|
|
|
|
if self.messageCallBack:
|
|
|
|
self.messageCallBack(self.name, str(list(header)), str(list(payload)), message)
|
|
|
|
|
|
|
|
def client_connect(self):
|
|
|
|
indigo.server.log('Connecting to host at ' + self._host + ', port ' + str(self._port), level=logging.WARNING)
|
|
|
|
self.set_terminator(b'\r\n')
|
|
|
|
# Create the socket
|
|
|
|
try:
|
|
|
|
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
2022-03-17 21:22:31 +00:00
|
|
|
except socket.error as e:
|
2022-02-05 20:38:57 +00:00
|
|
|
indigo.server.log("Error creating socket: " + str(e), level=logging.ERROR)
|
|
|
|
self.handle_close()
|
|
|
|
# Now connect
|
|
|
|
try:
|
|
|
|
self.connect((self._host, self._port))
|
2022-03-17 21:22:31 +00:00
|
|
|
except socket.gaierror as e:
|
2022-02-05 20:38:57 +00:00
|
|
|
indigo.server.log("\tError with address: " + str(e), level=logging.ERROR)
|
|
|
|
self.handle_close()
|
2022-03-17 21:22:31 +00:00
|
|
|
except socket.timeout as e:
|
2022-02-05 20:38:57 +00:00
|
|
|
indigo.server.log("\tSocket connection timed out: " + str(e), level=logging.ERROR)
|
|
|
|
self.handle_close()
|
2022-03-17 21:22:31 +00:00
|
|
|
except socket.error as e:
|
2022-02-05 20:38:57 +00:00
|
|
|
indigo.server.log("\tError opening connection: " + str(e), level=logging.ERROR)
|
|
|
|
self.handle_close()
|
|
|
|
else:
|
|
|
|
self.is_connected = True
|
|
|
|
indigo.server.log("\tConnected to B&O Gateway", level=logging.DEBUG)
|
|
|
|
|
|
|
|
def handle_connect(self):
|
|
|
|
indigo.server.log("\tAttempting to Authenticate...", level=logging.WARNING)
|
|
|
|
self.send_cmd(self._user)
|
|
|
|
self.send_cmd(self._pwd)
|
|
|
|
self.statefilter()
|
|
|
|
|
|
|
|
def handle_close(self):
|
|
|
|
indigo.server.log(self.name + ": Closing socket", level=logging.ERROR)
|
|
|
|
self.is_connected = False
|
|
|
|
self.close()
|
|
|
|
|
2023-11-13 20:00:48 +00:00
|
|
|
def send_cmd(self, payload):
|
|
|
|
payload = payload + "\r\n"
|
|
|
|
payload = payload.encode('UTF8')
|
|
|
|
telegram = bytearray()
|
|
|
|
# append payload
|
|
|
|
for p in payload:
|
|
|
|
telegram.append(p)
|
|
|
|
|
2022-02-05 20:38:57 +00:00
|
|
|
try:
|
2023-11-13 20:00:48 +00:00
|
|
|
self.push(telegram)
|
2022-03-17 21:22:31 +00:00
|
|
|
except socket.timeout as e:
|
2022-02-05 20:38:57 +00:00
|
|
|
indigo.server.log("\tSocket connection timed out: " + str(e), level=logging.ERROR)
|
|
|
|
self.handle_close()
|
2022-03-17 21:22:31 +00:00
|
|
|
except socket.error as e:
|
2022-02-05 20:38:57 +00:00
|
|
|
indigo.server.log("Error sending data: " + str(e), level=logging.ERROR)
|
|
|
|
self.handle_close()
|
|
|
|
else:
|
|
|
|
self.last_sent = telegram
|
|
|
|
self.last_sent_at = time.time()
|
|
|
|
if telegram == 'q Main/global/SYSTEM/BeoLink':
|
|
|
|
if self.debug:
|
2023-11-13 20:00:48 +00:00
|
|
|
indigo.server.log(self.name + " >>-SENT--> : " + payload.decode('UTF8'), level=logging.DEBUG)
|
2022-02-05 20:38:57 +00:00
|
|
|
else:
|
2023-11-13 20:00:48 +00:00
|
|
|
indigo.server.log(self.name + " >>-SENT--> : " + payload.decode('UTF8'), level=logging.INFO)
|
2022-02-05 20:38:57 +00:00
|
|
|
time.sleep(0.2)
|
|
|
|
|
|
|
|
def query(self, zone='*', room='*', dev_type='*', device='*'):
|
|
|
|
query = "q " + zone + "/" + room + "/" + dev_type + '/' + device
|
|
|
|
|
|
|
|
# Convert to human readable string
|
|
|
|
if zone == '*':
|
|
|
|
zone = ' in all zones.'
|
|
|
|
else:
|
|
|
|
zone = ' in zone ' + zone + '.'
|
|
|
|
if room == '*':
|
|
|
|
room = ' in all rooms'
|
|
|
|
else:
|
|
|
|
room = ' in room ' + room
|
|
|
|
if dev_type == '*':
|
|
|
|
dev_type = ' of all types'
|
|
|
|
else:
|
|
|
|
dev_type = ' of type ' + dev_type
|
|
|
|
if device == '*':
|
|
|
|
device = ' all devices'
|
|
|
|
else:
|
|
|
|
device = ' devices called ' + device
|
|
|
|
|
|
|
|
if self.debug:
|
|
|
|
indigo.server.log(self.name + ": sending state update request for" + device + dev_type + room + zone,
|
|
|
|
level=logging.DEBUG)
|
|
|
|
self.send_cmd(query)
|
|
|
|
|
|
|
|
def statefilter(self, zone='*', room='*', dev_type='*', device='*'):
|
|
|
|
s_filter = "f " + zone + "/" + room + "/" + dev_type + '/' + device
|
|
|
|
self.send_cmd(s_filter)
|
|
|
|
|
|
|
|
def locationevent(self, event):
|
|
|
|
if event in ['leave', 'arrive']:
|
|
|
|
event = 'l ' + event
|
|
|
|
self.send_cmd(event)
|
|
|
|
|
|
|
|
def ping(self):
|
|
|
|
self.query('Main', 'global', 'SYSTEM', 'BeoLink')
|
|
|
|
|
|
|
|
# Utility Functions
|
|
|
|
@staticmethod
|
|
|
|
def _srcdictsanitize(d, s):
|
|
|
|
result = d.get(s)
|
|
|
|
if result is None:
|
|
|
|
result = s
|
|
|
|
return str(result)
|
|
|
|
|
|
|
|
def _get_channel_track(self, message):
|
|
|
|
try:
|
|
|
|
node = indigo.devices[message['Device']]
|
|
|
|
# Get properties
|
|
|
|
node_props = node.pluginProps
|
|
|
|
source_name = message["State_Update"]["sourceName"].strip().replace(" ", "_")
|
|
|
|
if self.debug:
|
|
|
|
indigo.server.log('searching device ' + node.name + ' channel list for source ' + source_name,
|
|
|
|
level=logging.DEBUG)
|
|
|
|
if 'channels' in node_props['sources'][source_name]:
|
|
|
|
for channel in node_props['sources'][source_name]['channels']:
|
|
|
|
if self.debug:
|
|
|
|
indigo.server.log(source_name + " Channel " + channel[0][1:] + " = " + channel[1],
|
|
|
|
level=logging.DEBUG)
|
|
|
|
if int(channel[0][1:]) == int(
|
|
|
|
message["State_Update"]['nowPlayingDetails']["channel_track"]):
|
|
|
|
message["State_Update"]["nowPlaying"] = channel[1]
|
|
|
|
if self.debug:
|
|
|
|
indigo.server.log("Current Channel: " + channel[1], level=logging.DEBUG)
|
|
|
|
return
|
|
|
|
|
|
|
|
# If source list exhausted then return Unknown
|
|
|
|
message["State_Update"]["nowPlaying"] = 'Unknown'
|
|
|
|
except KeyError:
|
|
|
|
message["State_Update"]["nowPlaying"] = 'Unknown'
|