Plugins/Plugwise
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