Plugins/Plugwise
Jump to navigation
Jump to search
This plugin communicates with the Plugwise Stretch 3.0 (End of Life!).
Unlike the plugwise2py, which communicates directly with the USB stick.
Features:
- Basic switch operations
- Logging op power consumption.
Installation
- Create a folder under the domoticz\plugins, called Plugwise. Another name will require a change in the python code.
- Create a file "plugin.py" in this folder, containing the code below (in plugin.txt, as the form does not allow .py files).3
- Restart domoticz
- Add hardware (Plugwise should be in the hardware list). Setup parameters....
- Add unused devices to the UI.
Devices should be added to the setup upon start of the plugin.
Note: In the plugin folder, a file is created 'devices.json'. This file contains the link between circle/sensor and unitid in domoticz. Do _not_ deleted (unused) devices in domoticz or this file if you do not know what you are doing as this will case a mismatch between plugwise/plugin/domoticz. Also, if debug is enabled, a log.txt file is created.
Reset
If you made a mess: delete the 'hardware', delete the devices.json and repeat from step 4.
Plugin
# Plugwise stretch 3.x plugin
#
# Author: Gerrit Hulleman
#
"""
<plugin key="PlugwiseStretch" name="Plugwise stretch 3.x plugin" author="Gerrit Hulleman" version="1.0.3" wikilink="http://www.domoticz.com/wiki/plugins/plugin.html" externallink="https://www.google.com/">
<description>
<h2>Plugwise stretch plugin</h2><br/>
Uses the stretch to communicate with circles to read consumption (not total power usage) and toggle the relay.
<h3>Features</h3>
<ul style="list-style-type:square">
<li>Toggle circle/stealth relays</li>
<li>Read circle/stealth power usage</li>
</ul>
<h3>Recent history</h3>
Version 1.0.0 - initial
Version 1.0.2 - added try/catch handling and dumping
Version 1.0.3 - removed modelname from code (not used)
</description>
<params>
<param field="Address" label="Stretch host" width="200px" required="true" default="stretch08126a.fritz.box"/>
<param field="Username" label="Stretch username" width="200px" required="true" default="stretch"/>
<param field="Password" label="Stretch password/id" width="200px" required="true" default="" password="true"/>
<param field="Mode1" label="Interval" width="200px" required="true" default="10"/>
<param field="Mode6" label="Debug" width="75px">
<options>
<option label="True" value="Debug"/>
<option label="False" value="Normal" default="true" />
</options>
</param>
</params>
</plugin>
"""
import json
import os #for file IO operations
import urllib
import urllib.request
import base64
import xml.etree.ElementTree as ET
import time
import Domoticz
class BasePlugin:
enabled = False
debugMode = True
devicesfile = 'plugins/Plugwise/devices.json'
stretchHost = 'stretch08126a.fritz.box'
stretchUsername = 'stretch'
stretchPassword = ''
heartbeat = 10
unitId = 0
serviceIdApplianceIdMap = {} #contains a serviceId -> applianceId key/value map so the module can be related to the appliance.
moduleIdDataMap = {} #contains module id (applianceId, mac address)
applianceIdDataMap = {} #Contains appliance data (name)
moduleDeviceKeyToUnit = {} #Contains the unique key (moduleId+sensor name) and Domoticz unitId (for update)
def __init__(self):
#self.var = 123
return
def onStart(self):
self.logMessage("Loading parameters")
#if 'Parameters' in locals(): # in case of a debug, do not load the parameters.
self.stretchHost = Parameters["Address"]
self.stretchPassword = Parameters["Username"]
self.stretchPassword = Parameters["Password"]
self.heartbeat = Parameters["Mode1"]
if Parameters["Mode6"] != "Debug":
Domoticz.Debugging(0)
self.debugMode = False;
self.logMessage("Loading appliances from stretch")
# A call is made to the stretch to load the appliances. An appliance is a name and one/more actions/sensors (like the basic: relay on/off and power consumptions).
# a modules is a single device (also switches / sensors) and the only link between appliances and modules are the id's of the sensors in it
data = self.callStretch('core/appliances')
root = ET.fromstring(data)
self.logMessage("Processing appliances")
for child in root.findall("./appliance"):
name = child.find('name').text
applianceId = child.attrib['id']
if self.debugMode:
self.logMessage("Appliance found: "+applianceId+' - '+name)
self.applianceIdDataMap[applianceId] = { 'name' : name }
# find all services (relay/power usage etc.) linked to the appliance. This is the link to the module later on
for service in child.findall("./services/*"):
serviceId = service.attrib['id']
self.serviceIdApplianceIdMap[serviceId] = applianceId
# All known appliance are stored in the serviceIdApplianceIdMap, so the modules can be linked to the appliance based on the services they share
# We might want to timeout the cached appliances, as new devices are not automatically added unles the plugin is restarted. Optimalisation
self.logMessage("Loading mapped devices from config file")
# Load mapped modules / devices from file
# this wil link services to existing devices, if any.
if os.path.exists(self.devicesfile):
with open(self.devicesfile) as json_file:
data = json.load(json_file)
for device in data['devices']:
if self.debugMode:
self.logMessage(json.dumps(device))
self.moduleDeviceKeyToUnit[device['devicekey']]= {'unit' : device['domoticzunit'],
'applianceId' : device['applianceid'],
'name': device['name']}
#print(p)
self.logMessage("{} devices loaded from configuration file".format(len(self.moduleDeviceKeyToUnit)))
if self.debugMode:
DumpConfigToLog()
# find all devices in domoticz to find the last-used-unitid. In case new devices are created
for x in Devices:
if (x > self.unitId):
self.unitId = x
if self.debugMode:
self.logMessage("Last unitId : "+str(self.unitId))
self.logMessage("Heartbeat : "+str(self.heartbeat))
Domoticz.Heartbeat(int(self.heartbeat))
self.logMessage("Plugin operational")
def onStop(self):
if self.debugMode:
self.logMessage("onStop called")
def onConnect(self, Connection, Status, Description):
if self.debugMode:
self.logMessage("onConnect called")
def onMessage(self, Connection, Data):
if self.debugMode:
self.logMessage("onMessage called")
def onCommand(self, Unit, Command, Level, Hue):
if self.debugMode:
self.logMessage("onCommand called for Unit " + str(Unit) + ": Parameter '" + str(Command) + "', Level: " + str(Level))
#2019-11-04 16:08:13.300 (testplugwise) onCommand called for Unit 14: Parameter 'Off', Level: 0
# TEST : only relay
switchstatus = Command.lower() # on/off
applianceId = self.getApplianceIdFromUnit(Unit)
if applianceId == "":
Domoticz.Error("Unable to find appliance for unit {}".format(Unit))
return
url= 'http://{host}/core/appliances;id={applianceId}/relay'.format(host=self.stretchHost, applianceId=applianceId)
postdata = "<relay><state>" + switchstatus + "</state></relay>"
if self.debugMode:
self.logMessage("Call to: "+url)
userAndPass = '{}:{}'.format(self.stretchUsername, self.stretchPassword)
userAndPassBase64 = base64.b64encode(userAndPass.encode('ascii')).decode('ascii')
#print (userAndPassBase64)
headers = {'Authorization': "Basic %s" % userAndPassBase64,
"Content-Type":"application/xml"}
data = '';
req = urllib.request.Request(url, postdata.encode('ascii'), headers)
try:
with urllib.request.urlopen(req) as response:
if response.getcode() != 202:
self.logMessage("Response on relay: {}".format(response.getcode()))
data = response.read()
except urllib.error.HTTPError as e:
# Return code error (e.g. 404, 501, ...)
# ...
self.logMessage('HTTPError: {}'.format(e.code))
except urllib.error.URLError as e:
# Not an HTTP-specific error (e.g. connection refused)
# ...
self.logMessage('URLError: {}'.format(e.reason))
return
def onNotification(self, Name, Subject, Text, Status, Priority, Sound, ImageFile):
if self.debugMode:
self.logMessage("Notification: " + Name + "," + Subject + "," + Text + "," + Status + "," + str(Priority) + "," + Sound + "," + ImageFile)
def onDisconnect(self, Connection):
return
def onHeartbeat(self):
if self.debugMode:
self.logMessage("onHeartbeat called")
data = self.callStretch('core/modules')
#if self.debugMode:
# self.logMessage("Modules data: " + str(data, 'utf-8'))
root = ET.fromstring(data)
for child in root.findall("./module"):
try:
moduleData = self.getModuleDataFromNode(child)
for service in child.findall("./services/*"):
name = service.tag
#get device. Unique id: moduleId / name
deviceKey = moduleData['moduleId']+'.'+name
deviceUnitId = self.getDeviceUnit(deviceKey)
#if (name == "electricity_interval_meter"): # power usage? - not used at this moment
# valDouble = 0
# for measurement in service.findall('./measurement'):
# multiplier = 1 #default consumed
# if measurement.attrib['directionality'] == 'produced':
# multiplier = -1
# valDouble += float(measurement.text) * multiplier
# print (moduleData['name'], "interval", name, valDouble)
if (name == "relay"): # actual relay for on/off purposes
relayMeasurement = service.find('measurement')
state = "On" if relayMeasurement.text == "on" else "Off"
logDateTime = relayMeasurement.attrib['log_date']
#self.logMessage(moduleData['name']+','+name+','+state+','+logDateTime+','+str(deviceUnitId))
if deviceUnitId == 0:
# create device
applianceName = moduleData['name'];
applianceId = moduleData['applianceId']
self.logMessage("Name, id: "+applianceName+','+applianceId)
deviceUnitId = self.createDevice(applianceName, "Switch", deviceKey, applianceId)
#self.logMessage(json.dumps(moduleData))
# the sub will check it the device needs updating
self.updateDevice(deviceUnitId, 1 if state == "On" else 0, state, False)
if (name == "electricity_point_meter"): #current voltage, check if update needed
valDouble = 0
logDateTime = ''
# this service has two measurements: produced and consumed. The logdate seemstime to be in sync
for measurement in service.findall('./measurement'):
multiplier = 1 #default consumed
if measurement.attrib['directionality'] == 'produced':
multiplier = -1
valDouble += float(measurement.text) * multiplier
logDateTime = measurement.attrib['log_date']
if (not name in moduleData) or (moduleData[name] != logDateTime): # check if the module has been updated (logDateTime stored in moduleData['electricity_point_meter'])
#print (moduleData['name'], "point", name, valDouble)
moduleData[name] = logDateTime
if deviceUnitId == 0:
# create device
deviceUnitId = self.createDevice(moduleData['name'], "kWh", deviceKey, moduleData['applianceId'])
self.updateDevice(deviceUnitId, 0, str(valDouble)+';0', True) # force update, even if the voltage has no changed.
except :
# exception on child, log
self.logMessage("Exception caught")
if self.debugMode:
self.logMessage(ET.tostring(child).decode('utf-8'))
def createDevice(self, Name, Type, DeviceKey, ApplianceId):
self.unitId += 1
self.logMessage("Create device: "+Name+","+Type+","+DeviceKey)
newDevice = Domoticz.Device(Name=Name, Unit=self.unitId, TypeName=Type)
newDevice.Create()
self.logMessage("Add to map: "+str(newDevice.Unit))
self.moduleDeviceKeyToUnit[DeviceKey]={'unit' : newDevice.Unit, 'applianceId': ApplianceId, 'name': Name}
# save data to config file, as onStop is not called on restart.
data = {}
devices = []
for keyVal in self.moduleDeviceKeyToUnit:
self.logMessage("Key found: "+keyVal)
devices.append({
'devicekey': keyVal,
'domoticzunit': self.moduleDeviceKeyToUnit[keyVal]['unit'],
'applianceid': self.moduleDeviceKeyToUnit[keyVal]['applianceId'],
'name': self.moduleDeviceKeyToUnit[keyVal]['name']
})
data['devices'] = devices
with open(self.devicesfile, 'w+') as outfile:
json.dump(data, outfile)
return newDevice.Unit
def callStretch(self, SubUrl):
url= 'http://{host}/{subUrl}'.format(host=self.stretchHost, subUrl=SubUrl)
userAndPass = '{}:{}'.format(self.stretchUsername, self.stretchPassword)
userAndPassBase64 = base64.b64encode(userAndPass.encode('ascii')).decode('ascii')
headers = {'Authorization': "Basic %s" % userAndPassBase64}
data = '';
req = urllib.request.Request(url, None, headers)
try:
with urllib.request.urlopen(req) as response:
if response.getcode() != 200:
self.logMessage("Response on call: {}".format(response.getcode()))
data = response.read()
except urllib.error.HTTPError as e:
# Return code error (e.g. 404, 501, ...)
self.logMessage('HTTPError: {}'.format(e.code))
except urllib.error.URLError as e:
# Not an HTTP-specific error (e.g. connection refused)
self.logMessage('URLError: {}'.format(e.reason))
return data
# return: {applianceId, moduleId, mac, name} for a given module
def getModuleDataFromNode(self, ModuleNode):
moduleId = ModuleNode.attrib['id']
# For this logic, a module is an appliance. The link between a module and appliance is the services they share, unique service id's
if (moduleId in self.moduleIdDataMap):
return self.moduleIdDataMap[moduleId];
applianceId = ''
# modelName = ModuleNode.find('vendor_model').attrib['model_name'] # name: scan,
for service in ModuleNode.findall("./services/*"):
serviceId = service.attrib['id']
if (serviceId in self.serviceIdApplianceIdMap):
applianceId = self.serviceIdApplianceIdMap[serviceId]
break
# Get addional information: MAC,appliance name
macNode = ModuleNode.find('./protocols/network_router/mac_address')
macAddress = ''
applianceName = '-not applicable-' # not all modules have an appliance. Such as scans / switches
if (macNode):
macAddress = macNode.text
if applianceId in self.applianceIdDataMap:
applianceName = self.applianceIdDataMap[applianceId]['name']
moduleData = { 'applianceId' : applianceId, 'moduleId' : moduleId, 'mac' : macAddress, 'name' : applianceName }
self.moduleIdDataMap[moduleId] = moduleData
#print ("Module:", moduleId, modelName, applianceName)
return moduleData
# return the unit (id from domoticz) for a given devicekey (combi of moduleid + type of service)
# return 0 if not found
def getDeviceUnit(self, DeviceKey):
if DeviceKey in self.moduleDeviceKeyToUnit:
return self.moduleDeviceKeyToUnit[DeviceKey]['unit']
return 0
# return thee applianceId for a given unit (id from domoticz)
# note: multiple domoticz devices can be the same module/appliance (plug is both a switch as a power monitor)
# note: not indexed, just enumerator through the moduledevicekeys to find the unit and return the appliance. Only called on Command, so not a big impact on performance
def getApplianceIdFromUnit(self, Unit):
for key, value in self.moduleDeviceKeyToUnit.items():
if value['unit'] == Unit:
return value['applianceId']
return ''
# Update the given unit (domoticz Id), only if the value differs from previous or forced
def updateDevice(self, Unit, nValue, sValue, forceUpdate):
# Make sure that the Domoticz device still exists (they can be deleted) before updating it
#self.logMessage("Update "+str(nValue)+":'"+str(sValue)+"' ("+str(Unit)+")")
if (Unit in Devices):
if forceUpdate or (Devices[Unit].nValue != nValue) or (Devices[Unit].sValue != sValue):
Devices[Unit].Update(nValue=nValue, sValue=str(sValue))
if self.debugMode:
self.logMessage("Update do "+str(nValue)+":'"+str(sValue)+"' ("+Devices[Unit].Name+")")
def logMessage(self, Message):
#if 'Domoticz'in locals():
if self.debugMode:
f= open("plugins/Plugwise/log.txt","a+")
f.write(Message+'\r\n')
Domoticz.Log(Message)
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 DumpConfigToLog():
for x in Parameters:
if Parameters[x] != "":
Domoticz.Log( "'" + x + "':'" + str(Parameters[x]) + "'")
Domoticz.Log("Device count: " + str(len(Devices)))
for x in Devices:
Domoticz.Log("Device: " + str(x) + " - " + str(Devices[x]))
Domoticz.Log("Device ID: '" + str(Devices[x].ID) + "'")
Domoticz.Log("Device UnitID: '" + str(Devices[x].Unit) + "'")
Domoticz.Log("Device DeviceID: '" + str(Devices[x].DeviceID) + "'")
Domoticz.Log("Device Name: '" + Devices[x].Name + "'")
Domoticz.Log("Device nValue: " + str(Devices[x].nValue))
Domoticz.Log("Device sValue: '" + Devices[x].sValue + "'")
Domoticz.Log("Device LastLevel: " + str(Devices[x].LastLevel))
return