I am trying to use WebSockets for the Harmony Hub remote, but got stuck on incoming messages. I know Logitech has revived the old api, but lets call my effort 'future proofing' or 'a learning experience'
The little proof of concept below will get the configuration from the hub and on every heartbeat the current status. The information is dumped to the log. The code is only usable to get some knowledge about the inner workings of the plugin framework and Harmony api. The hubid has to be set in the plugin configuration. No auto detection (yet).
If I run this 'plugin' both request work. The hub returns the configuration and the current status. I still have to figure out how to properly proces the incoming messages (found the earthquake example). However, the configuration gets broken up into separate messages. As far as I can tell it is broken up on a lower lever than the WebSockets protocol itself, because all following messages are missing the WebSockets header (it's only on the first message).
It would be great if websockers were to be implemented in the Domoticz framework! Thats way beyond my skills though.
Code: Select all
<plugin key="harmonytest" name="harmonytest" author="EScape" version="0.1" externallink="https://www.google.com">
<description>
<h2>Harmony testing</h2><br/>
Can i use the plugin?
</description>
<params>
<param field="Address" label="IP Address" width="200px" required="true" default="192.168.1.111"/>
<param field="Mode5" label="hubId" width="200px" required="true" default="0000"/>
<param field="Mode6" label="Debug" width="150px">
<options>
<option label="None" value="0" default="true" />
<option label="Python Only" value="2"/>
<option label="Basic Debugging" value="62"/>
<option label="Basic+Messages" value="126"/>
<option label="Connections Only" value="16"/>
<option label="Connections+Python" value="18"/>
<option label="Connections+Queue" value="144"/>
<option label="All" value="-1"/>
</options>
</param>
</params>
</plugin>
"""
import Domoticz
import json
from struct import Struct
import random
class BasePlugin:
apiConn = None
runAgain = 2
disconnectCount = 0
PACK_LEN1 = Struct('!BB').pack
PACK_LEN2 = Struct('!BBH').pack
PACK_LEN3 = Struct('!BBQ').pack
def __init__(self):
return
def send_frame(self, message, opcode=0x01):
#Adapted from https://gemfury.com/agriconnect/python:aiohttp/-/content/http_websocket.py
MSG_SIZE = 2 ** 14
rsv = 0
msg_length = len(message)
mask_bit = 0
if msg_length < 126:
header = self.PACK_LEN1(0x80 | rsv | opcode, msg_length | mask_bit)
elif msg_length < (1 << 16):
header = self.PACK_LEN2(0x80 | rsv | opcode, 126 | mask_bit, msg_length)
else:
header = self.PACK_LEN3(0x80 | rsv | opcode, 127 | mask_bit, msg_length)
if len(message) > MSG_SIZE:
self.apiConn.Send(header)
self.apiConn.Send(message)
else:
self.apiConn.Send(header + message)
# Description of frame
# HEADER, = struct.unpack("!H", Data[:2])
# wsData = Data[2:]
# FIN = (HEADER >> 15) & 0x01
# RSV1 = (HEADER >> 14) & 0x01
# RSV2 = (HEADER >> 13) & 0x01
# RSV3 = (HEADER >> 12) & 0x01
# OPCODE = (HEADER >> 8) & 0x0F
# MASKED = (HEADER >> 7) & 0x01
# LEN = (HEADER >> 0) & 0x7F
# Opcode:
# 0x00: this frame continues the payload from the last.
# 0x01: this frame includes utf-8 text data.
# 0x02: this frame includes binary data.
# 0x08: this frame terminates the connection.
# 0x09: this frame is a ping.
# 0x0a: this frame is a pong.
def request_config(self):
payload={}
#payload['hubId']=self.hub_id #Doesn't even need the hubid?
payload['timeout']=self.timeout
payload['hbus']={}
payload['hbus']['cmd']='vnd.logitech.harmony/vnd.logitech.harmony.engine?config'
payload['hbus']['id']='1'
payload['hbus']['params']='{"verb":"get","format":"json"}'
sendData=json.dumps(payload)
self.send_frame(sendData.encode('utf-8'))
Domoticz.Status('request config!')
def request_state(self):
payload={}
# payload['hubId']=self.hub_id
payload['timeout']=self.timeout
payload['hbus']={}
payload['hbus']['cmd']='vnd.logitech.connect/vnd.logitech.statedigest?get'
payload['hbus']['id']='22'
payload['hbus']['params']='{"verb":"get","format":"json"}'
payloadstr=b'{"hbus": {"cmd": "vnd.logitech.connect/vnd.logitech.statedigest?get", "params": "{\"verb\":\"get\",\"format\":\"json\"}", "id": "0"}, "timeout": 30}'
sendData=json.dumps(payload)
self.send_frame(sendData.encode('utf-8'))
Domoticz.Status('request state!')
return
def hex2bin(self,bytestr):
return "{0:0{1}b}".format(bytestr, 4)
def onStart(self):
runAgain = 2
self.timeout=30
self.Connected=False
self.Initialized=False
if Parameters["Mode6"] != "0":
Domoticz.Statusging(int(Parameters["Mode6"]))
DumpConfigToLog()
self.hub_ip=Parameters["Address"]
self.hub_id=Parameters["Mode5"]
self.apiConn = Domoticz.Connection(Name="HarmonySocks", Transport="TCP/IP", Protocol="None", Address=self.hub_ip, Port="8088")
self.apiConn.Connect()
Domoticz.Status("onStart finished")
def onStop(self):
Domoticz.Log("onStop - Plugin is stopping.")
self.apiConn.Disconnect()
def onConnect(self, Connection, Status, Description):
sendData='GET /?domain=svcs.myharmony.com&hubId='+self.hub_id+' HTTP/1.1\r\n' \
'Origin: http://'+self.hub_ip+':8088\r\n' \
'Connection: Upgrade\r\n' \
'Host: '+self.hub_ip+':8088\r\n' \
'Sec-WebSocket-Key: qcLfmlZLMw3jqHxKDHGV6A==\r\n' \
'Sec-WebSocket-Version: 13\r\n' \
'Upgrade: websocket\r\n\r\n' \
Domoticz.Status("onConnect called")
if (Status == 0):
Domoticz.Status("Hub connected successfully.")
Domoticz.Status(sendData)
Connection.Send(sendData)
else:
Domoticz.Log("Failed to connect ("+str(Status)+") to: "+self.hub_ip+":8088 with error: "+Description)
def onMessage(self, Connection, Data):
Domoticz.Status('Message!')
# Domoticz.Status('firstbyte received:' + str(Data[:1]))
Domoticz.Status(str(Data))
if not self.Initialized:
self.request_config()
self.Initialized=True
def onCommand(self, Unit, Command, Level, Hue):
Domoticz.Status("onCommand called for Unit " + str(Unit) + ": Parameter '" + str(Command) + "', Level: " + str(Level))
def onDisconnect(self, Connection):
Domoticz.Log("onDisconnect called for connection to: "+Connection.Address+":"+Connection.Port)
def onHeartbeat(self):
if (self.apiConn != None and (self.apiConn.Connecting() or self.apiConn.Connected())):
Domoticz.Status("onHeartbeat called, Connection is alive.")
self.request_state()
else:
self.runAgain = self.runAgain - 1
if self.runAgain <= 0:
if (self.apiConn == None):
self.apiConn = Domoticz.Connection(Name='HarmonySocks', Transport="TCP/IP", Protocol="None", Address=self.hub_ip, Port="8088")
self.apiConn.Connect()
self.runAgain = 2
else:
Domoticz.Status("onHeartbeat called, run again in "+str(self.runAgain)+" heartbeats.")
global _plugin
_plugin = BasePlugin()
def onStart():
global _plugin
_plugin.onStart()
def onStop():
global _plugin
_plugin.onStop()
def onConnect(Connection, Status, Description):
global _plugin
_plugin.onConnect(Connection, Status, Description)
def onMessage(Connection, Data):
global _plugin
_plugin.onMessage(Connection, Data)
def onCommand(Unit, Command, Level, Hue):
global _plugin
_plugin.onCommand(Unit, Command, Level, Hue)
def onNotification(Name, Subject, Text, Status, Priority, Sound, ImageFile):
global _plugin
_plugin.onNotification(Name, Subject, Text, Status, Priority, Sound, ImageFile)
def onDisconnect(Connection):
global _plugin
_plugin.onDisconnect(Connection)
def onHeartbeat():
global _plugin
_plugin.onHeartbeat()
# Generic helper functions
def LogMessage(Message):
if Parameters["Mode6"] == "File":
f = open(Parameters["HomeFolder"]+"http.html","w")
f.write(Message)
f.close()
Domoticz.Log("File written")
def DumpConfigToLog():
for x in Parameters:
if Parameters[x] != "":
Domoticz.Status( "'" + x + "':'" + str(Parameters[x]) + "'")
Domoticz.Status("Device count: " + str(len(Devices)))
for x in Devices:
Domoticz.Status("Device: " + str(x) + " - " + str(Devices[x]))
Domoticz.Status("Device ID: '" + str(Devices[x].ID) + "'")
Domoticz.Status("Device Name: '" + Devices[x].Name + "'")
Domoticz.Status("Device nValue: " + str(Devices[x].nValue))
Domoticz.Status("Device sValue: '" + Devices[x].sValue + "'")
Domoticz.Status("Device LastLevel: " + str(Devices[x].LastLevel))
return
def DumpHTTPResponseToLog(httpDict):
if isinstance(httpDict, dict):
Domoticz.Status("HTTP Details ("+str(len(httpDict))+"):")
for x in httpDict:
if isinstance(httpDict[x], dict):
Domoticz.Status("--->'"+x+" ("+str(len(httpDict[x]))+"):")
for y in httpDict[x]:
Domoticz.Status("------->'" + y + "':'" + str(httpDict[x][y]) + "'")
else:
Domoticz.Status("--->'" + x + "':'" + str(httpDict[x]) + "'")