Add files via upload

This commit is contained in:
LukeSpad 2021-11-23 15:14:40 +00:00 committed by GitHub
parent 344b61da5a
commit 1b82746dd7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 1713 additions and 0 deletions

234
Resources/BLHIP_CLIENT.py Normal file
View file

@ -0,0 +1,234 @@
import asynchat
import logging
import socket
import time
import json
import urllib
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', cb=None):
asynchat.async_chat.__init__(self)
self.log = logging.getLogger('Client (%7s)' % name)
self.log.setLevel('INFO')
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
self.log.debug(data)
self._received_data += data
def found_terminator(self):
self.last_received = self._received_data
self.last_received_at = time.time()
self.log.debug(self._received_data)
if self._received_data == 'error':
self.handle_close()
if self._received_data == 'e OK f%20%2A/%2A/%2A/%2A':
self.log.info('\tAuthentication Successful!')
self.query(dev_type="AV renderer")
telegram = urllib.unquote(self._received_data)
telegram = telegram.split('/')
header = telegram[0:4]
self._received_data = ""
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':
self.log.info('Command Successfully Processed: ' + urllib.unquote(self._received_data))
elif e_string[2:5] == 'CMD':
self.log.info('Wrong or Unrecognised Command: ' + urllib.unquote(self._received_data))
return
elif e_string[2:5] == 'SYN':
self.log.info('Bad Syntax, or Wrong Character Encoding: ' + urllib.unquote(self._received_data))
return
elif e_string[2:5] == 'ACC':
self.log.info('Zone Access Violation: ' + urllib.unquote(self._received_data))
return
elif e_string[2:5] == 'LEN':
self.log.info('Received Message Too Long: ' + urllib.unquote(self._received_data))
return
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":
playDetails = s.split('=')
if len(playDetails[1]) >0:
playDetails = playDetails[1].split('; ')
message['State_Update']["nowPlayingDetails"] = OrderedDict()
for p in playDetails:
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]
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):
#Report messages, excluding regular clock pings from gateway
self.last_message = message
if message.get('Device').upper() != 'CLOCK':
self.log.debug(self.name + "\n" + str(json.dumps(message, indent=4)))
if self.messageCallBack:
self.messageCallBack(self.name, str(list(header)), str(list(payload)), message)
def client_connect(self):
self.log.info('Connecting to host at %s, port %i', self._host, self._port)
self.set_terminator(b'\r\n')
#Create the socket
try:
socket.setdefaulttimeout(3)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error, e:
self.log.info("Error creating socket: %s" % e)
self.handle_close()
#Now connect
try:
self.connect((self._host, self._port))
except socket.gaierror, e:
self.log.info("\tError with address %s:%i - %s" % (self._host, self._port, e))
self.handle_close()
except socket.error, e:
self.log.info("\tError opening connection to %s:%i - %s" % (self._host, self._port, e))
self.handle_close()
except socket.timeout, e:
self.log.info("\tSocket connection to %s:%i timed out- %s" % (self._host, self._port, e))
self.handle_close()
else:
self.is_connected = True
self.log.info("\tConnected to B&O Gateway")
def handle_connect(self):
self.log.info("\tAttempting to Authenticate...")
self.send_cmd(self._user)
self.send_cmd(self._pwd)
self.statefiler()
def handle_close(self):
self.log.info(self.name + ": Closing socket")
self.is_connected = False
self.close()
def send_cmd(self,telegram):
try:
self.push(telegram.encode("ascii") + "\r\n")
except socket.error, e:
self.log.info("Error sending data: %s" % e)
self.handle_close()
except socket.timeout, e:
self.log.info("\tSocket connection to %s:%i timed out- %s" % (self._host, self._port, e))
self.handle_close()
else:
self.last_sent = telegram
self.last_sent_at = time.time()
self.log.info(self.name + " >>-SENT--> : " + telegram)
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
self.log.info(self.name + ": sending state update request for" + device + dev_type + room + zone)
self.send_cmd(query)
def statefiler(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')
def _srcdictsanitize(self, d, s):
result = d.get(s)
if result == None:
result = s
return str(result)

463
Resources/CONSTANTS.py Normal file
View file

@ -0,0 +1,463 @@
# Constants for B&O telegram protocols
# ########################################################################################
### Config data (set on initialisation)
gateway = dict()
rooms = []
devices = []
available_sources = []
# ########################################################################################
### Beo4 Commands
beo4_commanddict = dict(
[
# Source selection:
(0x0C, "Standby"),
(0x47, "Sleep"),
(0x80, "TV"),
(0x81, "Radio"),
(0x82, "V.Aux/DTV2"),
(0x83, "A.Aux"),
(0x84, "Media"),
(0x85, "V.Tape/V.Mem/DVD2"),
(0x86, "DVD"),
(0x87, "Camera"),
(0x88, "Text"),
(0x8A, "Sat/DTV"),
(0x8B, "PC"),
(0x8C, "Web"),
(0x8D, "Doorcam"),
(0x8E, "Photo"),
(0x90, "USB2"),
(0x91, "A.Tape/A.Mem"),
(0x92, "CD"),
(0x93, "Phono/N.Radio"),
(0x94, "A.Tape2/N.Music"),
(0x95, "Server"),
(0x96, "Spotify"),
(0x97, "CD2/Join"),
(0xBF, "AV"),
(0xFA, "P-IN-P"),
# Digits:
(0x00, "Digit-0"),
(0x01, "Digit-1"),
(0x02, "Digit-2"),
(0x03, "Digit-3"),
(0x04, "Digit-4"),
(0x05, "Digit-5"),
(0x06, "Digit-6"),
(0x07, "Digit-7"),
(0x08, "Digit-8"),
(0x09, "Digit-9"),
# Source control:
(0x1E, "Step Up"),
(0x1F, "Step Down"),
(0x32, "Rewind"),
(0x33, "Return"),
(0x34, "Wind"),
(0x35, "Go/Play"),
(0x36, "Stop"),
(0xD4, "Yellow"),
(0xD5, "Green"),
(0xD8, "Blue"),
(0xD9, "Red"),
# Sound and picture control
(0x0D, "Mute"),
(0x1C, "P.Mute"),
(0x2A, "Format"),
(0x44, "Sound/Speaker"),
(0x5C, "Menu"),
(0x60, "Volume Up"),
(0x64, "Volume Down"),
(0xDA, "Cinema_On"),
(0xDB, "Cinema_Off"),
# Other controls:
(0xF7, "Stand"),
(0x0A, "Clear"),
(0x0B, "Store"),
(0x0E, "Reset"),
(0x14, "Back"),
(0x15, "MOTS"),
(0x20, "Goto"),
(0x28, "Show Clock"),
(0x2D, "Eject"),
(0x37, "Record"),
(0x3F, "Select"),
(0x46, "Sound"),
(0x7F, "Exit"),
(0xC0, "Shift-0/Edit"),
(0xC1, "Shift-1/Random"),
(0xC2, "Shift-2"),
(0xC3, "Shift-3/Repeat"),
(0xC4, "Shift-4/Select"),
(0xC5, "Shift-5"),
(0xC6, "Shift-6"),
(0xC7, "Shift-7"),
(0xC8, "Shift-8"),
(0xC9, "Shift-9"),
# Continue functionality:
(0x70, "Rewind Repeat"),
(0x71, "Wind Repeat"),
(0x72, "Step_UP Repeat"),
(0x73, "Step_DW Repeat"),
(0x75, "Go Repeat"),
(0x76, "Green Repeat"),
(0x77, "Yellow Repeat"),
(0x78, "Blue Repeat"),
(0x79, "Red Repeat"),
(0x7E, "Key Release"),
# Functions:
(0x40, "Guide"),
(0x43, "Info"),
# Cursor functions:
(0x13, "Select"),
(0xCA, "Cursor_Up"),
(0xCB, "Cursor_Down"),
(0xCC, "Cursor_Left"),
(0xCD, "Cursor_Right"),
# Light/Control commands
(0x9B, "Light"),
(0x9C, "Command"),
(0x58, "Light Timeout"),
# Dummy for 'Listen for all commands'
(0xFF, "<all>"),
]
)
BEO4_CMDS = {v.upper(): k for k, v in beo4_commanddict.items()}
### BeoRemote One Commands
beoremoteone_commanddict = dict(
[
#Source, (Cmd, Unit)
("TV", (0x80, 0)),
("RADIO", (0x81, 0)),
("TUNEIN", (0x81, 1)),
("DVB_RADIO", (0x81, 2)),
("AV.IN", (0x82, 0)),
("LINE.IN", (0x83, 0)),
("A.AUX", (0x83, 1)),
("BLUETOOTH", (0x83, 2)),
("HOMEMEDIA", (0x84, 0)),
("DNLA", (0x84, 1)),
("RECORDINGS", (0x85, 0)),
("CAMERA", (0x87, 0)),
("FUTURE.USE", (0x89, 0)),
("USB", (0x90, 0)),
("A.MEM", (0x91, 0)),
("CD", (0x92, 0)),
("N.RADIO", (0x93, 0)),
("MUSIC", (0x94, 0)),
("DNLA-DMR", (0x94, 1)),
("AIRPLAY", (0x94, 2)),
("SPOTIFY", (0x96, 0)),
("DEEZER", (0x96, 1)),
("QPLAY", (0x96, 2)),
("JOIN", (0x97, 0)),
("WEBMEDIA", (0x8C, 0)),
("YOUTUBE", (0x8C, 1)),
("HOME.APP", (0x8C, 2)),
("HDMI_1", (0xCE, 0)),
("HDMI_2", (0xCE, 1)),
("HDMI_3", (0xCE, 2)),
("HDMI_4", (0xCE, 3)),
("HDMI_5", (0xCE, 4)),
("HDMI_6", (0xCE, 5)),
("HDMI_7", (0xCE, 6)),
("HDMI_8", (0xCE, 7)),
("MATRIX_1", (0xCF, 0)),
("MATRIX_2", (0xCF, 1)),
("MATRIX_3", (0xCF, 2)),
("MATRIX_4", (0xCF, 3)),
("MATRIX_5", (0xCF, 4)),
("MATRIX_6", (0xCF, 5)),
("MATRIX_7", (0xCF, 6)),
("MATRIX_8", (0xCF, 7)),
("MATRIX_9", (0xD0, 0)),
("MATRIX_10", (0xD0, 1)),
("MATRIX_11", (0xD0, 2)),
("MATRIX_12", (0xD0, 3)),
("MATRIX_13", (0xD0, 4)),
("MATRIX_14", (0xD0, 5)),
("MATRIX_15", (0xD0, 6)),
("MATRIX_16", (0xD0, 7)),
("PERSONAL_1", (0xD1, 0)),
("PERSONAL_2", (0xD1, 1)),
("PERSONAL_3", (0xD1, 2)),
("PERSONAL_4", (0xD1, 3)),
("PERSONAL_5", (0xD1, 4)),
("PERSONAL_6", (0xD1, 5)),
("PERSONAL_7", (0xD1, 6)),
("PERSONAL_8", (0xD1, 7)),
("TV.ON", (0xD2, 0)),
("MUSIC.ON", (0xD3, 0)),
("PATTERNPLAY",(0xD3, 1)),
]
)
# ########################################################################################
# Source Activity
_sourceactivitydict = dict(
[
(0x00, "Unknown"),
(0x01, "Stop"),
(0x02, "Play"),
(0x03, "Wind"),
(0x04, "Rewind"),
(0x05, "Record Lock"),
(0x06, "Standby"),
(0x07, "Load/No Media"),
(0x08, "Still Picture"),
(0x14, "Scan Forward"),
(0x15, "Scan Reverse"),
(0xFF, "None"),
]
)
# ########################################################################################
# ##### MasterLink (not MLGW) Protocol packet constants
_ml_telegram_type_dict = dict(
[
(0x0A, "COMMAND"),
(0x0B, "REQUEST"),
(0x14, "RESPONSE"),
(0x2C, "INFO"),
(0x5E, "CONFIG"),
]
)
_ml_command_type_dict = dict(
[
(0x04, "MASTER_PRESENT"),
# REQUEST_DISTRIBUTED_SOURCE: seen when a device asks what source is being distributed
# subtypes seen 01:request 04:no source 06:has source (byte 13 is source)
(0x08, "REQUEST_DISTRIBUTED_SOURCE"),
(0x0D, "BEO4_KEY"),
(0x10, "STANDBY"),
(0x11, "RELEASE"), # when a device turns off
(0x20, "MLGW_REMOTE_BEO4"),
# REQUEST_LOCAL_SOURCE: Seen when a device asks what source is playing locally to a device
# subtypes seen 02:request 04:no source 05:secondary source 06:primary source (byte 11 is source)
# byte 10 is bitmask for distribution: 0x01: coaxial cable - 0x02: MasterLink ML_BUS -
# 0x08: local screen
(0x30, "REQUEST_LOCAL_SOURCE"),
(0x3C, "TIMER"),
(0x40, "CLOCK"),
(0x44, "TRACK_INFO"),
# LOCK_MANAGER_COMMAND: Lock to Determine what device issues source commands
# reference: https://tidsskrift.dk/daimipb/article/download/7043/6004/0
(0x45, "GOTO_SOURCE"),
(0x5C, "LOCK_MANAGER_COMMAND"),
(0x6C, "DISTRIBUTION_REQUEST"),
(0x82, "TRACK_INFO_LONG"),
# Source Status
# byte 10:source - byte 13: 80 when DTV is turned off. 00 when it's on
# byte 18H 17L: source medium - byte 19: channel/track - byte 21:activity
# byte 22: 01: audio source 02: video source ff:undefined - byte 23: picture identifier
(0x87, "STATUS_INFO"),
(0x94, "VIDEO_TRACK_INFO"),
#
# -----------------------------------------------------------------------
# More packets that we see on the bus, with a guess of the type
# DISPLAY_SOURCE: Message sent with a payload showing the displayed source name.
# subtype 3 has the printable source name starting at byte 10 of the payload
(0x06, "DISPLAY_SOURCE"),
# START_VIDEO_DISTRIBUTION: Sent when a locally playing source starts being distributed on coaxial cable
(0x07, "START_VIDEO_DISTRIBUTION"),
# EXTENDED_SOURCE_INFORMATION: message with 6 subtypes showing information about the source.
# Printable info at byte 14 of the payload
# For Radio: 1: "" 2: Genre 3: Country 4: RDS info 5: Associated beo4 button 6: "Unknown"
# For A.Mem: 1: Genre 2: Album 3: Artist 4: Track name 5: Associated beo4 button 6: "Unknown"
(0x0B, "EXTENDED_SOURCE_INFORMATION"),
(0x96, "PC_PRESENT"),
# PICTURE AND SOUND STATUS
# byte 0: bit 0-1: sound status - bit 2-3: stereo mode (can be 0 in a 5.1 setup)
# byte 1: speaker mode (see below)
# byte 2: audio volume
# byte 3: picture format identifier (see below)
# byte 4: bit 0: screen1 mute - bit 1: screen2 mute - bit 2: screen1 active -
# bit 3: screen2 active - bit 4: cinema mode
(0x98, "PICT_SOUND_STATUS"),
# Unknown commands - seen on power up and initialisation
#########################################################
# On power up all devices send out a request key telegram. If
# no lock manager is allocated the devices send out a key_lost telegram. The Video Master (or Power
# Master in older implementations) then asserts a NEW_LOCK_MANAGER telegram and assumes responsibility
# for LOCK_MANAGER_COMMAND telegrams until a key transfer occurs.
(0x12, "KEY_LOST"), #?
# Unknown command with payload of length 1.
# bit 0: unknown
# bit 1: unknown
(0xA0, "NEW_LOCK_MANAGER"), #?
# Unknown command with payload of length 2
# bit 0: unknown
# bit 1: unknown
# bit 2: unknown
]
)
_ml_command_type_request_key_subtype_dict = dict(
[
(0x01, "Request Key"),
(0x02, "Transfer Key"),
(0x03, "Transfer Impossible"),
(0x04, "Key Received"),
(0x05, "Timeout"),
(0xFF, "Undefined"),
]
)
_ml_activity_dict = dict(
[
(0x01, "Request Source"),
(0x02, "Request Source"),
(0x04, "No Source"),
(0x06, "Source Active"),
]
)
_ml_device_dict = dict(
[
(0xC0, "VIDEO_MASTER"),
(0xC1, "AUDIO_MASTER"),
(0xC2, "SOURCE_CENTER"),
(0x81, "ALL_AUDIO_LINK_DEVICES"),
(0x82, "ALL_VIDEO_LINK_DEVICES"),
(0x83, "ALL_LINK_DEVICES"),
(0x80, "ALL"),
(0xF0, "MLGW"),
# Power Master exists in older (pre 1996?) ML implementations. Later revisions enforced the Video Master
# as lock key manager for the system and the concept was phased out. If your system is older than 2000
# you may see this device type on the network.
(0xFF, "POWER_MASTER"), #?
]
)
_ml_pictureformatdict = dict(
[
(0x00, "Not known"),
(0x01, "Known by decoder"),
(0x02, "4:3"),
(0x03, "16:9"),
(0x04, "4:3 Letterbox middle"),
(0x05, "4:3 Letterbox top"),
(0x06, "4:3 Letterbox bottom"),
(0xFF, "Blank picture"),
]
)
_ml_selectedsourcedict = dict(
[
(0x00, "NONE"),
(0x0B, "TV"),
(0x15, "V.TAPE/V.MEM"),
(0x16, "DVD2"),
(0x1F, "DTV"),
(0x29, "DVD"),
(0x33, "V.AUX"),
(0x3E, "DOORCAM"),
(0x47, "PC"),
(0x6F, "RADIO"),
(0x79, "A.TAPE/A.MEM"),
(0x7A, "A.TAPE2/N.MUSIC"),
(0x8D, "CD"),
(0x97, "A.AUX"),
(0xA1, "PHONO/N.RADIO"),
# Dummy for 'Listen for all sources'
(0xFE, "ALL"), # have also seen 0xFF as "all"
(0xFF, "ALL"),
]
)
_ml_trackinfo_subtype_dict = dict([(0x05, "Current Source"),(0x07, "Change Source"),])
_ml_selectedsource_type_dict = dict(
[
("VIDEO", (0x0B, 0x1F)),
("VIDEO_PAUSABLE", (0x15, 0x16, 0x29, 0x33)),
("AUDIO", (0x6F, 0x97)),
("AUDIO_PAUSABLE", (0x8D, 0x79, 0x7A, 0xA1, 0x8D)),
("ALL", (0xFE, 0xFF)),
("OTHER", (0x47, 0x3E)),
]
)
# ########################################################################################
# ##### MLGW Protocol packet constants
_mlgw_payloadtypedict = dict(
[
(0x01, "Beo4 Command"),
(0x02, "Source Status"),
(0x03, "Picture and Sound Status"),
(0x04, "Light and Control command"),
(0x05, "All standby notification"),
(0x06, "BeoRemote One control command"),
(0x07, "BeoRemote One source selection"),
(0x20, "MLGW virtual button event"),
(0x30, "Login request"),
(0x31, "Login status"),
(0x32, "Change password request"),
(0x33, "Change password response"),
(0x34, "Secure login request"),
(0x36, "Ping"),
(0x37, "Pong"),
(0x38, "Configuration change notification"),
(0x39, "Request Serial Number"),
(0x3A, "Serial Number"),
(0x40, "Location based event"),
]
)
MLGW_PL = {v.upper(): k for k, v in _mlgw_payloadtypedict.items()}
_destselectordict = dict(
[
(0x00, "Video Source"),
(0x01, "Audio Source"),
(0x05, "V.TAPE/V.MEM"),
(0x0F, "All Products"),
(0x1B, "MLGW"),
]
)
CMDS_DEST = {v.upper(): k for k, v in _destselectordict.items()}
_mlgw_secsourcedict = dict([(0x00, "V.TAPE/V.MEM"),(0x01, "V.TAPE2/DVD2/V.MEM2"),])
_mlgw_linkdict = dict([(0x00, "Local/Default Source"),(0x01, "Remote Source/Option 4 Product"),])
_mlgw_virtualactiondict = dict([(0x01, "PRESS"), (0x02, "HOLD"), (0x03, "RELEASE")])
### for '0x03: Picture and Sound Status'
_mlgw_soundstatusdict = dict([(0x00, "Not muted"), (0x01, "Muted")])
_mlgw_speakermodedict = dict(
[
(0x01, "Center channel"),
(0x02, "2ch stereo"),
(0x03, "Front surround"),
(0x04, "4ch stereo"),
(0x05, "Full surround"),
(0xFD, "<all>"), # Dummy for 'Listen for all modes'
]
)
_mlgw_screenmutedict = dict([(0x00, "not muted"), (0x01, "muted")])
_mlgw_screenactivedict = dict([(0x00, "not active"), (0x01, "active")])
_mlgw_cinemamodedict = dict([(0x00, "Cinemamode=off"), (0x01, "Cinemamode=on")])
_mlgw_stereoindicatordict = dict([(0x00, "Mono"), (0x01, "Stereo")])
### for '0x04: Light and Control command'
_mlgw_lctypedict = dict([(0x01, "LIGHT"), (0x02, "CONTROL")])
### for '0x31: Login Status
_mlgw_loginstatusdict = dict([(0x00, "OK"), (0x01, "FAIL")])
# ########################################################################################
# ##### BeoLink Gateway Protocol packet constants
_blgw_srcdict = dict(
[
("TV", "TV"),
("DVD", "DVD"),
("RADIO", "RADIO"),
("TP1", "A.TAPE/A.MEM"),
("TP2", "A.TAPE2/N.MUSIC"),
("CD", "CD"),
("PH", "PHONO/N.RADIO"),
]
)

301
Resources/MLCLI_CLIENT.py Normal file
View file

@ -0,0 +1,301 @@
import asynchat
import logging
import socket
import time
import json
from collections import OrderedDict
import Resources.CONSTANTS as const
class MLCLIClient(asynchat.async_chat):
"""Client to monitor raw packet traffic on the Masterlink network via the undocumented command line interface
of the Bang & Olufsen Gateway."""
def __init__(self, host_address='blgw.local', port=23, user='admin', pwd='admin', name='ML_CLI', cb=None):
asynchat.async_chat.__init__(self)
self.log = logging.getLogger('Client (%7s)' % name)
self.log.setLevel('INFO')
self._host = host_address
self._port = int(port)
self._user = user
self._pwd = pwd
self.name = name
self.is_connected = False
self._i = 0
self._header_lines = 6
self._received_data = ""
self.last_sent = ''
self.last_sent_at = time.time()
self.last_received = ''
self.last_received_at = time.time()
self.last_message = {}
self.isBLGW = False
# 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.log.debug(data)
self._received_data += data
def found_terminator(self):
self.last_received = self._received_data
self.last_received_at = time.time()
self.log.debug(self._received_data)
telegram = self._received_data
self._received_data = ""
#clear login process lines before processing telegrams
if self._i <= self._header_lines:
self._i += 1
if self._i == self._header_lines - 1:
self.log.info("\tAuthenticated! Gateway type is " + telegram[0:4] + "\n")
if telegram[0:4] != "MLGW":
self.isBLGW = True
#Process telegrams and return json data in human readable format
if self._i > self._header_lines:
items = telegram.split()[1:]
if len(items):
telegram=bytearray()
for item in items:
try:
telegram.append(int(item[:-1],base=16))
except:
#abort if invalid character found
break
#Decode any telegram with a valid 9 byte header, excluding typy 0x14 (regular clock sync pings)
if len(telegram) >= 9 and telegram[7] != 0x14:
#Header: To_Device/From_Device/1/Type/To_Source/From_Source/0/Payload_Type/Length
header = telegram[:9]
payload = telegram[9:]
message = self._decode(telegram)
self._report(header, payload, message)
def client_connect(self):
self.log.info('Connecting to host at %s, port %i', self._host, self._port)
self.set_terminator(b'\r\n')
# Create the socket
try:
socket.setdefaulttimeout(3)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error, e:
self.log.info("Error creating socket: %s" % e)
self.handle_close()
# Now connect
try:
self.connect((self._host, self._port))
except socket.gaierror, e:
self.log.info("\tError with address %s:%i - %s" % (self._host, self._port, e))
self.handle_close()
except socket.error, e:
self.log.info("\tError opening connection to %s:%i - %s" % (self._host, self._port, e))
self.handle_close()
except socket.timeout, e:
self.log.info("\tSocket connection to %s:%i timed out- %s" % (self._host, self._port, e))
self.handle_close()
else:
self.is_connected = True
self.log.info("\tConnected to B&O Gateway")
def handle_connect(self):
self.log.info("\tAttempting to Authenticate...")
self.send_cmd(self._pwd)
self.send_cmd("_MLLOG ONLINE")
def handle_close(self):
self.log.info(self.name + ": Closing socket")
self.is_connected = False
self.close()
def send_cmd(self,telegram):
try:
self.push(telegram + "\r\n")
except socket.error, e:
self.log.info("Error sending data: %s" % e)
self.handle_close()
except socket.timeout, e:
self.log.info("\tSocket connection to %s:%i timed out- %s" % (self._host, self._port, e))
self.handle_close()
else:
self.last_sent = telegram
self.last_sent_at = time.time()
self.log.info(self.name + " >>-SENT--> : " + telegram)
time.sleep(0.2)
def _report(self, header, payload, message):
# Report messages, excluding regular clock pings from gateway
self.last_message = message
self.log.debug(self.name + "\n" + str(json.dumps(message, indent=4)))
if self.messageCallBack:
self.messageCallBack(self.name, str(list(header)), str(list(payload)), message)
def ping(self):
self.log.info(self.name + " >>-SENT--> : Ping")
self.push('\n')
# ########################################################################################
# ##### Utility functions
def _hexbyte(self, byte):
resultstr = hex(byte)
if byte < 16:
resultstr = resultstr[:2] + "0" + resultstr[2]
return resultstr
def _hexword(self, byte1, byte2):
resultstr = self._hexbyte(byte2)
resultstr = self._hexbyte(byte1) + resultstr[2:]
return resultstr
def _dictsanitize(self, d, s):
result = d.get(s)
if result == None:
result = self._hexbyte(s)
self.log.debug("UNKNOWN (type=" + result + ")")
return str(result)
def _get_type(self, d, s):
rev_dict = {value: key for key, value in d.items()}
for i in range(len(list(rev_dict))):
if s in list(rev_dict)[i]:
return rev_dict.get(list(rev_dict)[i])
# ########################################################################################
# ##### Decode Masterlink Protocol packet to a serializable dict
def _decode(self, telegram):
# Decode header
message = OrderedDict()
if const.devices:
for device in const.devices:
if device['ML_ID'] == telegram[1]:
message["Zone"] = device["Zone"].upper()
message["Room"] = device["Room"].uppr()
message["Type"] = "AV RENDERER"
message["Device"] = device["Device"]
message["from_device"] = self._dictsanitize(const._ml_device_dict, telegram[1])
message["from_source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[5])
message["to_device"] = self._dictsanitize(const._ml_device_dict, telegram[0])
message["to_source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[4])
message["type"] = self._dictsanitize(const._ml_telegram_type_dict, telegram[3])
message["payload_type"] = self._dictsanitize(const._ml_command_type_dict, telegram[7])
message["payload_len"] = telegram[8] + 1
message["payload"] = OrderedDict()
# source status info
# TTFF__TYDSOS__PTLLPS SR____LS______SLSHTR__ACSTPI________________________TRTR______
if message.get("payload_type") == "STATUS_INFO":
message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[10])
message["payload"]["sourceID"] = telegram[10]
message["payload"]["source_type"] = telegram[22]
message["payload"]["local_source"] = telegram[13]
message["payload"]["source_medium"] = self._hexword(telegram[18], telegram[17])
message["payload"]["channel_track"] = (
telegram[19] if telegram[8] < 27 else (telegram[36] * 256 + telegram[37])
)
message["payload"]["picture_identifier"] = self._dictsanitize(const._ml_pictureformatdict, telegram[23])
message["payload"]["state"] = self._dictsanitize(const._sourceactivitydict, telegram[21])
# display source information
if message.get("payload_type") == "DISPLAY_SOURCE":
_s = ""
for i in range(0, telegram[8] - 5):
_s = _s + chr(telegram[i + 15])
message["payload"]["display_source"] = _s.rstrip()
# extended source information
if message.get("payload_type") == "EXTENDED_SOURCE_INFORMATION":
message["payload"]["info_type"] = telegram[10]
_s = ""
for i in range(0, telegram[8] - 14):
_s = _s + chr(telegram[i + 24])
message["payload"]["info_value"] = _s
# beo4 command
if message.get("payload_type") == "BEO4_KEY":
message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[10])
message["payload"]["sourceID"] = telegram[10]
message["payload"]["source_type"] = self._get_type(const._ml_selectedsource_type_dict, telegram[10])
message["payload"]["command"] = self._dictsanitize(const.beo4_commanddict, telegram[11])
# audio track info long
if message.get("payload_type") == "TRACK_INFO_LONG":
message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[11])
message["payload"]["sourceID"] = telegram[11]
message["payload"]["source_type"] = self._get_type(const._ml_selectedsource_type_dict, telegram[11])
message["payload"]["channel_track"] = telegram[12]
message["payload"]["state"] = self._dictsanitize(const._sourceactivitydict, telegram[13])
# video track info
if message.get("payload_type") == "VIDEO_TRACK_INFO":
message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[13])
message["payload"]["sourceID"] = telegram[13]
message["payload"]["source_type"] = self._get_type(const._ml_selectedsource_type_dict, telegram[13])
message["payload"]["channel_track"] = telegram[11] * 256 + telegram[12]
message["payload"]["state"] = self._dictsanitize(const._sourceactivitydict, telegram[14])
# track change info
if message.get("payload_type") == "TRACK_INFO":
message["payload"]["subtype"] = self._dictsanitize(const._ml_trackinfo_subtype_dict, telegram[9])
if message["payload"].get("subtype") == "Change Source":
message["payload"]["prev_source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[11])
message["payload"]["prev_sourceID"] = telegram[11]
message["payload"]["prev_source_type"] = self._get_type(
const._ml_selectedsource_type_dict, telegram[11])
if len(telegram) > 18:
message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[22])
message["payload"]["sourceID"] = telegram[22]
if message["payload"].get("subtype") == "Current Source":
message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[11])
message["payload"]["sourceID"] = telegram[11]
message["payload"]["source_type"] = self._get_type(const._ml_selectedsource_type_dict, telegram[11])
else:
message["payload"]["subtype"] = "Undefined: " + self._hexbyte(telegram[9])
# goto source
if message.get("payload_type") == "GOTO_SOURCE":
message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[11])
message["payload"]["sourceID"] = telegram[11]
message["payload"]["source_type"] = self._get_type(const._ml_selectedsource_type_dict, telegram[11])
message["payload"]["channel_track"] = telegram[12]
# remote request
if message.get("payload_type") == "MLGW_REMOTE_BEO4":
message["payload"]["command"] = self._dictsanitize(const.beo4_commanddict, telegram[14])
message["payload"]["destination"] = self._dictsanitize(const._destselectordict, telegram[11])
# request_key
if message.get("payload_type") == "LOCK_MANAGER_COMMAND":
message["payload"]["subtype"] = self._dictsanitize(
const._ml_command_type_request_key_subtype_dict, telegram[9])
# request distributed audio source
if message.get("payload_type") == "REQUEST_DISTRIBUTED_SOURCE":
message["payload"]["subtype"] = self._dictsanitize(const._ml_activity_dict, telegram[9])
if message["payload"].get('subtype') == "Source Active":
message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[13])
message["payload"]["sourceID"] = telegram[13]
message["payload"]["source_type"] = self._get_type(const._ml_selectedsource_type_dict, telegram[13])
# request local audio source
if message.get("payload_type") == "REQUEST_LOCAL_SOURCE":
message["payload"]["subtype"] = self._dictsanitize(const._ml_activity_dict, telegram[9])
if message["payload"].get('subtype') == "Source Active":
message["payload"]["source"] = self._dictsanitize(const._ml_selectedsourcedict, telegram[11])
message["payload"]["sourceID"] = telegram[11]
message["payload"]["source_type"] = self._get_type(const._ml_selectedsource_type_dict, telegram[11])
return message

