Add files via upload
Branch from pre-release migrating to a fully featured Indigo Domotics plugin
BIN
BeoGateway.indigoPlugin.zip
Normal file
25
Info.plist
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?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.0">
|
||||
<dict>
|
||||
<key>PluginVersion</key>
|
||||
<string>1.0.0</string>
|
||||
<key>ServerApiVersion</key>
|
||||
<string>1.0.0</string>
|
||||
<key>IwsApiVersion</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>BeoGateway Plugin</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>uk.co.lukes_plugins.BeoGateway.plugin</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>https://github.com/LukeSpad/BeoGateway</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
220
ReadMe
BIN
Resources/Bang-Olufsen-Logo.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
Resources/BnO_Resources/BO_Background.png
Normal file
After Width: | Height: | Size: 127 KiB |
BIN
Resources/BnO_Resources/BO_Beolab4000.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
Resources/BnO_Resources/BO_Beolab6000.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
Resources/BnO_Resources/BO_Beomaster7000_2x2.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
Resources/BnO_Resources/BO_Beovision6.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
Resources/BnO_Resources/BO_Beovision7_32.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
Resources/BnO_Resources/BO_Beovision7_40.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
Resources/BnO_Resources/BO_Cam_DR.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
Resources/BnO_Resources/BO_Cam_Hall.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
Resources/BnO_Resources/BO_Cameras.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
Resources/BnO_Resources/BO_Device_Icons.png
Normal file
After Width: | Height: | Size: 163 KiB |
BIN
Resources/BnO_Resources/BO_Inset_Speaker.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
Resources/BnO_Resources/BO_Lights.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
Resources/BnO_Resources/BO_Logo.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
Resources/BnO_Resources/BO_Logo_Small.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
Resources/BnO_Resources/BO_Logo_Tiny.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
Resources/BnO_Resources/BO_Numeric.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
Resources/BnO_Resources/BO_Scenes.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
Resources/BnO_Resources/BO_Scenes_Long.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
Resources/BnO_Resources/BO_Speaker_Icons.png
Normal file
After Width: | Height: | Size: 168 KiB |
BIN
Resources/BnO_Resources/BO_Transport1.png
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
Resources/BnO_Resources/BO_Transport2.png
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
Resources/BnO_Resources/BO_Transport3.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
Resources/BnO_Resources/BO_Transport_Basic.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
Resources/BnO_Resources/BO_Transport_Basic_Long.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
Resources/BnO_Resources/Icons.png
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
Resources/BnO_Resources/Icons2.png
Normal file
After Width: | Height: | Size: 153 KiB |
BIN
Resources/Documentation/EN-BeoLink-handbook-v1-9.pdf
Normal file
BIN
Resources/Documentation/HIP_Protocol/driverDevelopmentGuide.pdf
Normal file
BIN
Resources/Documentation/Legacy_IR/MCL_service_technical-eng.pdf
Normal file
BIN
Resources/Documentation/Legacy_IR/infrared_and_homelink.pdf
Normal file
BIN
Resources/Documentation/MLGW_Protocol/MlgwProto0240.pdf
Normal file
BIN
Resources/Documentation/ML_NL_Converter/beolink_nlml.pdf
Normal file
BIN
Resources/Notification.png
Normal file
After Width: | Height: | Size: 39 KiB |
203
Server Plugin/Actions.xml
Normal 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&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>
|
157
Server Plugin/Devices.xml
Normal file
|
@ -0,0 +1,157 @@
|
|||
<?xml version="1.0"?>
|
||||
<Devices>
|
||||
<Device type="custom" id="BOGateway">
|
||||
<Name>B&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>
|
66
Server Plugin/Events.xml
Normal 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>
|
15
Server Plugin/MenuItems.xml
Normal 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>
|
93
Server Plugin/PluginConfig.xml
Normal file
|
@ -0,0 +1,93 @@
|
|||
<?xml version="1.2"?>
|
||||
<PluginConfig>
|
||||
<SupportURL>http://</SupportURL>
|
||||
<Field id="title" type="label" alignText="center" fontColor="blue">
|
||||
<Label>Bang & 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>
|
140
Server Plugin/Resources/ASBridge.py
Normal 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()
|
BIN
Server Plugin/Resources/ASBridge.pyc
Normal file
282
Server Plugin/Resources/BLHIP_CLIENT.py
Normal 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'
|
BIN
Server Plugin/Resources/BLHIP_CLIENT.pyc
Normal file
683
Server Plugin/Resources/CONSTANTS.py
Normal 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")
|
||||
]
|
||||
)
|
BIN
Server Plugin/Resources/CONSTANTS.pyc
Normal file
462
Server Plugin/Resources/MLCLI_CLIENT.py
Normal 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'
|
BIN
Server Plugin/Resources/MLCLI_CLIENT.pyc
Normal file
298
Server Plugin/Resources/MLCONFIG.py
Normal 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)
|
BIN
Server Plugin/Resources/MLCONFIG.pyc
Normal file
431
Server Plugin/Resources/MLGW_CLIENT.py
Normal 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'
|
BIN
Server Plugin/Resources/MLGW_CLIENT.pyc
Normal file
370
Server Plugin/Resources/MLtn_CLIENT.py
Normal 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)
|
BIN
Server Plugin/Resources/MLtn_CLIENT.pyc
Normal file
78
Server Plugin/Resources/Notify.app/Contents/Info.plist
Normal 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>
|
BIN
Server Plugin/Resources/Notify.app/Contents/MacOS/applet
Normal file
1
Server Plugin/Resources/Notify.app/Contents/PkgInfo
Normal file
|
@ -0,0 +1 @@
|
|||
APPLaplt
|
After Width: | Height: | Size: 388 B |
|
@ -0,0 +1,5 @@
|
|||
{\rtf1\ansi\ansicpg1252\cocoartf2636
|
||||
\cocoatextscaling0\cocoaplatform0{\fonttbl}
|
||||
{\colortbl;\red255\green255\blue255;}
|
||||
{\*\expandedcolortbl;;}
|
||||
}
|
BIN
Server Plugin/Resources/Scripts/blue.scpt
Normal file
BIN
Server Plugin/Resources/Scripts/green.scpt
Normal file
BIN
Server Plugin/Resources/Scripts/red.scpt
Normal file
BIN
Server Plugin/Resources/Scripts/yellow.scpt
Normal file
1
Server Plugin/Resources/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# init
|