Add files via upload

This commit is contained in:
LukeSpad 2022-02-05 20:38:57 +00:00 committed by GitHub
parent 3347edeb04
commit 73a79139ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 4562 additions and 0 deletions

View file

@ -0,0 +1,203 @@
<?xml version="1.0"?>
<Actions>
<SupportURL>http://</SupportURL>
<Action id="sendBeo4Src" deviceFilter="self.AVrenderer" uiPath="DeviceActions">
<Name>Send Beo4 Source Selection Command</Name>
<CallbackMethod>send_beo4_src</CallbackMethod>
<ConfigUI>
<Field type="menu" id="keyCode" defaultValue="0">
<Label>Command:</Label>
<List class="self" filter="" method="beo4sourcelistgenerator"/>
</Field>
<Field type="menu" id="destination" defaultValue="0">
<Label>Destination:</Label>
<List class="self" filter="" method="destinationlistgenerator"/>
</Field>
<Field type="menu" id="linkcmd" defaultValue="0">
<Label>Link:</Label>
<List>
<Option value="0">Local/Default Source</Option>
<Option value="1">Link Source (Remote/Option 4 Product)</Option>
</List>
</Field>
</ConfigUI>
</Action>
<Action id="sendBeo4Key" deviceFilter="self.AVrenderer" uiPath="DeviceActions">
<Name>Send Beo4 Key</Name>
<CallbackMethod>send_beo4_key</CallbackMethod>
<ConfigUI>
<Field type="menu" id="keyCode" defaultValue="0">
<Label>Command:</Label>
<List class="self" filter="" method="keylistgenerator2"/>
</Field>
<Field type="menu" id="destination" defaultValue="0">
<Label>Destination:</Label>
<List class="self" filter="" method="destinationlistgenerator"/>
</Field>
<Field type="menu" id="linkcmd" defaultValue="0">
<Label>Link:</Label>
<List>
<Option value="0">Local/Default Source</Option>
<Option value="1">Link Source (Remote/Option 4 Product)</Option>
</List>
</Field>
</ConfigUI>
</Action>
<Action id="sendBeoRemoteOneSrc" deviceFilter="self.AVrenderer" uiPath="DeviceActions">
<Name>Send BeoRemote One Source Selection Command</Name>
<CallbackMethod>send_br1_src</CallbackMethod>
<ConfigUI>
<Field type="menu" id="keyCode" defaultValue="0">
<Label>Source:</Label>
<List class="self" filter="" method="br1sourcelistgenerator"/>
</Field>
<Field type="menu" id="netBit" defaultValue="0">
<Label>Link:</Label>
<List>
<Option value="0">Local/Default Source</Option>
<Option value="1">Network Source (Link/Option 4 Product)</Option>
</List>
</Field>
</ConfigUI>
</Action>
<Action id="sendBeoRemoteOneKey" deviceFilter="self.AVrenderer" uiPath="DeviceActions">
<Name>Send BeoRemote One Key</Name>
<CallbackMethod>send_br1_key</CallbackMethod>
<ConfigUI>
<Field type="menu" id="keyCode" defaultValue="0">
<Label>Command:</Label>
<List class="self" filter="" method="keylistgenerator2"/>
</Field>
<Field type="menu" id="netBit" defaultValue="0">
<Label>Link:</Label>
<List>
<Option value="0">Local/Default Source</Option>
<Option value="1">Network Source (Link/Option 4 Product)</Option>
</List>
</Field>
</ConfigUI>
</Action>
<Action id="requestdevupdate" deviceFilter="self.AVrenderer" uiPath="DeviceActions">
<Name>Request Device State Update</Name>
<CallbackMethod>request_state_update</CallbackMethod>
</Action>
<Action id="sendHIPquery">
<Name>Request BLGW Home Integration Protocol State Updates</Name>
<CallbackMethod>send_hip_query</CallbackMethod>
<ConfigUI>
<Field type="menu" id="zone" defaultValue="0">
<Label>Zone:</Label>
<List class="self" filter="" method="zonelistgenerator"/>
</Field>
<Field type="menu" id="room" defaultValue="0">
<Label>Room:</Label>
<List class="self" filter="" method="roomlistgenerator2"/>
</Field>
<Field type="menu" id="devType" defaultValue="0">
<Label>Device Type:</Label>
<List class="self" filter="" method="hiptypelistgenerator"/>
</Field>
<Field type="textfield" id="deviceID" defaultValue="*">
<Label>Device Name:</Label>
</Field>
</ConfigUI>
</Action>
<Action id="sendHIPcmd">
<Name>Send BLGW Home Integration Protocol Command</Name>
<CallbackMethod>send_hip_cmd</CallbackMethod>
<ConfigUI>
<Field type="menu" id="zone" defaultValue="0">
<Label>Zone:</Label>
<List class="self" filter="" method="zonelistgenerator"/>
</Field>
<Field type="menu" id="room" defaultValue="0">
<Label>Room:</Label>
<List class="self" filter="" method="roomlistgenerator2"/>
</Field>
<Field type="menu" id="devType" defaultValue="0">
<Label>Device Type:</Label>
<List class="self" filter="" method="hiptypelistgenerator"/>
</Field>
<Field type="textfield" id="deviceID" defaultValue="*">
<Label>Device Name:</Label>
</Field>
<Field type="textfield" id="hip_cmd" defaultValue="*">
<Label>Command:</Label>
</Field>
<Field type="label" id="lab0" fontColor="blue">
<Label>Note you do not need to add the '/' prefix for your command: The plugin will add this for you!</Label>
</Field>
<Field id="simpleSeparator1" type="separator"/>
<Field type="label" id="lab1">
<Label>Common HIP Commands:</Label>
</Field>
<Field type="label" id="lab2">
<Label>Press a virtual button: PRESS</Label>
</Field>
<Field type="label" id="lab3">
<Label>Turn on a button: _STATE_UPDATE?STATE=1</Label>
</Field>
<Field type="label" id="lab4">
<Label>Set a dimmer: SET?LEVEL=60</Label>
</Field>
<Field type="label" id="lab5">
<Label>Send a Beo4 Command: Beo4 command?Command=TV&amp;Destination selector=Video_source</Label>
</Field>
<Field type="label" id="lab6">
<Label>Turn off all AV Renderers: All standby</Label>
</Field>
</ConfigUI>
</Action>
<Action id="sendHIPcmd2">
<Name>Send Free Text Home Integration Protocol Command</Name>
<CallbackMethod>send_hip_cmd2</CallbackMethod>
<ConfigUI>
<Field type="textfield" id="hip_cmd" defaultValue="*">
<Label>Command:</Label>
</Field>
</ConfigUI>
</Action>
<Action id="sendVirtualButton">
<Name>Send Virtual Button</Name>
<CallbackMethod>send_virtual_button</CallbackMethod>
<ConfigUI>
<Field type="textfield" id="buttonID" defaultValue="0">
<Label>Virtual Button Number:</Label>
</Field>
<Field type="menu" id="action" defaultValue="1">
<Label>Action:</Label>
<List>
<Option value="1">Press</Option>
<Option value="2">Hold</Option>
<Option value="3">Release</Option>
</List>
</Field>
</ConfigUI>
</Action>
<Action id="postNotification">
<Name>Post Message in Notification Centre</Name>
<CallbackMethod>post_notification</CallbackMethod>
<ConfigUI>
<Field type="textfield" id="title" defaultValue="BeoSystem Update">
<Label>Notification Title:</Label>
</Field>
<Field type="textfield" id="body" defaultValue="BeoSystem Update">
<Label>Body Text:</Label>
</Field>
</ConfigUI>
</Action>
<Action id="allStandby">
<Name>Send All Standby Command</Name>
<CallbackMethod>all_standby</CallbackMethod>
</Action>
</Actions>

View file