362
Resources/MLGW_CLIENT.py Normal file
View file

@ -0,0 +1,362 @@
import asynchat
import logging
import socket
import time
import json
from collections import OrderedDict
import Resources.CONSTANTS as const
class MLGWClient(asynchat.async_chat):
"""Client to interact with a B&O Gateway via the MasterLink Gateway Protocol
http://mlgw.bang-olufsen.dk/source/documents/mlgw_2.24b/MlgwProto0240.pdf ."""
def __init__(self, host_address='blgw.local', port=9000, user='admin', pwd='admin', name='MLGW_Protocol', cb=None):
asynchat.async_chat.__init__(self)
self.log = logging.getLogger('Client (%7s)' % name)
self.log.setLevel('INFO')
self._host = host_address
self._port = int(port)
self._user = user
self._pwd = pwd
self.name = name
self.is_connected = False
self._received_data = bytearray()
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
#Expose dictionaries via API
self.BEO4_CMDS = const.BEO4_CMDS
self.CMDS_DEST = const.CMDS_DEST
self.MLGW_PL = const.MLGW_PL
# ########################################################################################
# ##### Open Socket and connect to B&O Gateway
self.client_connect()
# ########################################################################################
# ##### Client functions
def collect_incoming_data(self, data):
self.is_connected = True
self.log.debug(data)
self._received_data = bytearray(data)
bit1 = int(self._received_data[0]) #Start of Header == 1
bit2 = int(self._received_data[1]) #Message Type
bit3 = int(self._received_data[2]) #Payload length
bit4 = int(self._received_data[3]) #Spare Bit/End of Header == 0
payload = bytearray()
for item in self._received_data[4:bit3 + 4]:
payload.append(item)
if bit1 == 1 and len(self._received_data) == bit3 + 4 and bit4 == 0:
self.found_terminator(bit2, payload)
else:
self.log.info("Incomplete Telegram Received: " + str(list(self._received_data)) + " - Ignoring!\n")
def found_terminator(self, msg_type, payload):
self.last_received = str(list(self._received_data))
self.last_received_at = time.time()
self.log.debug(self._received_data)
header = self._received_data[0:4]
self._received_data = ""
self._decode(msg_type, header, payload)
def _decode(self, msg_type, header, payload):
message = OrderedDict()
payload_type = self._dictsanitize(const._mlgw_payloadtypedict, msg_type)
message["Payload_type"] = payload_type
if payload_type == "MLGW virtual button event":
virtual_btn = payload[0]
if len(payload) < 1:
virtual_action = self._getvirtualactionstr(0x01)
else:
virtual_action = self._getvirtualactionstr(payload[1])
message["button"] = virtual_btn
message["action"] = virtual_action
elif payload_type == "Login status":
if payload == 0:
self.log.info("\tAuthentication Failed: MLGW protocol Password required for %s", self._host)
self.handle_close()
message['Connected'] = "False"
return
else:
self.log.info("\tLogin successful to %s", self._host)
self.is_connected = True
message['Connected'] = "True"
self.get_serial()
elif payload_type == "Pong":
self.is_connected = True
message['CONNECTION'] = 'Online'
elif payload_type == "Serial Number":
sn = ''
for c in payload:
sn += chr(c)
message['Serial_Num'] = sn
elif payload_type == "Source Status":
if const.rooms and const.devices:
for device in const.devices:
if device['MLN'] == payload[0]:
name = device['Device']
for room in const.rooms:
if name in room['Products']:
message["Zone"] = room['Zone'].upper()
message["Room"] = room['Room_Name'].upper()
message["Type"] = 'AV RENDERER'
message["Device"] = name
message["MLN"] = payload[0]
message["Source"] = self._getselectedsourcestr(payload[1]).upper()
message["Source_medium_position"] = self._hexword(payload[2], payload[3])
message["Source_position"] = self._hexword(payload[4], payload[5])
message["Picture_format"] = self._getdictstr(const.ml_pictureformatdict, payload[7])
message["State"] = self._getdictstr(const._sourceactivitydict, payload[6])
elif payload_type == "Picture and Sound Status":
if const.rooms and const.devices:
for device in const.devices:
if device['MLN'] == payload[0]:
name = device['Device']
for room in const.rooms:
if name in room['Products']:
message["Zone"] = room['Zone'].upper()
message["Room"] = room['Room_Name'].upper()
message["Type"] = 'AV RENDERER'
message["Device"] = name
message["MLN"] = payload[0]
message["Sound_status"] = self._getdictstr(const.mlgw_soundstatusdict, payload[1])
message["Speaker_mode"] = self._getdictstr(const._mlgw_speakermodedict, payload[2])
message["Stereo_mode"] = self._getdictstr(const._mlgw_stereoindicatordict, payload[9])
message["Volume"] = int(payload[3])
message["Screen1_mute"] = self._getdictstr(const._mlgw_screenmutedict, payload[4])
message["Screen1_active"] = self._getdictstr(const._mlgw_screenactivedict, payload[5])
message["Screen2_mute"] = self._getdictstr(const._mlgw_screenmutedict, payload[6])
message["Screen2_active"] = self._getdictstr(const._mlgw_screenactivedict, payload[7])
message["Cinema_mode"] = self._getdictstr(const._mlgw_cinemamodedict, payload[8])
elif payload_type == "All standby notification":
message["Command"] = "All Standby"
elif payload_type == "Light and Control command":
if const.rooms:
for room in const.rooms:
if room['Room_Number'] == payload[0]:
message["Zone"] = room['Zone'].upper()
message["Room"] = room['Room_Name'].upper()
message["Type"] = self._getdictstr(const._mlgw_lctypedict, payload[1]).upper() + " COMMAND"
message["Device"] = 'Beo4/BeoRemote One'
message["Room number"] = str(payload[0])
message["Command"] = self._getbeo4commandstr(payload[2])
if message != '':
self._report(header, payload, message)
def client_connect(self):
self.log.info('Connecting to host at %s, port %i', self._host, self._port)
self.set_terminator(b'\r\n')
# Create the socket
try:
socket.setdefaulttimeout(3)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error, e:
self.log.info("Error creating socket: %s" % e)
self.handle_close()
# Now connect
try:
self.connect((self._host, self._port))
except socket.gaierror, e:
self.log.info("\tError with address %s:%i - %s" % (self._host, self._port, e))
self.handle_close()
except socket.error, e:
self.log.info("\tError opening connection to %s:%i - %s" % (self._host, self._port, e))
self.handle_close()
except socket.timeout, e:
self.log.info("\tSocket connection to %s:%i timed out- %s" % (self._host, self._port, e))
self.handle_close()
else:
self.is_connected = True
self.log.info("\tConnected to B&O Gateway")
def handle_connect(self):
login=[]
for c in self._user:
login.append(c)
login.append(0x00)
for c in self._pwd:
login.append(c)
self.log.info("\tAttempting to Authenticate...")
self._send_cmd(const.MLGW_PL.get("LOGIN REQUEST"), login)
def handle_close(self):
self.log.info(self.name + ": Closing socket")
self.is_connected = False
self.close()
def _report(self, header, payload, message):
self.last_message = message
self.log.debug(self.name + "\n" + str(json.dumps(message, indent=4)))
if self.messageCallBack:
self.messageCallBack(self.name, str(list(header)), str(list(payload)), message)
# ########################################################################################
# ##### mlgw send_cmder functions
## send_cmd command to mlgw
def _send_cmd(self, msg_type, payload):
#construct header
telegram = [1,msg_type,len(payload),0]
#append payload
for p in payload:
telegram.append(p)
try:
self.push(str(bytearray(telegram)))
except socket.error, e:
self.log.info("Error sending data: %s" % e)
self.handle_close()
except socket.timeout, e:
self.log.info("\tSocket connection to %s:%i timed out- %s" % (self._host, self._port, e))
self.handle_close()
else:
self.last_sent = str(bytearray(telegram))
self.last_sent_at = time.time()
self.log.info(
self.name + " >>-SENT--> "
+ self._getpayloadtypestr(msg_type)
+ ": "
+ str(list(telegram))
)
# Sleep to allow msg to arrive
time.sleep(0.2)
def ping(self):
self._send_cmd(const.MLGW_PL.get("PING"), "")
## Get serial number of mlgw
def get_serial(self):
if self.is_connected:
# Request serial number
self._send_cmd(const.MLGW_PL.get("REQUEST SERIAL NUMBER"), "")
## send_cmd Beo4 command to mlgw
def send_beo4_cmd(self, mln, dest, cmd, sec_source=0x00, link=0x00):
payload = []
payload.append(mln) # byte[0] MLN
payload.append(dest) # byte[1] Dest-Sel (0x00, 0x01, 0x05, 0x0f)
payload.append(cmd) # byte[2] Beo4 Command
payload.append(sec_source) # byte[3] Sec-Source
payload.append(link) # byte[4] Link
self._send_cmd(const.MLGW_PL.get("BEO4 COMMAND"), payload)
## send_cmd BeoRemote One command to mlgw
def send_beoremoteone_cmd(self, mln, cmd, network_bit=0x00):
payload = []
payload.append(mln) # byte[0] MLN
payload.append(cmd) # byte[1] Beo4 Command
payload.append(0x00) # byte[2] AV (needs to be 0)
payload.append(network_bit) # byte[3] Network_bit (0 = local source, 1 = network source)
self._send_cmd(const.MLGW_PL.get("BEOREMOTE ONE CONTROL COMMAND"), payload)
## send_cmd BeoRemote One Source Select to mlgw
def send_beoremoteone_select_source(self, mln, cmd, unit, network_bit=0x00):
payload = []
payload.append(mln) # byte[0] MLN
payload.append(cmd) # byte[1] Beormyone Command
payload.append(unit) # byte[2] Unit
payload.append(0x00) # byte[3] AV (needs to be 0)
payload.append(network_bit) # byte[4] Network_bit (0 = local source, 1 = network source)
self._send_cmd(const.MLGW_PL.get("BEOREMOTE ONE SOURCE SELECTION"), payload)
## send_cmd Beo4 commmand and store the source name
def send_beo4_select_source(self, mln, dest, source, sec_source=0x00, link=0x00):
beolink_source = self._dictsanitize(const.beo4_commanddict, source).upper()
self.send_beo4_cmd(mln, dest, source, sec_source, link)
# ########################################################################################
# ##### Utility functions
def _hexbyte(self, byte):
resultstr = hex(byte)
if byte < 16:
resultstr = resultstr[:2] + "0" + resultstr[2]
return resultstr
def _hexword(self, byte1, byte2):
resultstr = self._hexbyte(byte2)
resultstr = self._hexbyte(byte1) + resultstr[2:]
return resultstr
def _dictsanitize(self, d, s):
result = d.get(s)
if result == None:
result = "UNKNOWN (type=" + self._hexbyte(s) + ")"
return str(result)
# ########################################################################################
# ##### Decode MLGW Protocol packet to readable string
## Get message string for mlgw packet's payload type
#
def _getpayloadtypestr(self, payloadtype):
result = const._mlgw_payloadtypedict.get(payloadtype)
if result == None:
result = "UNKNOWN (type=" + self._hexbyte(payloadtype) + ")"
return str(result)
def _getmlnstr(self, mln):
result = "MLN=" + str(mln)
return result
def _getbeo4commandstr(self, command):
result = const.beo4_commanddict.get(command)
if result == None:
result = "Cmd=" + self._hexbyte(command)
return result
def _getvirtualactionstr(self, action):
result = const._mlgw_virtualactiondict.get(action)
if result == None:
result = "Action=" + self._hexbyte(action)
return result
def _getselectedsourcestr(self, source):
result = const.ml_selectedsourcedict.get(source)
if result == None:
result = "Src=" + self._hexbyte(source)
return result
def _getspeakermodestr(self, source):
result = const._mlgw_speakermodedict.get(source)
if result == None:
result = "mode=" + self._hexbyte(source)
return result
def _getdictstr(self, mydict, mykey):
result = mydict.get(mykey)
if result == None:
result = self._hexbyte(mykey)
return result

