Domoticz-WiZ-connected-plugin/plugin.py

368 wiersze
14 KiB
Python

# Domoticz WiZ connected Plugin
#
# Author: Syds Post sydspost@gmail.com
#
"""
<plugin key="wiz" name="WiZ connected" author="Syds Post" version="1.0.0" wikilink="" externallink="https://www.wizconnected.com/">
<description>
<h2>WiZ connected Plugin</h2><br/>
This plugin is meant to control WiZ connected devices. WiZ connected devices may come with different brands as Philips, TAO and WiZ connected etc.
<h3>Features</h3>
<ul style="list-style-type:square">
<li>Auto-detection of devices on network</li>
<li>On/Off control, state and available status display</li>
<li>Dimmer/Warm-cold setting for Lights</li>
<!-- <li>Scene activation support</li> -->
</ul>
<h3>Devices</h3>
<ul style="list-style-type:square">
<li>All devices that have on/off state should be supported</li>
</ul>
<h3>Configuration</h3>
Devices can be renamed in Domoticz or you can rename them in the App and remove them from Domoticz so they are detected with a new name or layout.
Be careful when setting the subnet, setting class A or class B networks can lead to performance issues because the plugin uses ARP ping
to detect the WiZ connected devices. Hostname Prefix is a comma seperated list of the first 4 characters of the hostname of the WiZ connected devices per brand.
</description>
<params>
<param field="Mode1" label="Hostname Prefix" width="200px" required="true" default="wiz_"/>
<param field="Mode2" label="Subnet" width="200px" required="true" default="192.168.2.0/24"/>
<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 threading
import socket
import html
import sys
import time
import math
import json
import re
from scapy.all import srp,Ether,ARP,conf
class BasePlugin:
startup = True;
devs = {}
last_update = 0
def __init__(self):
return
def onStart(self):
Domoticz.Log("WiZ connected plugin started")
if Parameters["Mode6"] != "0":
Domoticz.Debugging(int(Parameters["Mode6"]))
DumpConfigToLog()
# Find devices that already exist, create those that don't
conf.verb=0
ans,unans=srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst=Parameters["Mode2"]), timeout=2)
hostNames = []
for snd,rcv in ans:
ipAddress=rcv.sprintf(r"%ARP.psrc%")
try:
hostName = socket.gethostbyaddr(ipAddress)
except:
hostName = "Onbekend"
if hostName[0].startswith(Parameters["Mode1"]):
hostNames.append(hostName[0])
for hostName in hostNames:
Domoticz.Debug("Endpoint '"+hostName+"' found.")
deviceFound = False
for Device in Devices:
if ((hostName == Devices[Device].DeviceID)): deviceFound = True
if (deviceFound == False):
Domoticz.Device(Name=hostName, DeviceID=hostName, Unit=len(Devices)+1, Type=241, Subtype=8, Switchtype=7, Image=0).Create()
# Create/Start update thread
self.updateThread = threading.Thread(name="WiZUpdateThread", target=BasePlugin.handleThread, args=(self,))
self.updateThread.start()
def onStop(self):
Domoticz.Debug("onStop called")
while (threading.active_count() > 1):
time.sleep(1.0)
def onConnect(self, Connection, Status, Description):
Domoticz.Debug("onConnect called")
def onMessage(self, Connection, Data):
Domoticz.Debug("onMessage called")
def onCommand(self, Unit, Command, Level, Color):
Domoticz.Debug("onCommand called for Unit " + str(Unit) + ": Parameter '" + str(Command) + "', Level: " + str(Level))
# Find the device for the Domoticz Unit provided
dev = None
try:
dev = Devices[Unit]
except Exception as e:
Domoticz.Debug("Device has no ID " + str(Unit) + " " + str(e))
# If we didn't find it, leave (probably disconnected at this time)
if dev == None:
Domoticz.Error('Command for DeviceID='+Devices[Unit].DeviceID+' but device is not available.')
return
# if not dev.available():
# Domoticz.Error('Command for DeviceID='+Devices[Unit].DeviceID+' but device is offline.')
# return
Domoticz.Log('Sending command for DeviceID='+Devices[Unit].DeviceID)
host = str(Devices[Unit].DeviceID)
port = 38899
# Control device and update status in Domoticz
if Command == 'On':
mJSON = b'{"method":"setPilot","params":{"src":"udp","state":true}}'
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
sock.sendto(mJSON, (host, port))
received = sock.recv(1024).decode('utf-8')
finally:
sock.close()
received=str(received).split(",")[2].split(":")[2]
received=received[0:len(received)-3].capitalize()
UpdateDevice(Unit, 1, 'On', not received)
elif Command == 'Off':
mJSON = b'{"method":"setPilot","params":{"src":"udp","state":false}}'
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
sock.sendto(mJSON, (host, port))
received = sock.recv(1024).decode('utf-8')
finally:
sock.close()
received=str(received).split(",")[2].split(":")[2]
received=received[0:len(received)-3].capitalize()
UpdateDevice(Unit, 0, 'Off', not received)
elif Command == 'Set Color':
# Convert RGB to Cold- and White level
rgb = json.loads(Color)
mode = rgb.get("m")
cw = rgb.get("cw")
ww = rgb.get("ww")
mJSON = bytes('{"method":"setPilot","params":{"src":"udp","state":true,"c":' + str(cw) + ',"w":' + str(ww) + '}}', 'utf-8')
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
sock.sendto(mJSON, (host, port))
received = sock.recv(1024).decode('utf-8')
finally:
sock.close()
received=str(received).split(",")[2].split(":")[2]
received=received[0:len(received)-3].capitalize()
# Update status of Domoticz device
Devices[Unit].Update(nValue=1, sValue=str(Level), TimedOut=not received, Color=Color)
elif Command == 'Set Level':
# Set new level
mJSON = bytes('{"method":"setPilot","params":{"src":"udp","state":true,"dimming":' + str(Level) + '}}', 'utf-8')
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
sock.sendto(mJSON, (host, port))
received = sock.recv(1024).decode('utf-8')
finally:
sock.close()
received=str(received).split(",")[2].split(":")[2]
received=received[0:len(received)-3].capitalize()
# Update status of Domoticz device
UpdateDevice(Unit, 1 if Devices[Unit].Type == 241 else 2, str(Level), not received)
# Set last update
self.last_update = time.time()
def onNotification(self, Name, Subject, Text, Status, Priority, Sound, ImageFile):
Domoticz.Debug("Notification: " + Name + "," + Subject + "," + Text + "," + Status + "," + str(Priority) + "," + Sound + "," + ImageFile)
def onDisconnect(self, Connection):
Domoticz.Debug("onDisconnect called")
def onHeartbeat(self):
Domoticz.Debug("onHeartbeat called time="+str(time.time()))
# If it hasn't been at least 1 minute (corrected for ~2s runtime) since last update, skip it
if time.time() - self.last_update < 58:
return
# Create/Start update thread
self.updateThread = threading.Thread(name="WiZUpdateThread", target=BasePlugin.handleThread, args=(self,))
self.updateThread.start()
# Separate thread looping ever 10 seconds searching for new WiZ connected devices on network and updating their status
def handleThread(self):
try:
Domoticz.Debug("in handlethread")
# Initialize/Update WiZ devices
# Set last update
self.last_update = time.time()
# Update devices
conf.verb=0
ans,unans=srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst=Parameters["Mode2"]), timeout=2)
hostNames = []
for snd,rcv in ans:
ipAddress=rcv.sprintf(r"%ARP.psrc%")
try:
hostName = socket.gethostbyaddr(ipAddress)
except:
hostName = "Onbekend"
if hostName[0][0:3] in Parameters["Mode1"]:
hostNames.append(hostName[0])
for hostName in hostNames:
Domoticz.Debug("Endpoint '"+hostName+"' found.")
deviceFound = False
for Device in Devices:
if ((hostName == Devices[Device].DeviceID)): deviceFound = True
host = str(Devices[Device].DeviceID)
port = 38899
mJSON = b'{"method":"getPilot"}'
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
sock.sendto(mJSON, (host, port))
received = sock.recv(1024).decode('utf-8')
finally:
sock.close()
received=json.loads(received)
wizstate=received["result"]["state"]
if wizstate:
wizstate = 1
else:
wizstate = 0
wizlevel=str(received["result"]["dimming"])
if Devices[Device].Color != "":
c=json.loads(Devices[Device].Color)
try:
wizcw=received["result"]["c"]
wizww=received["result"]["w"]
c=json.loads(Devices[Device].Color)
c["cw"] = wizcw
c["ww"] = wizww
except:
wiztemp=received["result"]["temp"]
c["t"] = (wiztemp - 2700) / 14.9
wizcolor=json.dumps(c)
else:
wizcolor=""
# Update status of Domoticz device
Devices[Device].Update(nValue=wizstate, sValue=wizlevel, TimedOut=False, Color=wizcolor)
if (deviceFound == False):
Domoticz.Device(Name=hostName, DeviceID=hostName, Unit=len(Devices)+1, Type=241, Subtype=8, Switchtype=7, Image=0).Create()
except Exception as err:
Domoticz.Error("handleThread: "+str(err)+' line '+format(sys.exc_info()[-1].tb_lineno))
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, Color):
global _plugin
_plugin.onCommand(Unit, Command, Level, Color)
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 DumpConfigToLog():
for x in Parameters:
if Parameters[x] != "":
Domoticz.Debug( "'" + x + "':'" + str(Parameters[x]) + "'")
Domoticz.Debug("Device count: " + str(len(Devices)))
for x in Devices:
Domoticz.Debug("Device: " + str(x) + " - " + str(Devices[x]))
Domoticz.Debug("Device ID: '" + str(Devices[x].ID) + "'")
Domoticz.Debug("Device Name: '" + Devices[x].Name + "'")
Domoticz.Debug("Device nValue: " + str(Devices[x].nValue))
Domoticz.Debug("Device sValue: '" + Devices[x].sValue + "'")
Domoticz.Debug("Device LastLevel: " + str(Devices[x].LastLevel))
return
# Loop thru domoticz devices and see if there's a device with matching DeviceID, if so, return unit number, otherwise return zero
def getUnit(devid):
unit = 0
for x in Devices:
if Devices[x].DeviceID == devid:
unit = x
break
return unit
# Find the smallest unit number available to add a device in domoticz
def nextUnit():
unit = 1
while unit in Devices and unit < 255:
unit = unit + 1
return unit
def UpdateDevice(Unit, nValue, sValue, TimedOut):
# Make sure that the Domoticz device still exists (they can be deleted) before updating it
if (Unit in Devices):
#if (Devices[Unit].nValue != nValue) or (Devices[Unit].sValue != sValue) or (Devices[Unit].TimedOut != TimedOut):
Devices[Unit].Update(nValue=nValue, sValue=str(sValue), TimedOut=TimedOut)
Domoticz.Log("Update "+str(nValue)+":'"+str(sValue)+"' ("+Devices[Unit].Name+") TimedOut="+str(TimedOut))
return