@ -0,0 +1,157 @@
<?xml version="1.0"?>
<Devices>
<Device type="custom" id="BOGateway">
<Name>B&amp;O Gateway (MLGW, BLGW)</Name>
<ConfigUI>
<Field type="textfield" id="address" defaultValue="1" hidden="True">
</Field>
<Field type="textfield" id="isBLGW" defaultValue="BLGW">
<Label>Gateway Type (MLGW/BLGW):</Label>
</Field>
<Field type="textfield" id="serial_no" defaultValue="NA">
<Label>Serial Number:</Label>
</Field>
<Field type="textfield" id="installer" defaultValue="NA">
<Label>Installer:</Label>
</Field>
<Field type="textfield" id="project" defaultValue="NA">
<Label>Project:</Label>
</Field>
<Field type="textfield" id="contact" defaultValue="NA">
<Label>Contact:</Label>
</Field>
</ConfigUI>
<States>
<State id="currentAudioSource">
<ValueType>String</ValueType>
<TriggerLabel>Audio Source Changed</TriggerLabel>
<ControlPageLabel>Current Audio Source is</ControlPageLabel>
</State>
<State id="currentAudioSourceName">
<ValueType>String</ValueType>
<TriggerLabel>Audio SourceName Changed</TriggerLabel>
<ControlPageLabel>Current Audio SourceName is</ControlPageLabel>
</State>
<State id="nowPlaying">
<ValueType>String</ValueType>
<TriggerLabel>Now Playing</TriggerLabel>
<ControlPageLabel>Now Playing</ControlPageLabel>
</State>
<State id="nAudioRenderers">
<ValueType>Integer</ValueType>
<TriggerLabel>Count of Active Audio Renderers</TriggerLabel>
<ControlPageLabel>Count of Active Audio Renderers</ControlPageLabel>
</State>
<State id="AudioRenderers">
<ValueType>String</ValueType>
<TriggerLabel>Names of Active Audio Renderers</TriggerLabel>
<ControlPageLabel>Names of Active Audio Renderers</ControlPageLabel>
</State>
<State id="nVideoRenderers">
<ValueType>Integer</ValueType>
<TriggerLabel>Count of Active Video Renderers</TriggerLabel>
<ControlPageLabel>Count of Active Video Renderers</ControlPageLabel>
</State>
<State id="VideoRenderers">
<ValueType>String</ValueType>
<TriggerLabel>Names of Active Video Renderers</TriggerLabel>
<ControlPageLabel>Names of Active Video Renderers</ControlPageLabel>
</State>
</States>
</Device>
<Device type="relay" id="AVrenderer">
<Name>AV renderer (Beovision, Beosound)</Name>
<ConfigUI>
<Field type="textfield" id="address" defaultValue="0">
<Label>Masterlink Node:</Label>
</Field>
<Field type="textfield" id="mlid" defaultValue="NA">
<Label>Masterlink ID:</Label>
</Field>
<Field type="textfield" id="serial_no" defaultValue="NA">
<Label>Serial Number:</Label>
</Field>
<Field type="textfield" id="zone" defaultValue="NA">
<Label>Zone:</Label>
</Field>
<Field type="textfield" id="room" defaultValue="NA">
<Label>Room:</Label>
</Field>
<Field type="textfield" id="roomnum" defaultValue="NA">
<Label>Room Number:</Label>
</Field>
</ConfigUI>
<States>
<State id="playState">
<ValueType>
<List>
<Option value="Unknown">Unknown</Option>
<Option value="None">None</Option>
<Option value="Stop">Stopped</Option>
<Option value="Play">Playing</Option>
<Option value="Wind">Wind</Option>
<Option value="Rewind">Rewind</Option>
<Option value="RecordLock">Record Lock</Option>
<Option value="Standby">Standby</Option>
<Option value="No_Media">No Media</Option>
<Option value="Still_Picture">Still Picture</Option>
<Option value="Scan_Forward">Scan Forwards</Option>
<Option value="Scan_Reverse">Scan Reverse</Option>
<Option value="Blank">Blank Status</Option>
</List>
</ValueType>
<TriggerLabel>Player Status Changed</TriggerLabel>
<TriggerLabelPrefix>Player Status is</TriggerLabelPrefix>
<ControlPageLabel>Current Player Status</ControlPageLabel>
<ControlPageLabelPrefix>Player Status is</ControlPageLabelPrefix>
</State>
<State id="mute">
<ValueType>Boolean</ValueType>
<TriggerLabel>Mute</TriggerLabel>
<ControlPageLabel>Mute</ControlPageLabel>
</State>
<State id="volume">
<ValueType>Integer</ValueType>
<TriggerLabel>Current Volume</TriggerLabel>
<ControlPageLabel>Current Volume</ControlPageLabel>
</State>
<State id="sep1">
<ValueType>Separator</ValueType>
</State>
<!-- SourceMediumPosition is the CD number representation for BeoSound 9000
<State id="sourceMediumPosition">
<ValueType>Integer</ValueType>
<TriggerLabel>Source Medium Position</TriggerLabel>
<ControlPageLabel>Source Medium Position</ControlPageLabel>
</State>
-->
<State id="channelTrack">
<ValueType>Integer</ValueType>
<TriggerLabel>Channel/Track</TriggerLabel>
<ControlPageLabel>Channel/Track</ControlPageLabel>
</State>
<State id="nowPlaying">
<ValueType>String</ValueType>
<TriggerLabel>Now Playing</TriggerLabel>
<ControlPageLabel>Now Playing</ControlPageLabel>
</State>
<State id="sep1">
<ValueType>Separator</ValueType>
</State>
<State id="source">
<ValueType>
<List>
<Option value="Unknown">Unknown</Option>
<Option value="Standby">Standby</Option>
</List>
</ValueType>
<TriggerLabel>Source Changed</TriggerLabel>
<TriggerLabelPrefix>Source is</TriggerLabelPrefix>
<ControlPageLabel>Current Source</ControlPageLabel>
<ControlPageLabelPrefix>Source is</ControlPageLabelPrefix>
</State>
</States>
<UiDisplayStateId>playState</UiDisplayStateId>
</Device>
</Devices>

View file

@ -0,0 +1,66 @@
<?xml version="1.0"?>
<Events>
<SupportURL>http://</SupportURL>
<Event id="allStandby">
<Name>All Standby</Name>
</Event>
<Event id="lightKey">
<Name>Light Command Received</Name>
<ConfigUI>
<Field type="menu" id="room" defaultValue="0">
<Label>Room:</Label>
<List class="self" filter="" method="roomlistgenerator"/>
</Field>
<Field type="menu" id="keyCode" defaultValue="0">
<Label>Command:</Label>
<List class="self" filter="" method="keylistgenerator"/>
</Field>
</ConfigUI>
</Event>
<Event id="controlKey">
<Name>Control Command Received</Name>
<ConfigUI>
<Field type="menu" id="room" defaultValue="0">
<Label>Room:</Label>
<List class="self" filter="" method="roomlistgenerator"/>
</Field>
<Field type="menu" id="keyCode" defaultValue="0">
<Label>Command:</Label>
<List class="self" filter="" method="keylistgenerator"/>
</Field>
</ConfigUI>
</Event>
<Event id="beo4Key">
<Name>BeoRemote Command Received</Name>
<ConfigUI>
<Field type="menu" id="sourceType" defaultValue="0">
<Label>Source Type:</Label>
<List class="self" filter="" method="beo4sourcelistgenerator2"/>
</Field>
<Field type="menu" id="keyCode" defaultValue="0">
<Label>Command:</Label>
<List class="self" filter="" method="keylistgenerator2"/>
</Field>
</ConfigUI>
</Event>
<Event id="virtualButton">
<Name>Virtual Button Pressed</Name>
<ConfigUI>
<Field type="textfield" id="buttonID" defaultValue="0">
<Label>Virtual Button Number:</Label>
</Field>
<Field type="menu" id="action" defaultValue="1">
<Label>Button Action:</Label>
<List>
<Option value="1">Press</Option>
<Option value="2">Hold</Option>
<Option value="3">Release</Option>
</List>
</Field>
</ConfigUI>
</Event>
</Events>

View file

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<MenuItems>
<MenuItem id="menu1">
<Name>Request Gateway Serial Number</Name>
<CallbackMethod>request_serial_number</CallbackMethod>
</MenuItem>
<MenuItem id="menu2">
<Name>Request Device State Update</Name>
<CallbackMethod>request_device_update</CallbackMethod>
</MenuItem>
<MenuItem id="menu3">
<Name>Reset Gateway Client Connections</Name>
<CallbackMethod>reset_clients</CallbackMethod>
</MenuItem>
</MenuItems>

View file