353
Resources/MLtn_CLIENT.py Normal file
View file

@ -0,0 +1,353 @@
import asynchat
import logging
import socket
import time
import json
from collections import OrderedDict
import Resources.CONSTANTS as const
class MLtnClient(asynchat.async_chat):
"""Client to monitor network activity on a Masterlink Gateway via the telnet monitor"""
def __init__(self, host_address='mlgw.local', port=23, user='admin', pwd='admin', name='MLGW_HIP', cb=None):
asynchat.async_chat.__init__(self)
self.log = logging.getLogger('Client (%7s)' % name)
self.log.setLevel('INFO')
self._host = host_address
self._port = int(port)
self._user = user
self._pwd = pwd
self.name = name
self.is_connected = False
self._i = 0
self._header_lines = 4
self._received_data = ''
self.last_sent = ''
self.last_sent_at = time.time()
self.last_received = ''
self.last_received_at = time.time()
self.last_message = {}
self.isBLGW = False
# 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.log.debug(data)
self._received_data += data
def found_terminator(self):
self.last_received = self._received_data
self.last_received_at = time.time()
self.log.debug(self._received_data)
items = self._received_data.split(' ')
if self._i <= self._header_lines:
self._i += 1
if self._received_data[0:4] != "MLGW":
self.isBLGW = True
if self._i == self._header_lines - 1:
self.log.info("\t" + self._received_data)
if self._received_data == 'incorrect password':
self.handle_close()
else:
self._decode(items)
self._received_data = ""
self.last_sent = ''
self.last_sent_at = 0
self.last_received = ''
self.last_received_at = 0
def _decode(self, items):
time_stamp = ''.join([items[1],' at ',items[0]])
header = items[3][:-1]
telegramStarts = len(''.join(items[:4])) + 4
telegram = self._received_data[telegramStarts:].replace('!','').split('/')
message = OrderedDict()
if header == 'integration_protocol':
message = self._decode_ip(telegram, message)
if header == 'resource_found':
message['State_Update'] = telegram[0]
if header == 'action_executed':
message = self._decode_action(telegram, message)
if header == 'command_executed':
message = self._decode_command(telegram, message)
if header == 'macro_fired':
message['Zone'] = telegram[0].upper()
message['Room'] = telegram[1].upper()
message['Macro_Name'] = telegram[3]
if header == 'trigger_fired':
message = self._decode_trigger(telegram, message)
self._report(header, telegram, message)
def _decode_ip(self, telegram, message):
if ''.join(telegram).split(':')[0] == 'Integration Protocol login':
chars = ''.join(telegram).split(':')[1][2:].split(' ')
message['Payload'] = ''
for c in chars:
if c == '0x0':
message['Payload'] += '0'
else:
message['Payload'] += chr(int(c, base=16))
if ''.join(telegram).split(':')[0] == 'Integration Protocol':
if ''.join(telegram).split(':')[1] == ' processed serial number request':
message['Payload'] = 'processed serial number request'
else:
s = ''.join(telegram).split(' ')
message['Type'] = 'Send Beo4 Command'
message[s[5]] = s[6]
message['Payload'] = OrderedDict()
for k in range(10, len(s)):
if k == 10:
message['Payload']['to_MLN'] = int(s[k], base=16)
if k == 11:
message['Payload']['Destination'] = self._dictsanitize(
const._mlgw_payloaddestdict, int(s[k], base=16))
if k == 12:
message['Payload']['Command'] = self._dictsanitize(
const.beo4_commanddict, int(s[k], base=16)).upper()
if k == 13:
message['Payload']['Sec-Source'] = self._dictsanitize(
const._mlgw_secsourcedict, int(s[k], base=16))
if k == 14:
message['Payload']['Link'] = self._dictsanitize(
const._mlgw_linkdict, int(s[k], base=16))
if k > 14:
message['Payload']['cmd' + str(k - 9)] = self._dictsanitize(
const.beo4_commanddict, int(s[k], base=16))
return message
def _decode_action(self, telegram, message):
message['Zone'] = telegram[0].upper()
message['Room'] = telegram[1].upper()
message['Type'] = telegram[2].upper()
message['Device'] = telegram[3]
message['State_Update'] = OrderedDict()
if message.get('Type') == 'BUTTON':
if telegram[4].split('=')[0] == '_SET STATE?STATE':
message['State_Update']['STATE'] = telegram[4].split('=')[1]
if message['State_Update'].get('STATE') == '0':
message['State_Update']['Status'] = "Off"
else:
message['State_Update']['Status'] = "On"
else:
message['State_Update']['STATE'] = telegram[4]
if message.get('Type') == 'DIMMER': # e.g. DownstairsHallwayDIMMERWall LightSTATE_UPDATE?LEVEL=5
if telegram[4].split('=')[0] == '_SET STATE?LEVEL':
message['State_Update']['LEVEL'] = telegram[4].split('=')[1]
if message['State_Update'].get('LEVEL') == '0':
message['State_Update']['Status'] = "Off"
else:
message['State_Update']['Status'] = "On"
else:
message['State_Update']['STATE'] = telegram[4]
return message
def _decode_command(self, telegram, message):
message['Zone'] = telegram[0].upper()
message['Room'] = telegram[1].upper()
message['Type'] = telegram[2].upper()
message['Device'] = telegram[3]
message['State_Update'] = OrderedDict()
if message.get('Type') == 'BUTTON':
if telegram[4].split('=')[0] == '_SET STATE?STATE':
message['State_Update']['STATE'] = telegram[4].split('=')[1]
if message['State_Update'].get('STATE') == '0':
message['State_Update']['Status'] = "Off"
else:
message['State_Update']['Status'] = "On"
else:
message['State_Update']['STATE'] = telegram[4]
if message.get('Type') == 'DIMMER':
if telegram[4].split('=')[0] == '_SET STATE?LEVEL':
message['State_Update']['LEVEL'] = telegram[4].split('=')[1]
if message['State_Update'].get('LEVEL') == '0':
message['State_Update']['Status'] = "Off"
else:
message['State_Update']['Status'] = "On"
else:
message['State_Update']['STATE'] = telegram[4]
return message
def _decode_trigger(self, telegram, message):
message['Zone'] = telegram[0].upper()
message['Room'] = telegram[1].upper()
message['Type'] = telegram[2].upper()
message['Device'] = telegram[3]
message['State_Update'] = OrderedDict()
if message.get('Type') == 'BUTTON':
if telegram[4].split('=')[0] == 'STATE_UPDATE?STATE':
message['State_Update']['STATE'] = telegram[4].split('=')[1]
if message['State_Update'].get('STATE') == '0':
message['State_Update']['Status'] = "Off"
else:
message['State_Update']['Status'] = "On"
else:
message['State_Update']['STATE'] = telegram[4]
if message.get('Type') == 'DIMMER':
if telegram[4].split('=')[0] == 'STATE_UPDATE?LEVEL':
message['State_Update']['LEVEL'] = telegram[4].split('=')[1]
if message['State_Update'].get('LEVEL') == '0':
message['State_Update']['Status'] = "Off"
else:
message['State_Update']['Status'] = "On"
else:
message['State_Update']['STATE'] = telegram[4]
if message.get('Type') == 'AV RENDERER':
if telegram[4][:5] == 'Light':
state = telegram[4][6:].split('&')
message['State_Update']['type'] = 'Light Command'
for s in state:
message['State_Update'][s.split('=')[0].lower()] = s.split('=')[1].title()
if message['State_Update'].get('command') == ' Cmd':
message['State_Update']['command'] = self._dictsanitize(const.beo4_commanddict,
int(s[13:].strip())).title()
elif telegram[4][:7] == 'Control':
state = telegram[4][6:].split('&')
message['State_Update']['type'] = 'Control Command'
for s in state:
message['State_Update'][s.split('=')[0].lower()] = s.split('=')[1]
if message['State_Update'].get('command') == ' cmd':
message['State_Update']['command'] = self._dictsanitize(const.beo4_commanddict,
int(s[13:].strip())).title()
elif telegram[4] == 'All standby':
message['State_Update']['command'] = telegram[4]
else:
state = telegram[4][13:].split('&')
for s in state:
if 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]
elif s.split('=')[0] == 'nowPlayingDetails':
message['State_Update']['nowPlayingDetails'] = OrderedDict()
details = s.split('=')[1].split(';')
if len(details) > 1:
for d in details:
if d.split(':')[0].strip() in ['track number', 'channel number']:
message['State_Update']['nowPlayingDetails']['channel_track'] \
= d.split(':')[1].strip()
else:
message['State_Update']['nowPlayingDetails'][d.split(':')[0].strip()] \
= d.split(':')[1].strip()
else:
message['State_Update'][s.split('=')[0]] = s.split('=')[1]
return message
def _report(self, header, telegram, message):
self.last_message = message
self.log.debug(self.name + "\n" + str(json.dumps(message, indent=4)))
if self.messageCallBack:
self.messageCallBack(self.name, ''.join(header).upper(), ''.join(telegram), message)
def client_connect(self):
self.log.info('Connecting to host at %s, port %i', self._host, self._port)
self.set_terminator(b'\r\n')
# Create the socket
try:
socket.setdefaulttimeout(3)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error, e:
self.log.info("Error creating socket: %s" % e)
self.handle_close()
# Now connect
try:
self.connect((self._host, self._port))
except socket.gaierror, e:
self.log.info("\tError with address %s:%i - %s" % (self._host, self._port, e))
self.handle_close()
except socket.error, e:
self.log.info("\tError opening connection to %s:%i - %s" % (self._host, self._port, e))
self.handle_close()
except socket.timeout, e:
self.log.info("\tSocket connection to %s:%i timed out- %s" % (self._host, self._port, e))
self.handle_close()
else:
self.is_connected = True
self.log.info("\tConnected to B&O Gateway")
def handle_connect(self):
self.log.info("\tAttempting to Authenticate...")
self._send_cmd(self._pwd)
self._send_cmd("MONITOR")
def handle_close(self):
self.log.info(self.name + ": Closing socket")
self.is_connected = False
self.close()
def _send_cmd(self,telegram):
try:
self.push(telegram +"\r\n")
except socket.error, e:
self.log.info("Error sending data: %s" % e)
self.handle_close()
except socket.timeout, e:
self.log.info("\tSocket connection to %s:%i timed out- %s" % (self._host, self._port, e))
self.handle_close()
else:
self.last_sent = telegram
self.last_sent_at = time.time()
self.log.info(self.name + " >>-SENT--> : " + telegram)
time.sleep(0.2)
def toggleEvents(self):
self._send_cmd('e')
def toggleMacros(self):
self._send_cmd('m')
def toggleCommands(self):
self._send_cmd('c')
def ping(self):
self._send_cmd('')
# ########################################################################################
# ##### Utility functions
def _hexbyte(self, byte):
resultstr = hex(byte)
if byte < 16:
resultstr = resultstr[:2] + "0" + resultstr[2]
return resultstr
def _dictsanitize(self, d, s):
result = d.get(s)
if result == None:
result = "UNKNOWN (type=" + self._hexbyte(s) + ")"
return str(result)
def _srcdictsanitize(self, d, s):
result = d.get(s)
if result == None:
result = s
return str(result)