@ -0,0 +1,93 @@
<?xml version="1.2"?>
<PluginConfig>
<SupportURL>http://</SupportURL>
<Field id="title" type="label" alignText="center" fontColor="blue">
<Label>Bang &amp; Olufsen Gateway Configuration (MLGW/BLGW)</Label>
</Field>
<Field id="subtitle1" type="label" alignText="left" fontColor="blue">
<Label>Gateway Network Address:</Label>
</Field>
<Field id="address" type="textfield" defaultValue="blgw.local.">
<Label>IP Address:</Label>
<CallbackMethod>set_gateway</CallbackMethod>
</Field>
<Field id="mlgw_port" type="textfield" defaultValue="9000">
<Label>MLGW Protocol Port:</Label>
<CallbackMethod>set_gateway</CallbackMethod>
</Field>
<Field id="hip_port" type="textfield" defaultValue="9100">
<Label>Home Integration Protocol Port:</Label>
<CallbackMethod>set_gateway</CallbackMethod>
</Field>
<Field id="simpleSeparator1" type="separator"/>
<Field id="subtitle2" type="label" alignText="left" fontColor="blue">
<Label>Login Details:</Label>
</Field>
<Field id="userID" type="textfield" defaultValue="admin">
<Label>User ID:</Label>
<CallbackMethod>set_login</CallbackMethod>
</Field>
<Field id="password" type="textfield" defaultValue="password" secure="true">
<Label>Password:</Label>
<CallbackMethod>set_login</CallbackMethod>
</Field>
<Field id="simpleSeparator2" type="separator"/>
<Field id="subtitle3" type="label" alignText="left" fontColor="blue">
<Label>Music Control Options:</Label>
</Field>
<Field type="menu" id="defaultAudio" defaultValue="RADIO">
<Label>Default Audio Source:</Label>
<List>
<Option value="RADIO">RADIO</Option>
<Option value="CD">CD</Option>
<Option value="A.TAPE/A.MEM">A.TAPE/A.MEM</Option>
<Option value="PHONO/N.RADIO">PHONO/N.RADIO</Option>
<Option value="A.AUX">A.AUX</Option>
<Option value="A.TAPE2/N.MUSIC">A.TAPE2/N.MUSIC</Option>
</List>
<CallbackMethod>set_default_audio</CallbackMethod>
</Field>
<Field type="checkbox" id="iTunesControl" defaultValue="true">
<Label>Control Apple Music:</Label>
<CallbackMethod>set_music_control</CallbackMethod>
<Description>play and control Apple Music</Description>
</Field>
<Field type="menu" id="iTunesSource" defaultValue="A.TAPE2/N.MUSIC" visibleBindingId="iTunesControl"
visibleBindingValue="true">
<Label>Apple Music Source:</Label>
<List>
<Option value="RADIO">RADIO</Option>
<Option value="CD">CD</Option>
<Option value="A.TAPE/A.MEM">A.TAPE/A.MEM</Option>
<Option value="PHONO/N.RADIO">PHONO/N.RADIO</Option>
<Option value="A.AUX">A.AUX</Option>
<Option value="A.TAPE2/N.MUSIC">A.TAPE2/N.MUSIC</Option>
</List>
<CallbackMethod>set_music_control</CallbackMethod>
</Field>
<Field type="checkbox" id="trackMode" defaultValue="true" visibleBindingId="iTunesControl"
visibleBindingValue="true">
<Label>Track Reporting:</Label>
<CallbackMethod>set_trackmode</CallbackMethod>
<Description>prints track info to the Indigo log</Description>
</Field>
<Field id="simpleSeparator3" type="separator"/>
<Field id="subtitle4" type="label" alignText="left" fontColor="blue">
<Label>Plugin Reporting Options:</Label>
</Field>
<Field type="checkbox" id="verboseMode" defaultValue="true">
<Label>Verbose Mode:</Label>
<CallbackMethod>set_verbose</CallbackMethod>
<Description>prints device telegrams to the Indigo log</Description>
</Field>
<Field type="checkbox" id="notifyMode" defaultValue="true">
<Label>Post Notifications:</Label>
<CallbackMethod>set_notifymode</CallbackMethod>
<Description>posts information to the Notification Centre</Description>
</Field>
<Field type="checkbox" id="debugMode" defaultValue="false">
<Label>Debug Mode:</Label>
<CallbackMethod>set_debug</CallbackMethod>
<Description>prints debug info to the Indigo log</Description>
</Field>
</PluginConfig>

View file

@ -0,0 +1,140 @@
#!/usr/bin/python
import indigo
import logging
import os
import unicodedata
import threading
from Foundation import NSAppleScript
from ScriptingBridge import SBApplication
''' Module defining a MusicController class for Apple Music, enables:
basic transport control of the player,
query of player status,
playing existing playlists,
running external appleScripts for more complex control, and
reporting messages in the notification centre'''
PLAYSTATE = dict([
(1800426320, "Play"),
(1800426352, "Pause"),
(1800426323, "Stop"),
(1800426310, "Wind"),
(1800426322, "Rewind")
])
class MusicController(object):
def __init__(self):
self.app = SBApplication.applicationWithBundleIdentifier_("com.apple.Music")
# ########################################################################################
# Player information
def get_current_track_info(self):
name = self.app.currentTrack().name()
album = self.app.currentTrack().album()
artist = self.app.currentTrack().artist()
number = self.app.currentTrack().trackNumber()
if name:
# Deal with tracks with non-ascii characters such as accents
name = unicodedata.normalize('NFD', name).encode('ascii', 'ignore')
album = unicodedata.normalize('NFD', album).encode('ascii', 'ignore')
artist = unicodedata.normalize('NFD', artist).encode('ascii', 'ignore')
return [name, album, artist, number]
def get_current_play_state(self):
return PLAYSTATE.get(self.app.playerState())
def get_current_track_position(self):
return self.app.playerPosition()
# ########################################################################################
# Transport Controls
def playpause(self):
self.app.playpause()
def play(self):
if PLAYSTATE.get(self.app.playerState()) in ['Wind', 'Rewind']:
self.app.resume()
elif PLAYSTATE.get(self.app.playerState()) == 'Pause':
self.app.playpause()
elif PLAYSTATE.get(self.app.playerState()) == 'Stop':
self. app.setValue_forKey_('true', 'shuffleEnabled')
playlist = self.app.sources().objectWithName_("Library")
playlist.playOnce_(None)
def pause(self):
if PLAYSTATE.get(self.app.playerState()) == 'Play':
self.app.pause()
def stop(self):
if PLAYSTATE.get(self.app.playerState()) != 'Stop':
self.app.stop()
def next_track(self):
self.app.nextTrack()
def previous_track(self):
self.app.previousTrack()
def wind(self, time):
# self.app.wind()
# Native wind function can be a bit annoying
# I provide an alternative below that skips a set number of seconds forwards
self.set_current_track_position(time)
def rewind(self, time):
# self.app.rewind()
# Native rewind function can be a bit annoying
# I provide an alternative below that skips a set number of seconds back
self.set_current_track_position(time)
# ########################################################################################
# More complex playback control functions
def shuffle(self):
if self.app.shuffleEnabled():
self.app.setValue_forKey_('false', 'shuffleEnabled')
else:
self.app.setValue_forKey_('true', 'shuffleEnabled')
def set_current_track_position(self, time, mode='Relative'):
if mode == 'Relative':
# Set playback position in seconds relative to current position
self.app.setPlayerPosition_(self.app.playerPosition() + time)
elif mode == 'Absolute':
# Set playback position in seconds from the start of the track
self.app.setPlayerPosition_(time)
def play_playlist(self, playlist):
self.app.stop()
playlist = self.app.sources().objectWithName_("Library").playlists().objectWithName_(playlist)
playlist.playOnce_(None)
# ########################################################################################
# Accessory functions - threaded due to execution time
@staticmethod
def run_script(script, debug):
script = 'run script ("' + script + '" as POSIX file)'
if debug:
indigo.server.log(script, level=logging.DEBUG)
def applet(_script):
# Run an external applescript file
s = NSAppleScript.alloc().initWithSource_(_script)
s.executeAndReturnError_(None)
threading.Thread(target=applet, args=(script,)).start()
@staticmethod
def notify(message, subtitle):
def applet(body, title):
# Post message in notification center
path = os.path.dirname(os.path.abspath(__file__))
script = 'tell application "' + path + '/Notify.app" to notify("BeoGateway", "' + \
body + '", "' + title + '")'
s = NSAppleScript.alloc().initWithSource_(script)
s.executeAndReturnError_(None)
threading.Thread(target=applet, args=(message, subtitle,)).start()

View file

@ -0,0 +1,282 @@
import indigo
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
self._received_data += data
def found_terminator(self):
# indigo.server.log("Raw Data: " + self._received_data)
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")
self._received_data = urllib.unquote(self._received_data)
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:
indigo.server.log('Command Successfully Processed: ' + str(urllib.unquote(self._received_data)),
level=logging.DEBUG)
elif e_string[2:5] == 'CMD':
indigo.server.log('Wrong or Unrecognised Command: ' + str(urllib.unquote(self._received_data)),
level=logging.WARNING)
elif e_string[2:5] == 'SYN':
indigo.server.log('Bad Syntax, or Wrong Character Encoding: ' +
str(urllib.unquote(self._received_data)), level=logging.WARNING)
elif e_string[2:5] == 'ACC':
indigo.server.log('Zone Access Violation: ' + str(urllib.unquote(self._received_data)),
level=logging.WARNING)
elif e_string[2:5] == 'LEN':
indigo.server.log('Received Message Too Long: ' + str(urllib.unquote(self._received_data)),
level=logging.WARNING)
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)
except socket.error, e:
indigo.server.log("Error creating socket: " + str(e), level=logging.ERROR)
self.handle_close()
# Now connect
try:
self.connect((self._host, self._port))
except socket.gaierror, e:
indigo.server.log("\tError with address: " + str(e), level=logging.ERROR)
self.handle_close()
except socket.timeout, e:
indigo.server.log("\tSocket connection timed out: " + str(e), level=logging.ERROR)
self.handle_close()
except socket.error, e:
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()
def send_cmd(self, telegram):
try:
self.push(telegram.encode("ascii") + "\r\n")
except socket.timeout, e:
indigo.server.log("\tSocket connection timed out: " + str(e), level=logging.ERROR)
self.handle_close()
except socket.error, e:
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:
indigo.server.log(self.name + " >>-SENT--> : " + telegram, level=logging.DEBUG)
else:
indigo.server.log(self.name + " >>-SENT--> : " + telegram, level=logging.INFO)
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'

View file

@ -0,0 +1,683 @@
from collections import OrderedDict
# Constants for B&O telegram protocols
# ########################################################################################
# Config data (set on initialisation)
rooms = []
available_sources = []
standby_state = [
{'key': 'onOffState', 'value': False},
{'key': 'playState', 'value': 'Standby'},
{'key': 'source', 'value': 'Standby'},
{'key': 'nowPlaying', 'value': 'Unknown'},
{'key': 'channelTrack', 'value': 0},
{'key': 'mute', 'value': True},
{'key': 'volume', 'value': 0},
]
gw_all_stb = [
{'key': 'AudioRenderers', 'value': ''},
{'key': 'VideoRenderers', 'value': ''},
{'key': 'nAudioRenderers', 'value': 0},
{'key': 'nVideoRenderers', 'value': 0},
{'key': 'currentAudioSource', 'value': 'Unknown'},
{'key': 'currentAudioSourceName', 'value': 'Unknown'},
{'key': 'nowPlaying', 'value': 'Unknown'},
]
# ########################################################################################
# Source Types
source_type_dict = dict(
[
("Video Sources", ("TV", "V.AUX/DTV2", "MEDIA", "V.TAPE/V.MEM/DVD2", "DVD", "DVD2", "CAMERA",
"SAT/DTV", "PC", "WEB", "DOORCAM", "PHOTO", "USB2", "WEBMEDIA", "AV.IN",
"HOMEMEDIA", "DVB_RADIO", "DNLA", "RECORDINGS", "CAMERA", "USB", "DNLA-DMR", "YOUTUBE",
"HOME.APP", "HDMI_1", "HDMI_2", "HDMI_3", "HDMI_4", "HDMI_5", "HDMI_6",
"HDMI_7", "HDMI_8", "MATRIX_1", "MATRIX_2", "MATRIX_3", "MATRIX_4", "MATRIX_5",
"MATRIX_6", "MATRIX_7", "MATRIX_8", "MATRIX_9", "MATRIX_10", "MATRIX_11",
"MATRIX_12", "MATRIX_13", "MATRIX_14", "MATRIX_15", "MATRIX_16", "PERSONAL_1",
"PERSONAL_2", "PERSONAL_3", "PERSONAL_4", "PERSONAL_5", "PERSONAL_6", "PERSONAL_7",
"PERSONAL_8")),
("Audio Sources", ("RADIO", "A.AUX", "A.TAPE/A.MEM", "CD", "PHONO/N.RADIO", "A.TAPE2/N.MUSIC",
"SERVER", "SPOTIFY", "CD2/JOIN", "TUNEIN", "DVB_RADIO", "LINE.IN", "BLUETOOTH",
"MUSIC", "AIRPLAY", "SPOTIFY", "DEEZER", "QPLAY"))
]
)
# ########################################################################################
# Beo4 Commands
beo4_srcdict = OrderedDict(
[
# Source selection:
(0x0C, "Standby"),
(0x47, "Sleep"),
(0x80, "TV"),
(0x81, "Radio"),
(0x82, "V.Aux/DTV2"),
(0x83, "A.Aux"),
(0x84, "Media"),
(0x85, "V.Tape/V.Mem"),
(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"),
]
)
beo4_commanddict = OrderedDict(
[
# Source selection:
(0x0C, "Standby"),
(0x47, "Sleep"),
(0x80, "TV"),
(0x81, "Radio"),
(0x82, "V.Aux/DTV2"),
(0x83, "A.Aux"),
(0x84, "Media"),
(0x85, "V.Tape/V.Mem"),
(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"),
(0x0F, "Function_1"),
(0x10, "Function_2"),
(0x11, "Function_3"),
(0x12, "Function_4"),
(0x19, "Function_5"),
(0x1A, "Function_6"),
(0x21, "Function_7"),
(0x22, "Function_8"),
(0x23, "Function_9"),
(0x24, "Function_10"),
(0x25, "Function_11"),
(0x26, "Function_12"),
(0x27, "Function_13"),
(0x39, "Function_14"),
(0x3A, "Function_15"),
(0x3B, "Function_16"),
(0x3C, "Function_17"),
(0x3D, "Function_18"),
(0x3E, "Function_19"),
(0x4B, "Function_20"),
(0x4C, "Function_21"),
(0x50, "Function_22"),
(0x51, "Function_23"),
(0x7D, "Function_24"),
(0xA5, "Function_25"),
(0xA6, "Function_26"),
(0xA9, "Function_27"),
(0xAA, "Function_28"),
(0xDD, "Function_29"),
(0xDE, "Function_30"),
(0xE0, "Function_31"),
(0xE1, "Function_32"),
(0xE2, "Function_33"),
(0xE6, "Function_34"),
(0xE7, "Function_35"),
(0xF2, "Function_36"),
(0xF3, "Function_37"),
(0xF4, "Function_38"),
(0xF5, "Function_39"),
(0xF6, "Function_40"),
# 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 = OrderedDict(
[
# Source, (Cmd, Unit)
("Standby", (0x0C, 0)),
("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)),
("A.TAPE2/N.MUSIC", (0x94, 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)),
]
)
beoremoteone_keydict = OrderedDict(
[
(0x0C, "Standby"),
# 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")
]
)
# ########################################################################################
# Source Activity
sourceactivitydict = OrderedDict(
[
(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"),
(0x45, "GOTO_SOURCE"),
# LOCKMANAGER_COMMAND: Lock to Determine what device issues source commands
# reference: https://tidsskrift.dk/daimipb/article/download/7043/6004/0
(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, "PICTURE_AND_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_LOCKmANAGER telegram and assumes responsibility
# for LOCKMANAGER_COMMAND telegrams until a key transfer occurs.
# reference: https://tidsskrift.dk/daimipb/article/download/7043/6004/0
(0x12, "KEY_LOST"), # ?
# Unknown command with payload of length 1.
# bit 0: unknown
# bit 1: unknown
(0xA0, "NEW_LOCKMANAGER"), # ?
# 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/SLAVE DEVICE"),
(0x81, "ALL AUDIO LINK DEVICES"),
(0x82, "ALL VIDEO LINK DEVICES"),
(0x83, "ALL LINK DEVICES"),
(0x80, "ALL"),
(0xF0, "MLGW"),
(0x29, "SYSTEM CONTROLLER/TIMER"),
# 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.
# reference: https://tidsskrift.dk/daimipb/article/download/7043/6004/0
(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, "SAT/DTV"),
(0x29, "DVD"),
(0x33, "V.AUX/DTV2"),
(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_sourcekind_dict = dict([(0x01, "audio source"), (0x02, "video source"), (0xFF, "undefined")])
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 = OrderedDict(
[
(0x00, "Video Source"),
(0x01, "Audio Source"),
(0x05, "Peripheral Video Source (V.TAPE/V.MEM/DVD)"),
(0x06, "Secondary Peripheral Video Source (V.TAPE2/V.MEM2/DVD2)"),
(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, "2 channel stereo"),
(0x03, "Front surround"),
(0x04, "4 channel 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, "Cinema mode off"), (0x01, "Cinema mode 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"),
]
)
blgw_devtypes = OrderedDict(
[
("*", "All"),
("SYSTEM", "System"),
("AV renderer", "AV Renderer"),
("BUTTON", "Button"),
("Dimmer", "Dimmer"),
("GPIO", "GPIO"),
("Thermostat 1 setpoint", "Thermostat 1 setpoint"),
("Thermostat 2 setpoints", "Thermostat 2 setpoints")
]
)

View file

@ -0,0 +1,462 @@
import indigo
import asynchat
import socket
import time
import logging
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',
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._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._received_data += data
def found_terminator(self):
self.last_received = self._received_data
self.last_received_at = time.time()
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:
indigo.server.log("\tAuthenticated! Gateway type is " + telegram[0:4] + "\n", level=logging.DEBUG)
if telegram[0:4] != "MLGW":
self.isBLGW = True
# Process telegrams and return json data in human readable format
if self._i > self._header_lines:
if "---- Logging" in telegram:
# Pong telegram
header = telegram
payload = []
message = OrderedDict([('payload_type', 'Pong'), ('State_Update', dict([('CONNECTION', 'Online')]))])
self.is_connected = True
if self.messageCallBack:
self.messageCallBack(self.name, header, str(list(payload)), message)
else:
# ML protocol message detected
items = telegram.split()[1:]
if len(items):
telegram = bytearray()
for item in items:
try:
telegram.append(int(item[:-1], base=16))
except (ValueError, TypeError):
# abort if invalid character found
if self.debug:
indigo.server.log('Invalid character ' + str(item) + ' found in telegram: ' +
''.join(items) + '\nAborting!', level=logging.ERROR)
break
# Decode any telegram with a valid 9 byte header, excluding typy 0x14 (regular clock sync pings)
if len(telegram) >= 9:
# 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):
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)
except socket.error, e:
indigo.server.log("Error creating socket: " + str(e), level=logging.ERROR)
self.handle_close()
# Now connect
try:
self.connect((self._host, self._port))
except socket.gaierror, e:
indigo.server.log("\tError with address: " + str(e), level=logging.ERROR)
self.handle_close()
except socket.timeout, e:
indigo.server.log("\tSocket connection timed out: " + str(e), level=logging.ERROR)
self.handle_close()
except socket.error, e:
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._pwd)
self.send_cmd("_MLLOG ONLINE")
def handle_close(self):
indigo.server.log(self.name + ": Closing socket", level=logging.ERROR)
self.is_connected = False
self.close()
def send_cmd(self, telegram):
try:
self.push(telegram + "\r\n")
except socket.timeout, e:
indigo.server.log("\tSocket connection timed out: " + str(e), level=logging.ERROR)
self.handle_close()
except socket.error, e:
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()
indigo.server.log(self.name + " >>-SENT--> : " + telegram, level=logging.INFO)
time.sleep(0.2)
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 ping(self):
if self.debug:
indigo.server.log(self.name + " >>-SENT--> : Ping", level=logging.DEBUG)
self.push('\n')
# ########################################################################################
# ##### Utility functions
@staticmethod
def _hexbyte(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 is None:
result = self._hexbyte(s)
return str(result)
@staticmethod
def _get_type(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()
self._get_device_info(message, telegram)
if 'Device' not in message:
# If ML telegram has been matched to a Masterlink node in the devices list then the 'from_device'
# key is redundant - it will always be identical to the 'Device' key
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._get_device_name(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["State_Update"] = OrderedDict()
# RELEASE command signifies product standby
if message.get("payload_type") in ["RELEASE", "STANDBY"]:
message["State_Update"]["state"] = 'Standby'
# source status info
# TTFF__TYDSOS__PTLLPS SR____LS______SLSHTR__ACSTPI________________________TRTR______
if message.get("payload_type") == "STATUS_INFO":
message["State_Update"]["nowPlaying"] = 'Unknown'
if telegram[8] < 27:
c_trk = telegram[19]
else:
c_trk = telegram[36] * 256 + telegram[37]
message["State_Update"]["nowPlayingDetails"] = OrderedDict(
[
("local_source", telegram[13]),
("type", self._dictsanitize(CONST.ml_sourcekind_dict, telegram[22])),
("channel_track", c_trk),
("source_medium_position", self._hexword(telegram[18], telegram[17])),
("picture_format", self._dictsanitize(CONST.ml_pictureformatdict, telegram[23]))
]
)
source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[10])
self._get_source_name(source, message)
message["State_Update"]["source"] = source
message["State_Update"]["sourceID"] = telegram[10]
self._get_channel_track(message)
message["State_Update"]["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["State_Update"]["display_source"] = _s.rstrip()
# extended source information
if message.get("payload_type") == "EXTENDED_SOURCE_INFORMATION":
message["State_Update"]["info_type"] = telegram[10]
_s = ""
for i in range(0, telegram[8] - 14):
_s = _s + chr(telegram[i + 24])
message["State_Update"]["info_value"] = _s
# beo4 command
if message.get("payload_type") == "BEO4_KEY":
source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[10])
self._get_source_name(source, message)
message["State_Update"] = OrderedDict(
[
("source", source),
("sourceID", telegram[10]),
("source_type", self._get_type(CONST.ml_selectedsource_type_dict, telegram[10])),
("command", self._dictsanitize(CONST.beo4_commanddict, telegram[11]))
]
)
# audio track info long
if message.get("payload_type") == "TRACK_INFO_LONG":
message["State_Update"]["nowPlaying"] = 'Unknown'
message["State_Update"]["nowPlayingDetails"] = OrderedDict(
[
("type", self._get_type(CONST.ml_selectedsource_type_dict, telegram[11])),
("channel_track", telegram[12]),
]
)
source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[11])
self._get_source_name(source, message)
message["State_Update"]["source"] = source
message["State_Update"]["sourceID"] = telegram[11]
self._get_channel_track(message)
message["State_Update"]["state"] = self._dictsanitize(CONST.sourceactivitydict, telegram[13])
# video track info
if message.get("payload_type") == "VIDEO_TRACK_INFO":
message["State_Update"]["nowPlaying"] = 'Unknown'
message["State_Update"]["nowPlayingDetails"] = OrderedDict(
[
("source_type", self._get_type(CONST.ml_selectedsource_type_dict, telegram[13])),
("channel_track", telegram[11] * 256 + telegram[12])
]
)
source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[13])
self._get_source_name(source, message)
message["State_Update"]["source"] = source
message["State_Update"]["sourceID"] = telegram[13]
self._get_channel_track(message)
message["State_Update"]["state"] = self._dictsanitize(CONST.sourceactivitydict, telegram[14])
# track change info
if message.get("payload_type") == "TRACK_INFO":
message["State_Update"]["subtype"] = self._dictsanitize(CONST.ml_trackinfo_subtype_dict, telegram[9])
# Change source
if message["State_Update"].get("subtype") == "Change Source":
message["State_Update"]["prev_source"] = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[11])
message["State_Update"]["prev_sourceID"] = telegram[11]
message["State_Update"]["prev_source_type"] = self._get_type(
CONST.ml_selectedsource_type_dict, telegram[11])
if len(telegram) > 18:
source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[22])
self._get_source_name(source, message)
message["State_Update"]["source"] = source
message["State_Update"]["sourceID"] = telegram[22]
# Current Source
if message["State_Update"].get("subtype") == "Current Source":
source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[11])
self._get_source_name(source, message)
message["State_Update"]["source"] = source
message["State_Update"]["sourceID"] = telegram[11]
message["State_Update"]["source_type"] = self._get_type(CONST.ml_selectedsource_type_dict, telegram[11])
message["State_Update"]["state"] = 'Unknown'
else:
message["State_Update"]["subtype"] = "Undefined: " + self._hexbyte(telegram[9])
# goto source
if message.get("payload_type") == "GOTO_SOURCE":
message["State_Update"]["nowPlaying"] = 'Unknown'
message["State_Update"]["nowPlayingDetails"] = OrderedDict(
[
("source_type", self._get_type(CONST.ml_selectedsource_type_dict, telegram[11])),
("channel_track", telegram[12])
]
)
source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[11])
self._get_source_name(source, message)
message["State_Update"]["source"] = source
message["State_Update"]["sourceID"] = telegram[11]
if telegram[12] not in [0, 255]:
self._get_channel_track(message)
# Device sending goto source command is playing
message["State_Update"]["state"] = 'Play'
# remote request
if message.get("payload_type") == "MLGW_REMOTE_BEO4":
message["State_Update"]["command"] = self._dictsanitize(CONST.beo4_commanddict, telegram[14])
message["State_Update"]["destination"] = self._dictsanitize(CONST.destselectordict, telegram[11])
# request_key
if message.get("payload_type") == "LOCK_MANAGER_COMMAND":
message["State_Update"]["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["State_Update"]["subtype"] = self._dictsanitize(CONST.ml_activity_dict, telegram[9])
if message["State_Update"].get('subtype') == "Source Active":
source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[13])
self._get_source_name(source, message)
message["State_Update"]["source"] = source
message["State_Update"]["sourceID"] = telegram[13]
message["State_Update"]["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["State_Update"]["subtype"] = self._dictsanitize(CONST.ml_activity_dict, telegram[9])
if message["State_Update"].get('subtype') == "Source Active":
source = self._dictsanitize(CONST.ml_selectedsourcedict, telegram[11])
self._get_source_name(source, message)
message["State_Update"]["source"] = source
message["State_Update"]["sourceID"] = telegram[11]
message["State_Update"]["source_type"] = self._get_type(CONST.ml_selectedsource_type_dict, telegram[11])
# request local audio source
if message.get("payload_type") == "PICTURE_AND_SOUND_STATUS":
message["State_Update"]["sound_status"] = OrderedDict(
[
("mute_status", self._dictsanitize(CONST.mlgw_soundstatusdict, telegram[10])),
("speaker_mode", self._dictsanitize(CONST.mlgw_speakermodedict, telegram[11])),
# ("stereo_mode", self._dictsanitize(CONST.mlgw_stereoindicatordict, telegram[9]))
]
)
# message["State_Update"]["picture_status"] = OrderedDict()
message['State_Update']['source'] = 'Unknown'
message['State_Update']['sourceName'] = 'Unknown'
message["State_Update"]["state"] = 'Unknown'
message["volume"] = int(telegram[12])
return message
@staticmethod
def _get_device_info(message, telegram):
# Loop over the device list
for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
# Get properties
node_props = node.pluginProps
# Skip netlink devices with no ml_id
if node_props['mlid'] == 'NA':
continue
# identify if the mlid is a number or a text string
try:
ml_id = int(node_props['mlid'], base=16)
except ValueError:
# If it is a text mlid then loop over the dictionary and get the numeric key
for item in CONST.ml_device_dict.items():
if item[1] == node_props['mlid']:
ml_id = int(item[0])
if ml_id == int(telegram[1]): # Match ML_ID
try:
message["Zone"] = node_props['zone'].upper()
except KeyError:
pass
message["Room"] = node_props['room'].upper()
message["Type"] = "AV RENDERER"
message["Device"] = node.name
break
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'
@staticmethod
def _get_device_name(dev):
for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
# Get properties
node_props = node.pluginProps
if node_props['mlid'] == dev:
return node.name
return dev
@staticmethod
def _get_source_name(source, message):
if CONST.available_sources:
for src in CONST.available_sources:
if str(src[0]) == str(source):
message["State_Update"]["sourceName"] = src[1]
return
# If source list exhausted then return Unknown
message["State_Update"]["sourceName"] = 'Unknown'

View file

@ -0,0 +1,298 @@
import indigo
import asyncore
import json
import requests
import logging
from requests.auth import HTTPDigestAuth, HTTPBasicAuth
from collections import OrderedDict
import Resources.CONSTANTS as CONST
class MLConfig:
def __init__(self, host_address='blgw.local', user='admin', pwd='admin', debug=False):
self.debug =debug
self._host = host_address
self._user = user
self._pwd = pwd
self._download_data()
def _download_data(self):
try:
indigo.server.log('Downloading configuration data from Gateway...', level=logging.WARNING)
url = 'http://' + self._host + '/mlgwpservices.json'
# try Basic Auth next (this is needed for the BLGW)
response = requests.get(url, auth=HTTPBasicAuth(self._user, self._pwd))
if response.status_code == 401:
# try Digest Auth first (this is needed for the MLGW)
response = requests.get(url, auth=HTTPDigestAuth(self._user, self._pwd))
if response.status_code == 401:
return
else:
# Once logged in successfully download and process the configuration data
configurationdata = json.loads(response.text)
self.configure_mlgw(configurationdata)
except ValueError:
pass
def configure_mlgw(self, data):
if "BeoGateway" not in indigo.devices.folders:
indigo.devices.folder.create("BeoGateway")
folder_id = indigo.devices.folders.getId("BeoGateway")
# Check to see if any devices already exist to avoid duplication
_nodes = []
for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
_nodes.append(int(node.address))
indigo.server.log('Processing Gateway configuration data...\n', level=logging.WARNING)
# Check to see if gateway device exists and create one if not
try:
gw = indigo.device.create(
protocol=indigo.kProtocol.Plugin,
name="Bang and Olufsen Gateway",
description="Automatically generated device for BeoGateway plugin:\n"
" - Please do not delete or rename!\n"
" - Editing device properties for advanced users only!",
deviceTypeId="BOGateway",
pluginId='uk.co.lukes_plugins.BeoGateway.plugin',
folder=folder_id,
address=1
)
except ValueError:
gw = indigo.devices['Bang and Olufsen Gateway']
try:
gateway_type = 'blgw'
gw.replacePluginPropsOnServer(
{
'serial_no': data['sn'],
'project': data['project'],
'installer': str(data['installer']['name']),
'contact': str(data['installer']['contact']),
'isBLGW': 'BLGW'
}
)
except KeyError:
gateway_type = 'mlgw'
gw.replacePluginPropsOnServer(
{
'serial_no': data['sn'],
'project': data['project'],
'isBLGW': 'MLGW'
}
)
# Replace States
gw.updateStatesOnServer(CONST.gw_all_stb)
# Loop over the config data to find the rooms, devices and sources in the installation
for zone in data["zones"]:
# Get rooms
if int(zone['number']) == 240:
continue
room = OrderedDict()
room['Room_Number'] = zone['number']
if gateway_type == 'blgw':
# BLGW arranges rooms within zones
room['Zone'] = str(zone['name']).split('/')[0]
room['Room_Name'] = str(zone['name']).split('/')[1]
elif gateway_type == 'mlgw':
# MLGW has no zoning concept - devices are arranged in rooms only
room['Room_Name'] = str(zone['name'])
# Get products
for product in zone["products"]:
device = OrderedDict()
# Device identification
device['Device'] = str(product["name"])
device['MLN'] = product["MLN"]
device['ML_ID'] = ''
try:
device['Serial_num'] = str(product["sn"])
except KeyError:
device['Serial_num'] = 'NA'
# Physical location
if gateway_type == 'blgw':
# BLGW arranges rooms within zones
device['Zone'] = str(zone['name']).split('/')[0]
device['Room'] = str(zone['name']).split('/')[1]
elif gateway_type == 'mlgw':
# MLGW has no zoning concept - devices are arranged in rooms only
device['Room'] = str(zone['name'])
device['Room_Number'] = str(zone["number"])
# Source information
device['Sources'] = dict()
for source in product["sources"]:
src_name = str(source["name"]).strip().replace(' ', '_')
device['Sources'][src_name] = dict()
for selectCmd in source["selectCmds"]:
if gateway_type == 'blgw':
# get source information from the BLGW config file
if str(source['sourceId']) == '':
source_id = self._srcdictsanitize(CONST.beo4_commanddict, source['selectID']).upper()
else:
source_id = str(source['sourceId'].split(':')[0])
source_id = self._srcdictsanitize(CONST.blgw_srcdict, source_id).upper()
device['Sources'][src_name]['source'] = source_id
device['Sources'][src_name]['uniqueID'] = str(source['sourceId'])
else:
# MLGW config file is structured differently
source_id = self._srcdictsanitize(CONST.beo4_commanddict, source['selectID']).upper()
device['Sources'][src_name]['source'] = source_id
source_tuple = (str(source_id), str(source["name"]))
device['Sources'][src_name]['BR1_cmd'] = \
dict([('command', int(selectCmd["cmd"])), ('unit', int(selectCmd["unit"]))])
# Establish the channel list for sources with favourites lists
if 'channels' in source:
device['Sources'][src_name]['channels'] = []
for channel in source['channels']:
c_num = 'c'
if gateway_type == 'blgw':
num = channel['selectSEQ'][::2]
else:
num = channel['selectSEQ'][:-1]
for n in num:
c_num += str(n)
c = (c_num, str(channel['name']))
device['Sources'][src_name]['channels'].append(c)
if source_tuple not in CONST.available_sources:
CONST.available_sources.append(source_tuple)
# Create indigo devices to represent the B&O AV renderers in the installation
if int(device['MLN']) not in _nodes:
if self.debug:
indigo.server.log("New Device! Creating Indigo Device " + device['Device'],
level=logging.DEBUG)
node = indigo.device.create(
protocol=indigo.kProtocol.Plugin,
name=device['Device'],
description="Automatically generated device for BeoGateway plugin:\n"
"- Device data sourced from gateway config:\n"
"- Please do not delete or rename!\n"
"- Editing device properties for advanced users only!",
deviceTypeId="AVrenderer",
pluginId='uk.co.lukes_plugins.BeoGateway.plugin',
folder=folder_id,
address=device['MLN'],
props={
'serial_no': device['Serial_num'],
'mlid': 'NA',
'zone': room['Zone'],
'room': device['Room'],
'roomnum': device['Room_Number'],
'sources': device['Sources']
}
)
# Update the device states
node.updateStatesOnServer(CONST.standby_state)
node.updateStateImageOnServer(indigo.kStateImageSel.PowerOff)
else:
# if node already exists, update the properties in case they have been updated
for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
if int(node.address) == int(device['MLN']):
if self.debug:
indigo.server.log("Old Device! Updating Properties for " + device['Device'] + "\n",
level=logging.DEBUG)
# Update the name of the device
node.name = device['Device']
node.description = "Automatically generated device for BeoGateway plugin:\n" \
" - Device data sourced from gateway config:\n" \
" - Please do not delete or rename!\n" \
" - Editing device properties for advanced users only!"
node.replaceOnServer()
# Update the properties of the device
node_props = node.pluginProps
node_props.update(
{
'serial_no': device['Serial_num'],
'zone': room['Zone'],
'room': device['Room'],
'roomnum': device['Room_Number'],
'sources': device['Sources']
}
)
node.replacePluginPropsOnServer(node_props)
# Update the device states
node.stateListOrDisplayStateIdChanged()
node.updateStatesOnServer(CONST.standby_state)
node.updateStateImageOnServer(indigo.kStateImageSel.PowerOff)
indigo.device.moveToFolder(node.id, value=folder_id)
break
# Keep track of the room data
CONST.rooms.append(room)
# Report details of the configuration
n_devices = indigo.devices.len(filter="uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer") - 1
indigo.server.log('Found ' + str(n_devices) + ' AV Renderers!', level=logging.DEBUG)
for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
indigo.server.log('\tMLN ' + str(node.address) + ': ' + str(node.name), level=logging.INFO)
indigo.server.log('\tFound ' + str(len(CONST.available_sources)) + ' Available Sources [Type, Name]:',
level=logging.DEBUG)
for i in range(len(CONST.available_sources)):
indigo.server.log('\t\t' + str(list(CONST.available_sources[i])), level=logging.INFO)
indigo.server.log('\tDone!\n', level=logging.DEBUG)
@staticmethod
def get_masterlink_id(mlgw, mlcli):
# Identify the MasterLink ID of products
indigo.server.log("Finding MasterLink ID of products:", level=logging.WARNING)
if mlgw.is_connected and mlcli.is_connected:
for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
indigo.server.log("Finding MasterLink ID of product " + node.name, level=logging.WARNING)
# Ping the device with a light timeout to elicit a ML telegram containing its ML_ID
mlgw.send_beo4_cmd(int(node.address),
CONST.CMDS_DEST.get("AUDIO SOURCE"),
CONST.BEO4_CMDS.get("LIGHT TIMEOUT"))
node_props = node.pluginProps
if node_props['serial_no'] in [None, 'NA', '']:
# If this is a MasterLink product it has no serial number...
# loop to until expected response received from ML Command Line Interface
test = True
while test:
try:
if mlcli.last_message['from_device'] == "MLGW" and \
mlcli.last_message['payload_type'] == "MLGW_REMOTE_BEO4" and \
mlcli.last_message['State_Update']['command'] == "Light Timeout":
if node_props['mlid'] == 'NA':
node_props['mlid'] = mlcli.last_message.get('to_device')
node_props['serial_no'] = 'NA'
node.replacePluginPropsOnServer(node_props)
indigo.server.log("\tMasterLink ID of product " +
node.name + " is " + node_props['mlid'] + ".\n",
level=logging.DEBUG)
test = False
except KeyError:
asyncore.loop(count=1, timeout=0.2)
else:
# If this is a NetLink product then it has a serial number and no ML_ID
indigo.server.log("\tNetworkLink ID of product " + node.name + " is " +
node_props['serial_no'] + ". No MasterLink ID assigned.\n",
level=logging.DEBUG)
# ########################################################################################
# Utility Functions
@staticmethod
def _srcdictsanitize(d, s):
result = d.get(s)
if result is None:
result = s
return str(result)

View file

@ -0,0 +1,431 @@
import indigo
import asynchat
import socket
import time
import logging
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',
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 = 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.BEORMT1_CMDS = CONST.beoremoteone_commanddict
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._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:
if self.debug:
indigo.server.log("Incomplete Telegram Received: " + str(list(self._received_data)) + " - Ignoring!\n",
level=logging.ERROR)
self._received_data = ""
def found_terminator(self, msg_type, payload):
self.last_received = str(list(self._received_data))
self.last_received_at = time.time()
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)
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["payload_type"] = payload_type
message["button"] = virtual_btn
message["action"] = virtual_action
elif payload_type == "Login status":
if payload == 0:
indigo.server.log("\tAuthentication Failed: Incorrect Password", level=logging.ERROR)
self.handle_close()
message['Connected'] = "False"
return
else:
indigo.server.log("\tLogin successful to " + self._host, level=logging.DEBUG)
self.is_connected = True
message["payload_type"] = payload_type
message['Connected'] = "True"
self.get_serial()
elif payload_type == "Pong":
self.is_connected = True
message = OrderedDict([('payload_type', 'Pong'), ('State_Update', dict([('CONNECTION', 'Online')]))])
elif payload_type == "Serial Number":
sn = ''
for c in payload:
sn += chr(c)
message["payload_type"] = payload_type
message['serial_Num'] = sn
elif payload_type == "Source Status":
self._get_device_info(message, payload)
message["payload_type"] = payload_type
message["MLN"] = payload[0]
message["State_Update"] = OrderedDict()
message["State_Update"]["nowPlaying"] = 'Unknown'
message["State_Update"]["nowPlayingDetails"] = OrderedDict(
[
("channel_track", self._hexword(payload[4], payload[5])),
("source_medium_position", self._hexword(payload[2], payload[3])),
("picture_format", self._getdictstr(CONST.ml_pictureformatdict, payload[7])),
]
)
source = self._getselectedsourcestr(payload[1]).upper()
self._get_source_name(source, message)
message["State_Update"]["source"] = source
self._get_channel_track(message)
message["State_Update"]["state"] = self._getdictstr(CONST.sourceactivitydict, payload[6])
elif payload_type == "Picture and Sound Status":
self._get_device_info(message, payload)
message["payload_type"] = payload_type
message["MLN"] = payload[0]
message["State_Update"] = OrderedDict()
message["State_Update"]["sound_status"] = OrderedDict(
[
("mute_status", self._getdictstr(CONST.mlgw_soundstatusdict, payload[1])),
("speaker_mode", self._getdictstr(CONST.mlgw_speakermodedict, payload[2])),
("stereo_mode", self._getdictstr(CONST.mlgw_stereoindicatordict, payload[9])),
]
)
message["State_Update"]["picture_status"] = OrderedDict(
[
("screen1_mute", self._getdictstr(CONST.mlgw_screenmutedict, payload[4])),
("screen1_active", self._getdictstr(CONST.mlgw_screenactivedict, payload[5])),
("screen2_mute", self._getdictstr(CONST.mlgw_screenmutedict, payload[6])),
("screen2_active", self._getdictstr(CONST.mlgw_screenactivedict, payload[7])),
("cinema_mode", self._getdictstr(CONST.mlgw_cinemamodedict, payload[8])),
]
)
message["State_Update"]["state"] = 'Unknown'
message["volume"] = int(payload[3])
elif payload_type == "All standby notification":
message["payload_type"] = payload_type
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]:
try:
message["Zone"] = room['Zone'].upper()
except KeyError:
pass
message["Room"] = room['Room_Name'].upper()
message["Type"] = self._getdictstr(CONST.mlgw_lctypedict, payload[1]).upper() + " COMMAND"
message["Device"] = 'Beo4/BeoRemote One'
message["payload_type"] = payload_type
message["room_number"] = str(payload[0])
message["command"] = self._getbeo4commandstr(payload[2])
if message != '':
self._report(header, 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)
except socket.error, e:
indigo.server.log("Error creating socket: " + str(e), level=logging.ERROR)
self.handle_close()
# Now connect
try:
self.connect((self._host, self._port))
except socket.gaierror, e:
indigo.server.log("\tError with address: " + str(e), level=logging.ERROR)
self.handle_close()
except socket.timeout, e:
indigo.server.log("\tSocket connection timed out: " + str(e), level=logging.ERROR)
self.handle_close()
except socket.error, e:
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):
login = []
for c in self._user:
login.append(c)
login.append(0x00)
for c in self._pwd:
login.append(c)
indigo.server.log("\tAttempting to Authenticate...", level=logging.WARNING)
self._send_cmd(CONST.MLGW_PL.get("LOGIN REQUEST"), login)
def handle_close(self):
indigo.server.log(self.name + ": Closing socket", level=logging.ERROR)
self.is_connected = False
self.close()
def _report(self, header, payload, message):
self.last_message = message
if self.messageCallBack:
self.messageCallBack(self.name, str(list(header)), str(list(payload)), message)
# ########################################################################################
# ##### mlgw send 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.timeout, e:
indigo.server.log("\tSocket connection to timed out: " + str(e), level=logging.ERROR)
self.handle_close()
except socket.error, e:
indigo.server.log("Error sending data: " + str(e), level=logging.ERROR)
self.handle_close()
else:
self.last_sent = str(bytearray(telegram))
self.last_sent_at = time.time()
if msg_type != CONST.MLGW_PL.get("PING"):
indigo.server.log(
self.name + " >>-SENT--> "
+ self._getpayloadtypestr(msg_type)
+ ": "
+ str(list(telegram)),
level=logging.INFO)
else:
if self.debug:
indigo.server.log(
self.name + " >>-SENT--> "
+ self._getpayloadtypestr(msg_type)
+ ": "
+ str(list(telegram)),
level=logging.DEBUG
)
# Sleep to allow msg to arrive
time.sleep(0.2)
# Ping the gateway
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 = [
mln, # byte[0] MLN
dest, # byte[1] Dest-Sel (0x00, 0x01, 0x05, 0x0f)
cmd, # byte[2] Beo4 Command
sec_source, # byte[3] Sec-Source
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 = [
mln, # byte[0] MLN
cmd, # byte[1] Beo4 Command
0x00, # byte[2] AV (needs to be 0)
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 = [
mln, # byte[0] MLN
cmd, # byte[1] Beoremote One Command
unit, # byte[2] Unit
0x00, # byte[3] AV (needs to be 0)
network_bit] # byte[4] Network_bit (0 = local source, 1 = network source)
self._send_cmd(CONST.MLGW_PL.get("BEOREMOTE ONE SOURCE SELECTION"), payload)
def send_virtualbutton(self, button, action):
payload = [
button, # byte[0] Button number
action] # byte[1] Action ID
self._send_cmd(CONST.MLGW_PL.get("MLGW VIRTUAL BUTTON EVENT"), payload)
# ########################################################################################
# ##### Utility functions
@staticmethod
def _hexbyte(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 is 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 is None:
result = "UNKNOWN (type = " + self._hexbyte(payloadtype) + ")"
return str(result)
def _getbeo4commandstr(self, command):
result = CONST.beo4_commanddict.get(command)
if result is None:
result = "CMD = " + self._hexbyte(command)
return result
def _getvirtualactionstr(self, action):
result = CONST.mlgw_virtualactiondict.get(action)
if result is None:
result = "Action = " + self._hexbyte(action)
return result
def _getselectedsourcestr(self, source):
result = CONST.ml_selectedsourcedict.get(source)
if result is None:
result = "SRC = " + self._hexbyte(source)
return result
def _getspeakermodestr(self, source):
result = CONST.mlgw_speakermodedict.get(source)
if result is None:
result = "mode = " + self._hexbyte(source)
return result
def _getdictstr(self, mydict, mykey):
result = mydict.get(mykey)
if result is None:
result = self._hexbyte(mykey)
return result
@staticmethod
def _get_source_name(source, message):
if CONST.available_sources:
for src in CONST.available_sources:
if src[1] == source:
message["State_Update"]["sourceName"] = src[0]
return
# If source list exhausted then return Unknown
message["State_Update"]["sourceName"] = 'Unknown'
@staticmethod
def _get_device_info(message, payload):
# Loop over the device list
for node in indigo.devices.iter('uk.co.lukes_plugins.BeoGateway.plugin.AVrenderer'):
# Get properties
node_props = node.pluginProps
if int(node.address) == int(payload[0]): # Match MLN
try:
message["Zone"] = node_props['zone'].upper()
except KeyError:
pass
message["Room"] = node_props['room'].upper()
message["Type"] = "AV RENDERER"
message["Device"] = node.name
break
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'

View file

@ -0,0 +1,370 @@
import indigo
import asynchat
import socket
import time
import logging
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',
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._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._received_data += data
def found_terminator(self):
self.last_received = self._received_data
self.last_received_at = time.time()
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:
if self.debug:
indigo.server.log("\t" + self._received_data, level=logging.DEBUG)
if self._received_data == 'incorrect password':
self.handle_close()
else:
try:
self._decode(items)
except IndexError:
pass
self._received_data = ""
def _decode(self, items):
header = items[3][:-1]
telegram_starts = len(''.join(items[:4])) + 4
telegram = self._received_data[telegram_starts:].replace('!', '').split('/')
message = OrderedDict()
if telegram[0] == 'Monitor events ( keys: M, E, C, (spc), Q ) ----':
self.toggle_commands()
self.toggle_macros()
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.destselectordict, 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
@staticmethod
def _decode_action(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
@staticmethod
def _decode_command(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
if self.messageCallBack:
self.messageCallBack(self.name, ''.join(header).upper(), ''.join(telegram), 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)
except socket.error, e:
indigo.server.log("Error creating socket: " + str(e), level=logging.ERROR)
self.handle_close()
# Now connect
try:
self.connect((self._host, self._port))
except socket.gaierror, e:
indigo.server.log("\tError with address: " + str(e), level=logging.ERROR)
self.handle_close()
except socket.timeout, e:
indigo.server.log("\tSocket connection timed out: " + str(e), level=logging.ERROR)
self.handle_close()
except socket.error, e:
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._pwd)
self._send_cmd("MONITOR")
def handle_close(self):
indigo.server.log(self.name + ": Closing socket", level=logging.ERROR)
self.is_connected = False
self.close()
def _send_cmd(self, telegram):
try:
self.push(telegram + "\r\n")
except socket.timeout, e:
indigo.server.log("\tSocket connection timed out: " + str(e), level=logging.ERROR)
self.handle_close()
except socket.error, e:
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()
indigo.server.log(self.name + " >>-SENT--> : " + telegram, level=logging.INFO)
time.sleep(0.2)
def toggle_events(self):
try:
self.push('e')
except socket.error, e:
indigo.server.log("Error sending data: " + str(e), level=logging.ERROR)
self.handle_close()
def toggle_macros(self):
try:
self.push('m')
except socket.error, e:
indigo.server.log("Error sending data: " + str(e), level=logging.ERROR)
self.handle_close()
def toggle_commands(self):
try:
self.push('c')
except socket.error, e:
indigo.server.log("Error sending data: " + str(e), level=logging.ERROR)
self.handle_close()
def ping(self):
self._send_cmd('')
# ########################################################################################
# ##### Utility functions
@staticmethod
def _hexbyte(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 is None:
result = "UNKNOWN (type=" + self._hexbyte(s) + ")"
return str(result)
@staticmethod
def _srcdictsanitize(d, s):
result = d.get(s)
if result is None:
result = s
return str(result)

View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>applet</string>
<key>CFBundleIconFile</key>
<string>applet</string>
<key>CFBundleIdentifier</key>
<string>com.apple.ScriptEditor.id.Notify</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Notify</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>aplt</string>
<key>LSMinimumSystemVersionByArchitecture</key>
<dict>
<key>x86_64</key>
<string>10.6</string>
</dict>
<key>LSRequiresCarbon</key>
<true/>
<key>NSAppleEventsUsageDescription</key>
<string>This script needs to control other applications to run.</string>
<key>NSAppleMusicUsageDescription</key>
<string>This script needs access to your music to run.</string>
<key>NSCalendarsUsageDescription</key>
<string>This script needs access to your calendars to run.</string>
<key>NSCameraUsageDescription</key>
<string>This script needs access to your camera to run.</string>
<key>NSContactsUsageDescription</key>
<string>This script needs access to your contacts to run.</string>
<key>NSHomeKitUsageDescription</key>
<string>This script needs access to your HomeKit Home to run.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This script needs access to your microphone to run.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This script needs access to your photos to run.</string>
<key>NSRemindersUsageDescription</key>
<string>This script needs access to your reminders to run.</string>
<key>NSSiriUsageDescription</key>
<string>This script needs access to Siri to run.</string>
<key>NSSystemAdministrationUsageDescription</key>
<string>This script needs access to administer this system to run.</string>
<key>OSAAppletShowStartupScreen</key>
<false/>
<key>OSAAppletStayOpen</key>
<true/>
<key>WindowState</key>
<dict>
<key>bundleDividerCollapsed</key>
<true/>
<key>bundlePositionOfDivider</key>
<real>0.0</real>
<key>dividerCollapsed</key>
<false/>
<key>eventLogLevel</key>
<integer>2</integer>
<key>name</key>
<string>ScriptWindowState</string>
<key>positionOfDivider</key>
<real>419</real>
<key>savedFrame</key>
<string>523 274 700 678 0 0 1680 1025 </string>
<key>selectedTab</key>
<string>description</string>
</dict>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

View file

@ -0,0 +1,5 @@
{\rtf1\ansi\ansicpg1252\cocoartf2636
\cocoatextscaling0\cocoaplatform0{\fonttbl}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
}

View file

@ -0,0 +1 @@
# init

File diff suppressed because it is too large Load diff