From 5fc5688bd75a004f829cb71dc9382a8a8715e79b Mon Sep 17 00:00:00 2001 From: Carsten Schmiemann Date: Mon, 29 Aug 2022 23:27:02 +0200 Subject: [PATCH 1/6] Fix phase compensation calculation because of old SDM630 --- .../meter_einspeisung.py | 17 +- node-red-flows/meters.json | 456 ++++-------------- 2 files changed, 96 insertions(+), 377 deletions(-) diff --git a/dbus-node-red-meter-einspeisung/meter_einspeisung.py b/dbus-node-red-meter-einspeisung/meter_einspeisung.py index e6cdf9e..ffa097d 100644 --- a/dbus-node-red-meter-einspeisung/meter_einspeisung.py +++ b/dbus-node-red-meter-einspeisung/meter_einspeisung.py @@ -98,6 +98,7 @@ class NodeRedMeterEinspeisung: #send data to DBus self._dbusservice['/Ac/Power'] = meter_data['einspeisung']['total_power'] # positive: consumption, negative: feed into grid + self._dbusservice['/Ac/Energy/Forward'] = meter_data['einspeisung']['total_kwh']; self._dbusservice['/Ac/Current'] = meter_data['einspeisung']['total_current'] self._dbusservice['/Ac/L1/Voltage'] = meter_data['einspeisung']['l1_voltage'] self._dbusservice['/Ac/L2/Voltage'] = meter_data['einspeisung']['l2_voltage'] @@ -111,16 +112,9 @@ class NodeRedMeterEinspeisung: self._dbusservice['/Ac/L1/Energy/Forward'] = meter_data['einspeisung']['l1_import'] self._dbusservice['/Ac/L2/Energy/Forward'] = meter_data['einspeisung']['l2_import'] self._dbusservice['/Ac/L3/Energy/Forward'] = meter_data['einspeisung']['l3_import'] - self._dbusservice['/Ac/L1/Energy/Reverse'] = meter_data['einspeisung']['l1_export'] - self._dbusservice['/Ac/L2/Energy/Reverse'] = meter_data['einspeisung']['l2_export'] - self._dbusservice['/Ac/L3/Energy/Reverse'] = meter_data['einspeisung']['l3_export'] - self._dbusservice['/Ac/Energy/Forward'] = self._dbusservice['/Ac/L1/Energy/Forward'] + self._dbusservice['/Ac/L2/Energy/Forward'] + self._dbusservice['/Ac/L3/Energy/Forward'] - self._dbusservice['/Ac/Energy/Reverse'] = self._dbusservice['/Ac/L1/Energy/Reverse'] + self._dbusservice['/Ac/L2/Energy/Reverse'] + self._dbusservice['/Ac/L3/Energy/Reverse'] #logging logging.debug("House Consumption (/Ac/Power): %s" % (self._dbusservice['/Ac/Power'])) - logging.debug("House Forward (/Ac/Energy/Forward): %s" % (self._dbusservice['/Ac/Energy/Forward'])) - logging.debug("House Reverse (/Ac/Energy/Revers): %s" % (self._dbusservice['/Ac/Energy/Reverse'])) logging.debug("---"); # increment UpdateIndex - to show that new data is available @@ -171,13 +165,9 @@ def main(): servicename='com.victronenergy.grid', deviceinstance=40, paths={ - '/Ac/Energy/Forward': {'initial': 0, 'textformat': _kwh}, # energy bought from the grid - '/Ac/Energy/Reverse': {'initial': 0, 'textformat': _kwh}, # energy sold to the grid '/Ac/Power': {'initial': 0, 'textformat': _w}, - + '/Ac/Energy/Forward': {'initial': 0, 'textformat': _kwh}, '/Ac/Current': {'initial': 0, 'textformat': _a}, - '/Ac/Voltage': {'initial': 0, 'textformat': _v}, - '/Ac/L1/Voltage': {'initial': 0, 'textformat': _v}, '/Ac/L2/Voltage': {'initial': 0, 'textformat': _v}, '/Ac/L3/Voltage': {'initial': 0, 'textformat': _v}, @@ -190,9 +180,6 @@ def main(): '/Ac/L1/Energy/Forward': {'initial': 0, 'textformat': _kwh}, '/Ac/L2/Energy/Forward': {'initial': 0, 'textformat': _kwh}, '/Ac/L3/Energy/Forward': {'initial': 0, 'textformat': _kwh}, - '/Ac/L1/Energy/Reverse': {'initial': 0, 'textformat': _kwh}, - '/Ac/L2/Energy/Reverse': {'initial': 0, 'textformat': _kwh}, - '/Ac/L3/Energy/Reverse': {'initial': 0, 'textformat': _kwh}, }) logging.info('Connected to dbus, and switching over to gobject.MainLoop() (= event based)') diff --git a/node-red-flows/meters.json b/node-red-flows/meters.json index 4df36b9..99f31ca 100644 --- a/node-red-flows/meters.json +++ b/node-red-flows/meters.json @@ -7,45 +7,6 @@ "info": "", "env": [] }, - { - "id": "b6fc74e8.4967b", - "type": "http request", - "z": "f96eea4d4a3a345d", - "name": "Total kWh", - "method": "GET", - "ret": "txt", - "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R114,d", - "tls": "", - "persist": false, - "proxy": "", - "authType": "", - "senderr": false, - "x": 320, - "y": 80, - "wires": [ - [ - "3677ad6e.d245a2" - ] - ] - }, - { - "id": "3677ad6e.d245a2", - "type": "function", - "z": "f96eea4d4a3a345d", - "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"einspeisung_total_kwh\", temp)\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 710, - "y": 80, - "wires": [ - [] - ] - }, { "id": "79ff2bd4c3604f42", "type": "inject", @@ -67,13 +28,10 @@ "topic": "", "payload": "", "payloadType": "date", - "x": 130, - "y": 400, + "x": 120, + "y": 300, "wires": [ [ - "b6fc74e8.4967b", - "45b9899aabe3cb69", - "67857677464e4bce", "5c5702e7ebd71a77", "ccf9f0980f83375d", "e2571df80f480199", @@ -101,8 +59,8 @@ "method": "get", "upload": false, "swaggerDoc": "", - "x": 90, - "y": 1260, + "x": 100, + "y": 760, "wires": [ [ "c61fcb67b3a33b47" @@ -135,8 +93,8 @@ "from": "", "to": "", "reg": false, - "x": 830, - "y": 1260, + "x": 840, + "y": 760, "wires": [ [ "83f39f5649b78b86" @@ -150,8 +108,8 @@ "name": "", "statusCode": "", "headers": {}, - "x": 1070, - "y": 1260, + "x": 1080, + "y": 760, "wires": [] }, { @@ -159,14 +117,14 @@ "type": "function", "z": "f96eea4d4a3a345d", "name": "Build Object", - "func": "var einspeisung_total_power = parseFloat(global.get(\"einspeisung_total_power\"))\nvar einspeisung_total_kwh = parseFloat(global.get(\"einspeisung_total_kwh\"))\nvar einspeisung_total_import = parseFloat(global.get(\"einspeisung_total_import\"))\nvar einspeisung_total_export = parseFloat(global.get(\"einspeisung_total_export\"))\n\nvar einspeisung_l1_voltage = parseFloat(global.get(\"einspeisung_l1_voltage\"))\nvar einspeisung_l2_voltage = parseFloat(global.get(\"einspeisung_l2_voltage\"))\nvar einspeisung_l3_voltage = parseFloat(global.get(\"einspeisung_l3_voltage\"))\n\nvar einspeisung_l1_current = parseFloat(global.get(\"einspeisung_l1_current\"))\nvar einspeisung_l2_current = parseFloat(global.get(\"einspeisung_l2_current\"))\nvar einspeisung_l3_current = parseFloat(global.get(\"einspeisung_l3_current\"))\nvar einspeisung_total_current = einspeisung_l1_current + einspeisung_l2_current + einspeisung_l3_current\n\nvar einspeisung_l1_power = parseFloat(global.get(\"einspeisung_l1_power\"))\nvar einspeisung_l2_power = parseFloat(global.get(\"einspeisung_l2_power\"))\nvar einspeisung_l3_power = parseFloat(global.get(\"einspeisung_l3_power\"))\n\nvar einspeisung_l1_import = parseFloat(global.get(\"einspeisung_l1_import\"))\nvar einspeisung_l2_import = parseFloat(global.get(\"einspeisung_l2_import\"))\nvar einspeisung_l3_import = parseFloat(global.get(\"einspeisung_l3_import\"))\n\nvar einspeisung_l1_export = parseFloat(global.get(\"einspeisung_l1_export\"))\nvar einspeisung_l2_export = parseFloat(global.get(\"einspeisung_l2_export\"))\nvar einspeisung_l3_export = parseFloat(global.get(\"einspeisung_l3_export\"))\n\nmsg.payload.einspeisung = {total_power:einspeisung_total_power, total_current:einspeisung_total_current, total_kwh:einspeisung_total_kwh, total_import:einspeisung_total_import, total_export:einspeisung_total_export, l1_voltage:einspeisung_l1_voltage, l2_voltage:einspeisung_l2_voltage, l3_voltage:einspeisung_l3_voltage, l1_current:einspeisung_l1_current, l2_current:einspeisung_l2_current, l3_current:einspeisung_l3_current, l1_power:einspeisung_l1_power, l2_power:einspeisung_l2_power, l3_power:einspeisung_l3_power, l1_import:einspeisung_l1_import, l2_import:einspeisung_l2_import, l3_import:einspeisung_l3_import, l1_export:einspeisung_l1_export, l2_export:einspeisung_l2_export, l3_export:einspeisung_l3_export};\n\nvar keller_total_kwh = parseFloat(global.get(\"keller_total_kwh\"))\n//var keller_l1_voltage = parseFloat(global.get(\"keller_l1_voltage\"))\nvar keller_l1_current = parseFloat(global.get(\"keller_l1_current\"))\nvar keller_l1_power = parseFloat(global.get(\"keller_l1_power\"))\n\nmsg.payload.keller = {total_kwh:keller_total_kwh, l1_current:keller_l1_current, l1_power:keller_l1_power};\nreturn msg;\n", + "func": "var einspeisung_total_power = parseFloat(global.get(\"einspeisung_total_power\"))\n\nvar einspeisung_l1_voltage = parseFloat(global.get(\"einspeisung_l1_voltage\"))\nvar einspeisung_l2_voltage = parseFloat(global.get(\"einspeisung_l2_voltage\"))\nvar einspeisung_l3_voltage = parseFloat(global.get(\"einspeisung_l3_voltage\"))\n\nvar einspeisung_l1_current = parseFloat(global.get(\"einspeisung_l1_current\"))\nvar einspeisung_l2_current = parseFloat(global.get(\"einspeisung_l2_current\"))\nvar einspeisung_l3_current = parseFloat(global.get(\"einspeisung_l3_current\"))\nvar einspeisung_total_current = einspeisung_l1_current + einspeisung_l2_current + einspeisung_l3_current\n\nvar einspeisung_l1_power = parseFloat(global.get(\"einspeisung_l1_power\"))\nvar einspeisung_l2_power = parseFloat(global.get(\"einspeisung_l2_power\"))\nvar einspeisung_l3_power = parseFloat(global.get(\"einspeisung_l3_power\"))\n\nvar einspeisung_l1_import = parseFloat(global.get(\"einspeisung_l1_import\") - global.get(\"einspeisung_l1_export\"));\nvar einspeisung_l2_import = parseFloat(global.get(\"einspeisung_l2_import\") - global.get(\"einspeisung_l2_export\"));\nvar einspeisung_l3_import = parseFloat(global.get(\"einspeisung_l3_import\") - global.get(\"einspeisung_l3_export\"));\n\nvar einspeisung_total_kWh = einspeisung_l1_import + einspeisung_l2_import + einspeisung_l3_import;\n\nmsg.payload.einspeisung = {total_kwh: einspeisung_total_kWh, total_power:einspeisung_total_power, total_current:einspeisung_total_current, l1_voltage:einspeisung_l1_voltage, l2_voltage:einspeisung_l2_voltage, l3_voltage:einspeisung_l3_voltage, l1_current:einspeisung_l1_current, l2_current:einspeisung_l2_current, l3_current:einspeisung_l3_current, l1_power:einspeisung_l1_power, l2_power:einspeisung_l2_power, l3_power:einspeisung_l3_power, l1_import:einspeisung_l1_import, l2_import:einspeisung_l2_import, l3_import:einspeisung_l3_import};\n\nreturn msg;\n", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 450, - "y": 1260, + "x": 460, + "y": 760, "wires": [ [ "307db2619dcf6228" @@ -181,92 +139,14 @@ "property": "payload", "action": "", "pretty": false, - "x": 650, - "y": 1260, + "x": 660, + "y": 760, "wires": [ [ "fbe8cca3419d8161" ] ] }, - { - "id": "45b9899aabe3cb69", - "type": "http request", - "z": "f96eea4d4a3a345d", - "name": "Total Import", - "method": "GET", - "ret": "txt", - "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R115,d", - "tls": "", - "persist": false, - "proxy": "", - "authType": "", - "senderr": false, - "x": 330, - "y": 120, - "wires": [ - [ - "dda7b62c4fed3474" - ] - ] - }, - { - "id": "dda7b62c4fed3474", - "type": "function", - "z": "f96eea4d4a3a345d", - "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"einspeisung_total_import\", temp)\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 710, - "y": 120, - "wires": [ - [] - ] - }, - { - "id": "67857677464e4bce", - "type": "http request", - "z": "f96eea4d4a3a345d", - "name": "Total Export", - "method": "GET", - "ret": "txt", - "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R116,d", - "tls": "", - "persist": false, - "proxy": "", - "authType": "", - "senderr": false, - "x": 330, - "y": 160, - "wires": [ - [ - "b6730232cea51e3f" - ] - ] - }, - { - "id": "b6730232cea51e3f", - "type": "function", - "z": "f96eea4d4a3a345d", - "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"einspeisung_total_export\", temp)\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 710, - "y": 160, - "wires": [ - [] - ] - }, { "id": "5c5702e7ebd71a77", "type": "http request", @@ -281,8 +161,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 330, - "y": 200, + "x": 320, + "y": 100, "wires": [ [ "f5853a536891d509" @@ -300,8 +180,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 710, - "y": 200, + "x": 700, + "y": 100, "wires": [ [] ] @@ -320,8 +200,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 330, - "y": 240, + "x": 320, + "y": 140, "wires": [ [ "a3b33ef1225b85e7" @@ -339,8 +219,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 710, - "y": 240, + "x": 700, + "y": 140, "wires": [ [] ] @@ -359,8 +239,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 330, - "y": 280, + "x": 320, + "y": 180, "wires": [ [ "b284d02cb539994f" @@ -378,8 +258,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 710, - "y": 280, + "x": 700, + "y": 180, "wires": [ [] ] @@ -398,8 +278,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 330, - "y": 320, + "x": 320, + "y": 220, "wires": [ [ "1da7740caa290264" @@ -417,8 +297,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 710, - "y": 320, + "x": 700, + "y": 220, "wires": [ [] ] @@ -437,8 +317,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 330, - "y": 360, + "x": 320, + "y": 260, "wires": [ [ "6c9ebd253a3c3b0d" @@ -456,8 +336,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 710, - "y": 360, + "x": 700, + "y": 260, "wires": [ [] ] @@ -476,8 +356,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 330, - "y": 400, + "x": 320, + "y": 300, "wires": [ [ "d848102461062b3f" @@ -495,8 +375,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 710, - "y": 400, + "x": 700, + "y": 300, "wires": [ [] ] @@ -515,8 +395,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 320, - "y": 440, + "x": 310, + "y": 340, "wires": [ [ "3e88cef8b8b508ac" @@ -534,8 +414,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 710, - "y": 440, + "x": 700, + "y": 340, "wires": [ [] ] @@ -554,8 +434,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 320, - "y": 480, + "x": 310, + "y": 380, "wires": [ [ "466e66a05eddb62f" @@ -573,8 +453,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 710, - "y": 480, + "x": 700, + "y": 380, "wires": [ [] ] @@ -593,8 +473,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 320, - "y": 520, + "x": 310, + "y": 420, "wires": [ [ "a0e6a154bfb85c09" @@ -612,8 +492,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 710, - "y": 520, + "x": 700, + "y": 420, "wires": [ [] ] @@ -671,8 +551,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 320, - "y": 560, + "x": 310, + "y": 460, "wires": [ [ "22bdca2522a4b3c9" @@ -684,14 +564,14 @@ "type": "function", "z": "f96eea4d4a3a345d", "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"einspeisung_l1_import\", temp)\nreturn msg;", + "func": "temp = parseFloat(msg.payload * 0.001).toFixed(2);\nglobal.set(\"einspeisung_l1_import\", temp)\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 710, - "y": 560, + "x": 700, + "y": 460, "wires": [ [] ] @@ -710,8 +590,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 320, - "y": 600, + "x": 310, + "y": 500, "wires": [ [ "51dc86ef4b4e31fd" @@ -723,14 +603,14 @@ "type": "function", "z": "f96eea4d4a3a345d", "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"einspeisung_l2_import\", temp)\nreturn msg;", + "func": "temp = parseFloat(msg.payload * 0.001).toFixed(2);\nglobal.set(\"einspeisung_l2_import\", temp)\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 710, - "y": 600, + "x": 700, + "y": 500, "wires": [ [] ] @@ -749,8 +629,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 320, - "y": 640, + "x": 310, + "y": 540, "wires": [ [ "804899fa9686be16" @@ -762,131 +642,14 @@ "type": "function", "z": "f96eea4d4a3a345d", "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"einspeisung_l3_import\", temp)\nreturn msg;", + "func": "temp = parseFloat(msg.payload * 0.001).toFixed(2);\nglobal.set(\"einspeisung_l3_import\", temp)\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 710, - "y": 640, - "wires": [ - [] - ] - }, - { - "id": "a5f8ee01c537330b", - "type": "http request", - "z": "f96eea4d4a3a345d", - "name": "Export L1", - "method": "GET", - "ret": "txt", - "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R150,d", - "tls": "", - "persist": false, - "proxy": "", - "authType": "", - "senderr": false, - "x": 320, - "y": 680, - "wires": [ - [ - "ba0021b15f94fc41" - ] - ] - }, - { - "id": "ba0021b15f94fc41", - "type": "function", - "z": "f96eea4d4a3a345d", - "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"einspeisung_l1_export\", temp)\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 710, - "y": 680, - "wires": [ - [] - ] - }, - { - "id": "1e1823db7c7b4edd", - "type": "http request", - "z": "f96eea4d4a3a345d", - "name": "Export L2", - "method": "GET", - "ret": "txt", - "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R151,d", - "tls": "", - "persist": false, - "proxy": "", - "authType": "", - "senderr": false, - "x": 320, - "y": 720, - "wires": [ - [ - "68157d83b4f176f5" - ] - ] - }, - { - "id": "68157d83b4f176f5", - "type": "function", - "z": "f96eea4d4a3a345d", - "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"einspeisung_l2_export\", temp)\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 710, - "y": 720, - "wires": [ - [] - ] - }, - { - "id": "7857cce5b19d21ea", - "type": "http request", - "z": "f96eea4d4a3a345d", - "name": "Export L3", - "method": "GET", - "ret": "txt", - "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R152,d", - "tls": "", - "persist": false, - "proxy": "", - "authType": "", - "senderr": false, - "x": 320, - "y": 760, - "wires": [ - [ - "4d12b6c71b023fc8" - ] - ] - }, - { - "id": "4d12b6c71b023fc8", - "type": "function", - "z": "f96eea4d4a3a345d", - "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"einspeisung_l3_export\", temp)\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 710, - "y": 760, + "x": 700, + "y": 540, "wires": [ [] ] @@ -921,151 +684,120 @@ ] }, { - "id": "c4dc8affa9729e08", + "id": "a5f8ee01c537330b", "type": "http request", "z": "f96eea4d4a3a345d", - "name": "Total kWh", + "name": "Export L1", "method": "GET", "ret": "txt", "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R140,d", + "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R150,d", "tls": "", "persist": false, "proxy": "", "authType": "", "senderr": false, - "x": 320, - "y": 860, + "x": 310, + "y": 580, "wires": [ [ - "c232ca118a4fe14b" + "ba0021b15f94fc41" ] ] }, { - "id": "c232ca118a4fe14b", + "id": "ba0021b15f94fc41", "type": "function", "z": "f96eea4d4a3a345d", "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"keller_total_kwh\", temp)\nreturn msg;", + "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"einspeisung_l1_export\", temp)\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 710, - "y": 860, + "x": 700, + "y": 580, "wires": [ [] ] }, { - "id": "426bcd77cd358a43", + "id": "1e1823db7c7b4edd", "type": "http request", "z": "f96eea4d4a3a345d", - "name": "Current L1", + "name": "Export L2", "method": "GET", "ret": "txt", "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R141,d", + "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R151,d", "tls": "", "persist": false, "proxy": "", "authType": "", "senderr": false, - "x": 330, - "y": 900, + "x": 310, + "y": 620, "wires": [ [ - "731a40c90fb45ee6" + "68157d83b4f176f5" ] ] }, { - "id": "731a40c90fb45ee6", + "id": "68157d83b4f176f5", "type": "function", "z": "f96eea4d4a3a345d", "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"keller_l1_current\", temp)\nreturn msg;", + "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"einspeisung_l2_export\", temp)\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 710, - "y": 900, + "x": 700, + "y": 620, "wires": [ [] ] }, { - "id": "2daafcdfb6ee11cf", + "id": "7857cce5b19d21ea", "type": "http request", "z": "f96eea4d4a3a345d", - "name": "Power L1", + "name": "Export L3", "method": "GET", "ret": "txt", "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R142,d", + "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R152,d", "tls": "", "persist": false, "proxy": "", "authType": "", "senderr": false, - "x": 320, - "y": 940, + "x": 310, + "y": 660, "wires": [ [ - "c9a03dfa1847f20e" + "4d12b6c71b023fc8" ] ] }, { - "id": "c9a03dfa1847f20e", + "id": "4d12b6c71b023fc8", "type": "function", "z": "f96eea4d4a3a345d", "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.1).toFixed(1);\nglobal.set(\"keller_l1_power\", temp)\nreturn msg;", + "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"einspeisung_l3_export\", temp)\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 710, - "y": 940, + "x": 700, + "y": 660, "wires": [ [] ] - }, - { - "id": "7298f7b74b3bb8df", - "type": "inject", - "z": "f96eea4d4a3a345d", - "name": "Poll every 5 sec", - "props": [ - { - "p": "payload" - }, - { - "p": "topic", - "vt": "str" - } - ], - "repeat": "", - "crontab": "", - "once": false, - "onceDelay": 0.1, - "topic": "", - "payload": "", - "payloadType": "date", - "x": 100, - "y": 900, - "wires": [ - [ - "c4dc8affa9729e08", - "426bcd77cd358a43", - "2daafcdfb6ee11cf" - ] - ] } ] \ No newline at end of file From bc03b8d259e37aec783337a75d5c05e4e5c6b6f2 Mon Sep 17 00:00:00 2001 From: Carsten Schmiemann Date: Tue, 30 Aug 2022 00:41:10 +0200 Subject: [PATCH 2/6] revert 7f11414f359768cbf365b871659866436004bed1 revert Remove victron lib because I use venus os ones --- dbus-node-red-meter-einspeisung/ve_utils.py | 262 +++++++++ dbus-node-red-meter-einspeisung/vedbus.py | 611 ++++++++++++++++++++ dbus-node-red-temp-outside/ve_utils.py | 262 +++++++++ dbus-node-red-temp-outside/vedbus.py | 611 ++++++++++++++++++++ 4 files changed, 1746 insertions(+) create mode 100644 dbus-node-red-meter-einspeisung/ve_utils.py create mode 100644 dbus-node-red-meter-einspeisung/vedbus.py create mode 100644 dbus-node-red-temp-outside/ve_utils.py create mode 100644 dbus-node-red-temp-outside/vedbus.py diff --git a/dbus-node-red-meter-einspeisung/ve_utils.py b/dbus-node-red-meter-einspeisung/ve_utils.py new file mode 100644 index 0000000..2843513 --- /dev/null +++ b/dbus-node-red-meter-einspeisung/ve_utils.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys +from traceback import print_exc +from os import _exit as os_exit +from os import statvfs +from subprocess import check_output, CalledProcessError +import logging +import dbus +logger = logging.getLogger(__name__) + +VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) + +class NoVrmPortalIdError(Exception): + pass + +# Use this function to make sure the code quits on an unexpected exception. Make sure to use it +# when using GLib.idle_add and also GLib.timeout_add. +# Without this, the code will just keep running, since GLib does not stop the mainloop on an +# exception. +# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2) +def exit_on_error(func, *args, **kwargs): + try: + return func(*args, **kwargs) + except: + try: + print ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit') + print_exc() + except: + pass + + # sys.exit() is not used, since that throws an exception, which does not lead to a program + # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. + os_exit(1) + + +__vrm_portal_id = None +def get_vrm_portal_id(): + # The original definition of the VRM Portal ID is that it is the mac + # address of the onboard- ethernet port (eth0), stripped from its colons + # (:) and lower case. This may however differ between platforms. On Venus + # the task is therefore deferred to /sbin/get-unique-id so that a + # platform specific method can be easily defined. + # + # If /sbin/get-unique-id does not exist, then use the ethernet address + # of eth0. This also handles the case where velib_python is used as a + # package install on a Raspberry Pi. + # + # On a Linux host where the network interface may not be eth0, you can set + # the VRM_IFACE environment variable to the correct name. + + global __vrm_portal_id + + if __vrm_portal_id: + return __vrm_portal_id + + portal_id = None + + # First try the method that works if we don't have a data partition. This + # will fail when the current user is not root. + try: + portal_id = check_output("/sbin/get-unique-id").decode("utf-8", "ignore").strip() + if not portal_id: + raise NoVrmPortalIdError("get-unique-id returned blank") + __vrm_portal_id = portal_id + return portal_id + except CalledProcessError: + # get-unique-id returned non-zero + raise NoVrmPortalIdError("get-unique-id returned non-zero") + except OSError: + # File doesn't exist, use fallback + pass + + # Fall back to getting our id using a syscall. Assume we are on linux. + # Allow the user to override what interface is used using an environment + # variable. + import fcntl, socket, struct, os + + iface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii') + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) + except IOError: + raise NoVrmPortalIdError("ioctl failed for eth0") + + __vrm_portal_id = info[18:24].hex() + return __vrm_portal_id + + +# See VE.Can registers - public.docx for definition of this conversion +def convert_vreg_version_to_readable(version): + def str_to_arr(x, length): + a = [] + for i in range(0, len(x), length): + a.append(x[i:i+length]) + return a + + x = "%x" % version + x = x.upper() + + if len(x) == 5 or len(x) == 3 or len(x) == 1: + x = '0' + x + + a = str_to_arr(x, 2); + + # remove the first 00 if there are three bytes and it is 00 + if len(a) == 3 and a[0] == '00': + a.remove(0); + + # if we have two or three bytes now, and the first character is a 0, remove it + if len(a) >= 2 and a[0][0:1] == '0': + a[0] = a[0][1]; + + result = '' + for item in a: + result += ('.' if result != '' else '') + item + + + result = 'v' + result + + return result + + +def get_free_space(path): + result = -1 + + try: + s = statvfs(path) + result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users + except Exception as ex: + logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) + + return result + + +def _get_sysfs_machine_name(): + try: + with open('/sys/firmware/devicetree/base/model', 'r') as f: + return f.read().rstrip('\x00') + except IOError: + pass + + return None + +# Returns None if it cannot find a machine name. Otherwise returns the string +# containing the name +def get_machine_name(): + # First try calling the venus utility script + try: + return check_output("/usr/bin/product-name").strip().decode('UTF-8') + except (CalledProcessError, OSError): + pass + + # Fall back to sysfs + name = _get_sysfs_machine_name() + if name is not None: + return name + + # Fall back to venus build machine name + try: + with open('/etc/venus/machine', 'r', encoding='UTF-8') as f: + return f.read().strip() + except IOError: + pass + + return None + + +def get_product_id(): + """ Find the machine ID and return it. """ + + # First try calling the venus utility script + try: + return check_output("/usr/bin/product-id").strip() + except (CalledProcessError, OSError): + pass + + # Fall back machine name mechanism + name = _get_sysfs_machine_name() + return { + 'Color Control GX': 'C001', + 'Venus GX': 'C002', + 'Octo GX': 'C006', + 'EasySolar-II': 'C007', + 'MultiPlus-II': 'C008', + 'Maxi GX': 'C009', + 'Cerbo GX': 'C00A' + }.get(name, 'C003') # C003 is Generic + + +# Returns False if it cannot open the file. Otherwise returns its rstripped contents +def read_file(path): + content = False + + try: + with open(path, 'r') as f: + content = f.read().rstrip() + except Exception as ex: + logger.debug("Error while reading %s: %s" % (path, ex)) + + return content + + +def wrap_dbus_value(value): + if value is None: + return VEDBUS_INVALID + if isinstance(value, float): + return dbus.Double(value, variant_level=1) + if isinstance(value, bool): + return dbus.Boolean(value, variant_level=1) + if isinstance(value, int): + try: + return dbus.Int32(value, variant_level=1) + except OverflowError: + return dbus.Int64(value, variant_level=1) + if isinstance(value, str): + return dbus.String(value, variant_level=1) + if isinstance(value, list): + if len(value) == 0: + # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. + # A (signed) integer is dangerous, because an empty list of signed integers is used to encode + # an invalid value. + return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) + return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) + if isinstance(value, dict): + # Wrapping the keys of the dictionary causes D-Bus errors like: + # 'arguments to dbus_message_iter_open_container() were incorrect, + # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && + # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || + # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' + return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) + return value + + +dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) + + +def unwrap_dbus_value(val): + """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, + a float will be returned.""" + if isinstance(val, dbus_int_types): + return int(val) + if isinstance(val, dbus.Double): + return float(val) + if isinstance(val, dbus.Array): + v = [unwrap_dbus_value(x) for x in val] + return None if len(v) == 0 else v + if isinstance(val, (dbus.Signature, dbus.String)): + return str(val) + # Python has no byte type, so we convert to an integer. + if isinstance(val, dbus.Byte): + return int(val) + if isinstance(val, dbus.ByteArray): + return "".join([bytes(x) for x in val]) + if isinstance(val, (list, tuple)): + return [unwrap_dbus_value(x) for x in val] + if isinstance(val, (dbus.Dictionary, dict)): + # Do not unwrap the keys, see comment in wrap_dbus_value + return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) + if isinstance(val, dbus.Boolean): + return bool(val) + return val diff --git a/dbus-node-red-meter-einspeisung/vedbus.py b/dbus-node-red-meter-einspeisung/vedbus.py new file mode 100644 index 0000000..8c101ea --- /dev/null +++ b/dbus-node-red-meter-einspeisung/vedbus.py @@ -0,0 +1,611 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import dbus.service +import logging +import traceback +import os +import weakref +from collections import defaultdict +from ve_utils import wrap_dbus_value, unwrap_dbus_value + +# vedbus contains three classes: +# VeDbusItemImport -> use this to read data from the dbus, ie import +# VeDbusItemExport -> use this to export data to the dbus (one value) +# VeDbusService -> use that to create a service and export several values to the dbus + +# Code for VeDbusItemImport is copied from busitem.py and thereafter modified. +# All projects that used busitem.py need to migrate to this package. And some +# projects used to define there own equivalent of VeDbusItemExport. Better to +# use VeDbusItemExport, or even better the VeDbusService class that does it all for you. + +# TODOS +# 1 check for datatypes, it works now, but not sure if all is compliant with +# com.victronenergy.BusItem interface definition. See also the files in +# tests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps +# something similar should also be done in VeDbusBusItemExport? +# 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object? +# 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking +# changes possible. Does everybody first invalidate its data before leaving the bus? +# And what about before taking one object away from the bus, instead of taking the +# whole service offline? +# They should! And after taking one value away, do we need to know that someone left +# the bus? Or we just keep that value in invalidated for ever? Result is that we can't +# see the difference anymore between an invalidated value and a value that was first on +# the bus and later not anymore. See comments above VeDbusItemImport as well. +# 9 there are probably more todos in the code below. + +# Some thoughts with regards to the data types: +# +# Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types +# --- +# Variants are represented by setting the variant_level keyword argument in the +# constructor of any D-Bus data type to a value greater than 0 (variant_level 1 +# means a variant containing some other data type, variant_level 2 means a variant +# containing a variant containing some other data type, and so on). If a non-variant +# is passed as an argument but introspection indicates that a variant is expected, +# it'll automatically be wrapped in a variant. +# --- +# +# Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass +# of Python int. dbus.String is a subclass of Python standard class unicode, etcetera +# +# So all together that explains why we don't need to explicitly convert back and forth +# between the dbus datatypes and the standard python datatypes. Note that all datatypes +# in python are objects. Even an int is an object. + +# The signature of a variant is 'v'. + +# Export ourselves as a D-Bus service. +class VeDbusService(object): + def __init__(self, servicename, bus=None): + # dict containing the VeDbusItemExport objects, with their path as the key. + self._dbusobjects = {} + self._dbusnodes = {} + self._ratelimiters = [] + self._dbusname = None + + # dict containing the onchange callbacks, for each object. Object path is the key + self._onchangecallbacks = {} + + # Connect to session bus whenever present, else use the system bus + self._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus()) + + # make the dbus connection available to outside, could make this a true property instead, but ach.. + self.dbusconn = self._dbusconn + + # Register ourselves on the dbus, trigger an error if already in use (do_not_queue) + self._dbusname = dbus.service.BusName(servicename, self._dbusconn, do_not_queue=True) + + # Add the root item that will return all items as a tree + self._dbusnodes['/'] = VeDbusRootExport(self._dbusconn, '/', self) + + logging.info("registered ourselves on D-Bus as %s" % servicename) + + # To force immediate deregistering of this dbus service and all its object paths, explicitly + # call __del__(). + def __del__(self): + for node in list(self._dbusnodes.values()): + node.__del__() + self._dbusnodes.clear() + for item in list(self._dbusobjects.values()): + item.__del__() + self._dbusobjects.clear() + if self._dbusname: + self._dbusname.__del__() # Forces call to self._bus.release_name(self._name), see source code + self._dbusname = None + + # @param callbackonchange function that will be called when this value is changed. First parameter will + # be the path of the object, second the new value. This callback should return + # True to accept the change, False to reject it. + def add_path(self, path, value, description="", writeable=False, + onchangecallback=None, gettextcallback=None, valuetype=None): + + if onchangecallback is not None: + self._onchangecallbacks[path] = onchangecallback + + item = VeDbusItemExport( + self._dbusconn, path, value, description, writeable, + self._value_changed, gettextcallback, deletecallback=self._item_deleted, valuetype=valuetype) + + spl = path.split('/') + for i in range(2, len(spl)): + subPath = '/'.join(spl[:i]) + if subPath not in self._dbusnodes and subPath not in self._dbusobjects: + self._dbusnodes[subPath] = VeDbusTreeExport(self._dbusconn, subPath, self) + self._dbusobjects[path] = item + logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable)) + + # Add the mandatory paths, as per victron dbus api doc + def add_mandatory_paths(self, processname, processversion, connection, + deviceinstance, productid, productname, firmwareversion, hardwareversion, connected): + self.add_path('/Mgmt/ProcessName', processname) + self.add_path('/Mgmt/ProcessVersion', processversion) + self.add_path('/Mgmt/Connection', connection) + + # Create rest of the mandatory objects + self.add_path('/DeviceInstance', deviceinstance) + self.add_path('/ProductId', productid) + self.add_path('/ProductName', productname) + self.add_path('/FirmwareVersion', firmwareversion) + self.add_path('/HardwareVersion', hardwareversion) + self.add_path('/Connected', connected) + + # Callback function that is called from the VeDbusItemExport objects when a value changes. This function + # maps the change-request to the onchangecallback given to us for this specific path. + def _value_changed(self, path, newvalue): + if path not in self._onchangecallbacks: + return True + + return self._onchangecallbacks[path](path, newvalue) + + def _item_deleted(self, path): + self._dbusobjects.pop(path) + for np in list(self._dbusnodes.keys()): + if np != '/': + for ip in self._dbusobjects: + if ip.startswith(np + '/'): + break + else: + self._dbusnodes[np].__del__() + self._dbusnodes.pop(np) + + def __getitem__(self, path): + return self._dbusobjects[path].local_get_value() + + def __setitem__(self, path, newvalue): + self._dbusobjects[path].local_set_value(newvalue) + + def __delitem__(self, path): + self._dbusobjects[path].__del__() # Invalidates and then removes the object path + assert path not in self._dbusobjects + + def __contains__(self, path): + return path in self._dbusobjects + + def __enter__(self): + l = ServiceContext(self) + self._ratelimiters.append(l) + return l + + def __exit__(self, *exc): + # pop off the top one and flush it. If with statements are nested + # then each exit flushes its own part. + if self._ratelimiters: + self._ratelimiters.pop().flush() + +class ServiceContext(object): + def __init__(self, parent): + self.parent = parent + self.changes = {} + + def __getitem__(self, path): + return self.parent[path] + + def __setitem__(self, path, newvalue): + c = self.parent._dbusobjects[path]._local_set_value(newvalue) + if c is not None: + self.changes[path] = c + + def flush(self): + if self.changes: + self.parent._dbusnodes['/'].ItemsChanged(self.changes) + +class TrackerDict(defaultdict): + """ Same as defaultdict, but passes the key to default_factory. """ + def __missing__(self, key): + self[key] = x = self.default_factory(key) + return x + +class VeDbusRootTracker(object): + """ This tracks the root of a dbus path and listens for PropertiesChanged + signals. When a signal arrives, parse it and unpack the key/value changes + into traditional events, then pass it to the original eventCallback + method. """ + def __init__(self, bus, serviceName): + self.importers = defaultdict(weakref.WeakSet) + self.serviceName = serviceName + self._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal( + "ItemsChanged", weak_functor(self._items_changed_handler)) + + def __del__(self): + self._match.remove() + self._match = None + + def add(self, i): + self.importers[i.path].add(i) + + def _items_changed_handler(self, items): + if not isinstance(items, dict): + return + + for path, changes in items.items(): + try: + v = changes['Value'] + except KeyError: + continue + + try: + t = changes['Text'] + except KeyError: + t = str(unwrap_dbus_value(v)) + + for i in self.importers.get(path, ()): + i._properties_changed_handler({'Value': v, 'Text': t}) + +""" +Importing basics: + - If when we power up, the D-Bus service does not exist, or it does exist and the path does not + yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its + initial value, which VeDbusItemImport will receive and use to update local cache. And, when set, + call the eventCallback. + - If when we power up, save it + - When using get_value, know that there is no difference between services (or object paths) that don't + exist and paths that are invalid (= empty array, see above). Both will return None. In case you do + really want to know ifa path exists or not, use the exists property. + - When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals + with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged- + signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this + class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this + class. + +Read when using this class: +Note that when a service leaves that D-Bus without invalidating all its exported objects first, for +example because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport, +make sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor, +because that takes care of all of that for you. +""" +class VeDbusItemImport(object): + def __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True): + instance = object.__new__(cls) + + # If signal tracking should be done, also add to root tracker + if createsignal: + if "_roots" not in cls.__dict__: + cls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k)) + + return instance + + ## Constructor + # @param bus the bus-object (SESSION or SYSTEM). + # @param serviceName the dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1' + # @param path the object-path, for example '/Dc/V' + # @param eventCallback function that you want to be called on a value change + # @param createSignal only set this to False if you use this function to one time read a value. When + # leaving it to True, make sure to also subscribe to the NameOwnerChanged signal + # elsewhere. See also note some 15 lines up. + def __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True): + # TODO: is it necessary to store _serviceName and _path? Isn't it + # stored in the bus_getobjectsomewhere? + self._serviceName = serviceName + self._path = path + self._match = None + # TODO: _proxy is being used in settingsdevice.py, make a getter for that + self._proxy = bus.get_object(serviceName, path, introspect=False) + self.eventCallback = eventCallback + + assert eventCallback is None or createsignal == True + if createsignal: + self._match = self._proxy.connect_to_signal( + "PropertiesChanged", weak_functor(self._properties_changed_handler)) + self._roots[serviceName].add(self) + + # store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to + # None, same as when a value is invalid + self._cachedvalue = None + try: + v = self._proxy.GetValue() + except dbus.exceptions.DBusException: + pass + else: + self._cachedvalue = unwrap_dbus_value(v) + + def __del__(self): + if self._match is not None: + self._match.remove() + self._match = None + self._proxy = None + + def _refreshcachedvalue(self): + self._cachedvalue = unwrap_dbus_value(self._proxy.GetValue()) + + ## Returns the path as a string, for example '/AC/L1/V' + @property + def path(self): + return self._path + + ## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1 + @property + def serviceName(self): + return self._serviceName + + ## Returns the value of the dbus-item. + # the type will be a dbus variant, for example dbus.Int32(0, variant_level=1) + # this is not a property to keep the name consistant with the com.victronenergy.busitem interface + # returns None when the property is invalid + def get_value(self): + return self._cachedvalue + + ## Writes a new value to the dbus-item + def set_value(self, newvalue): + r = self._proxy.SetValue(wrap_dbus_value(newvalue)) + + # instead of just saving the value, go to the dbus and get it. So we have the right type etc. + if r == 0: + self._refreshcachedvalue() + + return r + + ## Resets the item to its default value + def set_default(self): + self._proxy.SetDefault() + self._refreshcachedvalue() + + ## Returns the text representation of the value. + # For example when the value is an enum/int GetText might return the string + # belonging to that enum value. Another example, for a voltage, GetValue + # would return a float, 12.0Volt, and GetText could return 12 VDC. + # + # Note that this depends on how the dbus-producer has implemented this. + def get_text(self): + return self._proxy.GetText() + + ## Returns true of object path exists, and false if it doesn't + @property + def exists(self): + # TODO: do some real check instead of this crazy thing. + r = False + try: + r = self._proxy.GetValue() + r = True + except dbus.exceptions.DBusException: + pass + + return r + + ## callback for the trigger-event. + # @param eventCallback the event-callback-function. + @property + def eventCallback(self): + return self._eventCallback + + @eventCallback.setter + def eventCallback(self, eventCallback): + self._eventCallback = eventCallback + + ## Is called when the value of the imported bus-item changes. + # Stores the new value in our local cache, and calls the eventCallback, if set. + def _properties_changed_handler(self, changes): + if "Value" in changes: + changes['Value'] = unwrap_dbus_value(changes['Value']) + self._cachedvalue = changes['Value'] + if self._eventCallback: + # The reason behind this try/except is to prevent errors silently ending up the an error + # handler in the dbus code. + try: + self._eventCallback(self._serviceName, self._path, changes) + except: + traceback.print_exc() + os._exit(1) # sys.exit() is not used, since that also throws an exception + + +class VeDbusTreeExport(dbus.service.Object): + def __init__(self, bus, objectPath, service): + dbus.service.Object.__init__(self, bus, objectPath) + self._service = service + logging.debug("VeDbusTreeExport %s has been created" % objectPath) + + def __del__(self): + # self._get_path() will raise an exception when retrieved after the call to .remove_from_connection, + # so we need a copy. + path = self._get_path() + if path is None: + return + self.remove_from_connection() + logging.debug("VeDbusTreeExport %s has been removed" % path) + + def _get_path(self): + if len(self._locations) == 0: + return None + return self._locations[0][1] + + def _get_value_handler(self, path, get_text=False): + logging.debug("_get_value_handler called for %s" % path) + r = {} + px = path + if not px.endswith('/'): + px += '/' + for p, item in self._service._dbusobjects.items(): + if p.startswith(px): + v = item.GetText() if get_text else wrap_dbus_value(item.local_get_value()) + r[p[len(px):]] = v + logging.debug(r) + return r + + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetValue(self): + value = self._get_value_handler(self._get_path()) + return dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1) + + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetText(self): + return self._get_value_handler(self._get_path(), True) + + def local_get_value(self): + return self._get_value_handler(self.path) + +class VeDbusRootExport(VeDbusTreeExport): + @dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}') + def ItemsChanged(self, changes): + pass + + @dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}') + def GetItems(self): + return { + path: { + 'Value': wrap_dbus_value(item.local_get_value()), + 'Text': item.GetText() } + for path, item in self._service._dbusobjects.items() + } + + +class VeDbusItemExport(dbus.service.Object): + ## Constructor of VeDbusItemExport + # + # Use this object to export (publish), values on the dbus + # Creates the dbus-object under the given dbus-service-name. + # @param bus The dbus object. + # @param objectPath The dbus-object-path. + # @param value Value to initialize ourselves with, defaults to None which means Invalid + # @param description String containing a description. Can be called over the dbus with GetDescription() + # @param writeable what would this do!? :). + # @param callback Function that will be called when someone else changes the value of this VeBusItem + # over the dbus. First parameter passed to callback will be our path, second the new + # value. This callback should return True to accept the change, False to reject it. + def __init__(self, bus, objectPath, value=None, description=None, writeable=False, + onchangecallback=None, gettextcallback=None, deletecallback=None, + valuetype=None): + dbus.service.Object.__init__(self, bus, objectPath) + self._onchangecallback = onchangecallback + self._gettextcallback = gettextcallback + self._value = value + self._description = description + self._writeable = writeable + self._deletecallback = deletecallback + self._type = valuetype + + # To force immediate deregistering of this dbus object, explicitly call __del__(). + def __del__(self): + # self._get_path() will raise an exception when retrieved after the + # call to .remove_from_connection, so we need a copy. + path = self._get_path() + if path == None: + return + if self._deletecallback is not None: + self._deletecallback(path) + self.remove_from_connection() + logging.debug("VeDbusItemExport %s has been removed" % path) + + def _get_path(self): + if len(self._locations) == 0: + return None + return self._locations[0][1] + + ## Sets the value. And in case the value is different from what it was, a signal + # will be emitted to the dbus. This function is to be used in the python code that + # is using this class to export values to the dbus. + # set value to None to indicate that it is Invalid + def local_set_value(self, newvalue): + changes = self._local_set_value(newvalue) + if changes is not None: + self.PropertiesChanged(changes) + + def _local_set_value(self, newvalue): + if self._value == newvalue: + return None + + self._value = newvalue + return { + 'Value': wrap_dbus_value(newvalue), + 'Text': self.GetText() + } + + def local_get_value(self): + return self._value + + # ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ==== + + ## Dbus exported method SetValue + # Function is called over the D-Bus by other process. It will first check (via callback) if new + # value is accepted. And it is, stores it and emits a changed-signal. + # @param value The new value. + # @return completion-code When successful a 0 is return, and when not a -1 is returned. + @dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i') + def SetValue(self, newvalue): + if not self._writeable: + return 1 # NOT OK + + newvalue = unwrap_dbus_value(newvalue) + + # If value type is enforced, cast it. If the type can be coerced + # python will do it for us. This allows ints to become floats, + # or bools to become ints. Additionally also allow None, so that + # a path may be invalidated. + if self._type is not None and newvalue is not None: + try: + newvalue = self._type(newvalue) + except (ValueError, TypeError): + return 1 # NOT OK + + if newvalue == self._value: + return 0 # OK + + # call the callback given to us, and check if new value is OK. + if (self._onchangecallback is None or + (self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))): + + self.local_set_value(newvalue) + return 0 # OK + + return 2 # NOT OK + + ## Dbus exported method GetDescription + # + # Returns the a description. + # @param language A language code (e.g. ISO 639-1 en-US). + # @param length Lenght of the language string. + # @return description + @dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s') + def GetDescription(self, language, length): + return self._description if self._description is not None else 'No description given' + + ## Dbus exported method GetValue + # Returns the value. + # @return the value when valid, and otherwise an empty array + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetValue(self): + return wrap_dbus_value(self._value) + + ## Dbus exported method GetText + # Returns the value as string of the dbus-object-path. + # @return text A text-value. '---' when local value is invalid + @dbus.service.method('com.victronenergy.BusItem', out_signature='s') + def GetText(self): + if self._value is None: + return '---' + + # Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we + # have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from + # the application itself, as all data from the D-Bus should have been unwrapped by now. + if self._gettextcallback is None and type(self._value) == dbus.Byte: + return str(int(self._value)) + + if self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId': + return "0x%X" % self._value + + if self._gettextcallback is None: + return str(self._value) + + return self._gettextcallback(self.__dbus_object_path__, self._value) + + ## The signal that indicates that the value has changed. + # Other processes connected to this BusItem object will have subscribed to the + # event when they want to track our state. + @dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}') + def PropertiesChanged(self, changes): + pass + +## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference +## to the object which method is to be called. +## Use this object to break circular references. +class weak_functor: + def __init__(self, f): + self._r = weakref.ref(f.__self__) + self._f = weakref.ref(f.__func__) + + def __call__(self, *args, **kargs): + r = self._r() + f = self._f() + if r == None or f == None: + return + f(r, *args, **kargs) diff --git a/dbus-node-red-temp-outside/ve_utils.py b/dbus-node-red-temp-outside/ve_utils.py new file mode 100644 index 0000000..2843513 --- /dev/null +++ b/dbus-node-red-temp-outside/ve_utils.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys +from traceback import print_exc +from os import _exit as os_exit +from os import statvfs +from subprocess import check_output, CalledProcessError +import logging +import dbus +logger = logging.getLogger(__name__) + +VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) + +class NoVrmPortalIdError(Exception): + pass + +# Use this function to make sure the code quits on an unexpected exception. Make sure to use it +# when using GLib.idle_add and also GLib.timeout_add. +# Without this, the code will just keep running, since GLib does not stop the mainloop on an +# exception. +# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2) +def exit_on_error(func, *args, **kwargs): + try: + return func(*args, **kwargs) + except: + try: + print ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit') + print_exc() + except: + pass + + # sys.exit() is not used, since that throws an exception, which does not lead to a program + # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. + os_exit(1) + + +__vrm_portal_id = None +def get_vrm_portal_id(): + # The original definition of the VRM Portal ID is that it is the mac + # address of the onboard- ethernet port (eth0), stripped from its colons + # (:) and lower case. This may however differ between platforms. On Venus + # the task is therefore deferred to /sbin/get-unique-id so that a + # platform specific method can be easily defined. + # + # If /sbin/get-unique-id does not exist, then use the ethernet address + # of eth0. This also handles the case where velib_python is used as a + # package install on a Raspberry Pi. + # + # On a Linux host where the network interface may not be eth0, you can set + # the VRM_IFACE environment variable to the correct name. + + global __vrm_portal_id + + if __vrm_portal_id: + return __vrm_portal_id + + portal_id = None + + # First try the method that works if we don't have a data partition. This + # will fail when the current user is not root. + try: + portal_id = check_output("/sbin/get-unique-id").decode("utf-8", "ignore").strip() + if not portal_id: + raise NoVrmPortalIdError("get-unique-id returned blank") + __vrm_portal_id = portal_id + return portal_id + except CalledProcessError: + # get-unique-id returned non-zero + raise NoVrmPortalIdError("get-unique-id returned non-zero") + except OSError: + # File doesn't exist, use fallback + pass + + # Fall back to getting our id using a syscall. Assume we are on linux. + # Allow the user to override what interface is used using an environment + # variable. + import fcntl, socket, struct, os + + iface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii') + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) + except IOError: + raise NoVrmPortalIdError("ioctl failed for eth0") + + __vrm_portal_id = info[18:24].hex() + return __vrm_portal_id + + +# See VE.Can registers - public.docx for definition of this conversion +def convert_vreg_version_to_readable(version): + def str_to_arr(x, length): + a = [] + for i in range(0, len(x), length): + a.append(x[i:i+length]) + return a + + x = "%x" % version + x = x.upper() + + if len(x) == 5 or len(x) == 3 or len(x) == 1: + x = '0' + x + + a = str_to_arr(x, 2); + + # remove the first 00 if there are three bytes and it is 00 + if len(a) == 3 and a[0] == '00': + a.remove(0); + + # if we have two or three bytes now, and the first character is a 0, remove it + if len(a) >= 2 and a[0][0:1] == '0': + a[0] = a[0][1]; + + result = '' + for item in a: + result += ('.' if result != '' else '') + item + + + result = 'v' + result + + return result + + +def get_free_space(path): + result = -1 + + try: + s = statvfs(path) + result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users + except Exception as ex: + logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) + + return result + + +def _get_sysfs_machine_name(): + try: + with open('/sys/firmware/devicetree/base/model', 'r') as f: + return f.read().rstrip('\x00') + except IOError: + pass + + return None + +# Returns None if it cannot find a machine name. Otherwise returns the string +# containing the name +def get_machine_name(): + # First try calling the venus utility script + try: + return check_output("/usr/bin/product-name").strip().decode('UTF-8') + except (CalledProcessError, OSError): + pass + + # Fall back to sysfs + name = _get_sysfs_machine_name() + if name is not None: + return name + + # Fall back to venus build machine name + try: + with open('/etc/venus/machine', 'r', encoding='UTF-8') as f: + return f.read().strip() + except IOError: + pass + + return None + + +def get_product_id(): + """ Find the machine ID and return it. """ + + # First try calling the venus utility script + try: + return check_output("/usr/bin/product-id").strip() + except (CalledProcessError, OSError): + pass + + # Fall back machine name mechanism + name = _get_sysfs_machine_name() + return { + 'Color Control GX': 'C001', + 'Venus GX': 'C002', + 'Octo GX': 'C006', + 'EasySolar-II': 'C007', + 'MultiPlus-II': 'C008', + 'Maxi GX': 'C009', + 'Cerbo GX': 'C00A' + }.get(name, 'C003') # C003 is Generic + + +# Returns False if it cannot open the file. Otherwise returns its rstripped contents +def read_file(path): + content = False + + try: + with open(path, 'r') as f: + content = f.read().rstrip() + except Exception as ex: + logger.debug("Error while reading %s: %s" % (path, ex)) + + return content + + +def wrap_dbus_value(value): + if value is None: + return VEDBUS_INVALID + if isinstance(value, float): + return dbus.Double(value, variant_level=1) + if isinstance(value, bool): + return dbus.Boolean(value, variant_level=1) + if isinstance(value, int): + try: + return dbus.Int32(value, variant_level=1) + except OverflowError: + return dbus.Int64(value, variant_level=1) + if isinstance(value, str): + return dbus.String(value, variant_level=1) + if isinstance(value, list): + if len(value) == 0: + # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. + # A (signed) integer is dangerous, because an empty list of signed integers is used to encode + # an invalid value. + return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) + return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) + if isinstance(value, dict): + # Wrapping the keys of the dictionary causes D-Bus errors like: + # 'arguments to dbus_message_iter_open_container() were incorrect, + # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && + # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || + # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' + return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) + return value + + +dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) + + +def unwrap_dbus_value(val): + """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, + a float will be returned.""" + if isinstance(val, dbus_int_types): + return int(val) + if isinstance(val, dbus.Double): + return float(val) + if isinstance(val, dbus.Array): + v = [unwrap_dbus_value(x) for x in val] + return None if len(v) == 0 else v + if isinstance(val, (dbus.Signature, dbus.String)): + return str(val) + # Python has no byte type, so we convert to an integer. + if isinstance(val, dbus.Byte): + return int(val) + if isinstance(val, dbus.ByteArray): + return "".join([bytes(x) for x in val]) + if isinstance(val, (list, tuple)): + return [unwrap_dbus_value(x) for x in val] + if isinstance(val, (dbus.Dictionary, dict)): + # Do not unwrap the keys, see comment in wrap_dbus_value + return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) + if isinstance(val, dbus.Boolean): + return bool(val) + return val diff --git a/dbus-node-red-temp-outside/vedbus.py b/dbus-node-red-temp-outside/vedbus.py new file mode 100644 index 0000000..8c101ea --- /dev/null +++ b/dbus-node-red-temp-outside/vedbus.py @@ -0,0 +1,611 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import dbus.service +import logging +import traceback +import os +import weakref +from collections import defaultdict +from ve_utils import wrap_dbus_value, unwrap_dbus_value + +# vedbus contains three classes: +# VeDbusItemImport -> use this to read data from the dbus, ie import +# VeDbusItemExport -> use this to export data to the dbus (one value) +# VeDbusService -> use that to create a service and export several values to the dbus + +# Code for VeDbusItemImport is copied from busitem.py and thereafter modified. +# All projects that used busitem.py need to migrate to this package. And some +# projects used to define there own equivalent of VeDbusItemExport. Better to +# use VeDbusItemExport, or even better the VeDbusService class that does it all for you. + +# TODOS +# 1 check for datatypes, it works now, but not sure if all is compliant with +# com.victronenergy.BusItem interface definition. See also the files in +# tests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps +# something similar should also be done in VeDbusBusItemExport? +# 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object? +# 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking +# changes possible. Does everybody first invalidate its data before leaving the bus? +# And what about before taking one object away from the bus, instead of taking the +# whole service offline? +# They should! And after taking one value away, do we need to know that someone left +# the bus? Or we just keep that value in invalidated for ever? Result is that we can't +# see the difference anymore between an invalidated value and a value that was first on +# the bus and later not anymore. See comments above VeDbusItemImport as well. +# 9 there are probably more todos in the code below. + +# Some thoughts with regards to the data types: +# +# Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types +# --- +# Variants are represented by setting the variant_level keyword argument in the +# constructor of any D-Bus data type to a value greater than 0 (variant_level 1 +# means a variant containing some other data type, variant_level 2 means a variant +# containing a variant containing some other data type, and so on). If a non-variant +# is passed as an argument but introspection indicates that a variant is expected, +# it'll automatically be wrapped in a variant. +# --- +# +# Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass +# of Python int. dbus.String is a subclass of Python standard class unicode, etcetera +# +# So all together that explains why we don't need to explicitly convert back and forth +# between the dbus datatypes and the standard python datatypes. Note that all datatypes +# in python are objects. Even an int is an object. + +# The signature of a variant is 'v'. + +# Export ourselves as a D-Bus service. +class VeDbusService(object): + def __init__(self, servicename, bus=None): + # dict containing the VeDbusItemExport objects, with their path as the key. + self._dbusobjects = {} + self._dbusnodes = {} + self._ratelimiters = [] + self._dbusname = None + + # dict containing the onchange callbacks, for each object. Object path is the key + self._onchangecallbacks = {} + + # Connect to session bus whenever present, else use the system bus + self._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus()) + + # make the dbus connection available to outside, could make this a true property instead, but ach.. + self.dbusconn = self._dbusconn + + # Register ourselves on the dbus, trigger an error if already in use (do_not_queue) + self._dbusname = dbus.service.BusName(servicename, self._dbusconn, do_not_queue=True) + + # Add the root item that will return all items as a tree + self._dbusnodes['/'] = VeDbusRootExport(self._dbusconn, '/', self) + + logging.info("registered ourselves on D-Bus as %s" % servicename) + + # To force immediate deregistering of this dbus service and all its object paths, explicitly + # call __del__(). + def __del__(self): + for node in list(self._dbusnodes.values()): + node.__del__() + self._dbusnodes.clear() + for item in list(self._dbusobjects.values()): + item.__del__() + self._dbusobjects.clear() + if self._dbusname: + self._dbusname.__del__() # Forces call to self._bus.release_name(self._name), see source code + self._dbusname = None + + # @param callbackonchange function that will be called when this value is changed. First parameter will + # be the path of the object, second the new value. This callback should return + # True to accept the change, False to reject it. + def add_path(self, path, value, description="", writeable=False, + onchangecallback=None, gettextcallback=None, valuetype=None): + + if onchangecallback is not None: + self._onchangecallbacks[path] = onchangecallback + + item = VeDbusItemExport( + self._dbusconn, path, value, description, writeable, + self._value_changed, gettextcallback, deletecallback=self._item_deleted, valuetype=valuetype) + + spl = path.split('/') + for i in range(2, len(spl)): + subPath = '/'.join(spl[:i]) + if subPath not in self._dbusnodes and subPath not in self._dbusobjects: + self._dbusnodes[subPath] = VeDbusTreeExport(self._dbusconn, subPath, self) + self._dbusobjects[path] = item + logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable)) + + # Add the mandatory paths, as per victron dbus api doc + def add_mandatory_paths(self, processname, processversion, connection, + deviceinstance, productid, productname, firmwareversion, hardwareversion, connected): + self.add_path('/Mgmt/ProcessName', processname) + self.add_path('/Mgmt/ProcessVersion', processversion) + self.add_path('/Mgmt/Connection', connection) + + # Create rest of the mandatory objects + self.add_path('/DeviceInstance', deviceinstance) + self.add_path('/ProductId', productid) + self.add_path('/ProductName', productname) + self.add_path('/FirmwareVersion', firmwareversion) + self.add_path('/HardwareVersion', hardwareversion) + self.add_path('/Connected', connected) + + # Callback function that is called from the VeDbusItemExport objects when a value changes. This function + # maps the change-request to the onchangecallback given to us for this specific path. + def _value_changed(self, path, newvalue): + if path not in self._onchangecallbacks: + return True + + return self._onchangecallbacks[path](path, newvalue) + + def _item_deleted(self, path): + self._dbusobjects.pop(path) + for np in list(self._dbusnodes.keys()): + if np != '/': + for ip in self._dbusobjects: + if ip.startswith(np + '/'): + break + else: + self._dbusnodes[np].__del__() + self._dbusnodes.pop(np) + + def __getitem__(self, path): + return self._dbusobjects[path].local_get_value() + + def __setitem__(self, path, newvalue): + self._dbusobjects[path].local_set_value(newvalue) + + def __delitem__(self, path): + self._dbusobjects[path].__del__() # Invalidates and then removes the object path + assert path not in self._dbusobjects + + def __contains__(self, path): + return path in self._dbusobjects + + def __enter__(self): + l = ServiceContext(self) + self._ratelimiters.append(l) + return l + + def __exit__(self, *exc): + # pop off the top one and flush it. If with statements are nested + # then each exit flushes its own part. + if self._ratelimiters: + self._ratelimiters.pop().flush() + +class ServiceContext(object): + def __init__(self, parent): + self.parent = parent + self.changes = {} + + def __getitem__(self, path): + return self.parent[path] + + def __setitem__(self, path, newvalue): + c = self.parent._dbusobjects[path]._local_set_value(newvalue) + if c is not None: + self.changes[path] = c + + def flush(self): + if self.changes: + self.parent._dbusnodes['/'].ItemsChanged(self.changes) + +class TrackerDict(defaultdict): + """ Same as defaultdict, but passes the key to default_factory. """ + def __missing__(self, key): + self[key] = x = self.default_factory(key) + return x + +class VeDbusRootTracker(object): + """ This tracks the root of a dbus path and listens for PropertiesChanged + signals. When a signal arrives, parse it and unpack the key/value changes + into traditional events, then pass it to the original eventCallback + method. """ + def __init__(self, bus, serviceName): + self.importers = defaultdict(weakref.WeakSet) + self.serviceName = serviceName + self._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal( + "ItemsChanged", weak_functor(self._items_changed_handler)) + + def __del__(self): + self._match.remove() + self._match = None + + def add(self, i): + self.importers[i.path].add(i) + + def _items_changed_handler(self, items): + if not isinstance(items, dict): + return + + for path, changes in items.items(): + try: + v = changes['Value'] + except KeyError: + continue + + try: + t = changes['Text'] + except KeyError: + t = str(unwrap_dbus_value(v)) + + for i in self.importers.get(path, ()): + i._properties_changed_handler({'Value': v, 'Text': t}) + +""" +Importing basics: + - If when we power up, the D-Bus service does not exist, or it does exist and the path does not + yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its + initial value, which VeDbusItemImport will receive and use to update local cache. And, when set, + call the eventCallback. + - If when we power up, save it + - When using get_value, know that there is no difference between services (or object paths) that don't + exist and paths that are invalid (= empty array, see above). Both will return None. In case you do + really want to know ifa path exists or not, use the exists property. + - When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals + with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged- + signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this + class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this + class. + +Read when using this class: +Note that when a service leaves that D-Bus without invalidating all its exported objects first, for +example because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport, +make sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor, +because that takes care of all of that for you. +""" +class VeDbusItemImport(object): + def __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True): + instance = object.__new__(cls) + + # If signal tracking should be done, also add to root tracker + if createsignal: + if "_roots" not in cls.__dict__: + cls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k)) + + return instance + + ## Constructor + # @param bus the bus-object (SESSION or SYSTEM). + # @param serviceName the dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1' + # @param path the object-path, for example '/Dc/V' + # @param eventCallback function that you want to be called on a value change + # @param createSignal only set this to False if you use this function to one time read a value. When + # leaving it to True, make sure to also subscribe to the NameOwnerChanged signal + # elsewhere. See also note some 15 lines up. + def __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True): + # TODO: is it necessary to store _serviceName and _path? Isn't it + # stored in the bus_getobjectsomewhere? + self._serviceName = serviceName + self._path = path + self._match = None + # TODO: _proxy is being used in settingsdevice.py, make a getter for that + self._proxy = bus.get_object(serviceName, path, introspect=False) + self.eventCallback = eventCallback + + assert eventCallback is None or createsignal == True + if createsignal: + self._match = self._proxy.connect_to_signal( + "PropertiesChanged", weak_functor(self._properties_changed_handler)) + self._roots[serviceName].add(self) + + # store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to + # None, same as when a value is invalid + self._cachedvalue = None + try: + v = self._proxy.GetValue() + except dbus.exceptions.DBusException: + pass + else: + self._cachedvalue = unwrap_dbus_value(v) + + def __del__(self): + if self._match is not None: + self._match.remove() + self._match = None + self._proxy = None + + def _refreshcachedvalue(self): + self._cachedvalue = unwrap_dbus_value(self._proxy.GetValue()) + + ## Returns the path as a string, for example '/AC/L1/V' + @property + def path(self): + return self._path + + ## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1 + @property + def serviceName(self): + return self._serviceName + + ## Returns the value of the dbus-item. + # the type will be a dbus variant, for example dbus.Int32(0, variant_level=1) + # this is not a property to keep the name consistant with the com.victronenergy.busitem interface + # returns None when the property is invalid + def get_value(self): + return self._cachedvalue + + ## Writes a new value to the dbus-item + def set_value(self, newvalue): + r = self._proxy.SetValue(wrap_dbus_value(newvalue)) + + # instead of just saving the value, go to the dbus and get it. So we have the right type etc. + if r == 0: + self._refreshcachedvalue() + + return r + + ## Resets the item to its default value + def set_default(self): + self._proxy.SetDefault() + self._refreshcachedvalue() + + ## Returns the text representation of the value. + # For example when the value is an enum/int GetText might return the string + # belonging to that enum value. Another example, for a voltage, GetValue + # would return a float, 12.0Volt, and GetText could return 12 VDC. + # + # Note that this depends on how the dbus-producer has implemented this. + def get_text(self): + return self._proxy.GetText() + + ## Returns true of object path exists, and false if it doesn't + @property + def exists(self): + # TODO: do some real check instead of this crazy thing. + r = False + try: + r = self._proxy.GetValue() + r = True + except dbus.exceptions.DBusException: + pass + + return r + + ## callback for the trigger-event. + # @param eventCallback the event-callback-function. + @property + def eventCallback(self): + return self._eventCallback + + @eventCallback.setter + def eventCallback(self, eventCallback): + self._eventCallback = eventCallback + + ## Is called when the value of the imported bus-item changes. + # Stores the new value in our local cache, and calls the eventCallback, if set. + def _properties_changed_handler(self, changes): + if "Value" in changes: + changes['Value'] = unwrap_dbus_value(changes['Value']) + self._cachedvalue = changes['Value'] + if self._eventCallback: + # The reason behind this try/except is to prevent errors silently ending up the an error + # handler in the dbus code. + try: + self._eventCallback(self._serviceName, self._path, changes) + except: + traceback.print_exc() + os._exit(1) # sys.exit() is not used, since that also throws an exception + + +class VeDbusTreeExport(dbus.service.Object): + def __init__(self, bus, objectPath, service): + dbus.service.Object.__init__(self, bus, objectPath) + self._service = service + logging.debug("VeDbusTreeExport %s has been created" % objectPath) + + def __del__(self): + # self._get_path() will raise an exception when retrieved after the call to .remove_from_connection, + # so we need a copy. + path = self._get_path() + if path is None: + return + self.remove_from_connection() + logging.debug("VeDbusTreeExport %s has been removed" % path) + + def _get_path(self): + if len(self._locations) == 0: + return None + return self._locations[0][1] + + def _get_value_handler(self, path, get_text=False): + logging.debug("_get_value_handler called for %s" % path) + r = {} + px = path + if not px.endswith('/'): + px += '/' + for p, item in self._service._dbusobjects.items(): + if p.startswith(px): + v = item.GetText() if get_text else wrap_dbus_value(item.local_get_value()) + r[p[len(px):]] = v + logging.debug(r) + return r + + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetValue(self): + value = self._get_value_handler(self._get_path()) + return dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1) + + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetText(self): + return self._get_value_handler(self._get_path(), True) + + def local_get_value(self): + return self._get_value_handler(self.path) + +class VeDbusRootExport(VeDbusTreeExport): + @dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}') + def ItemsChanged(self, changes): + pass + + @dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}') + def GetItems(self): + return { + path: { + 'Value': wrap_dbus_value(item.local_get_value()), + 'Text': item.GetText() } + for path, item in self._service._dbusobjects.items() + } + + +class VeDbusItemExport(dbus.service.Object): + ## Constructor of VeDbusItemExport + # + # Use this object to export (publish), values on the dbus + # Creates the dbus-object under the given dbus-service-name. + # @param bus The dbus object. + # @param objectPath The dbus-object-path. + # @param value Value to initialize ourselves with, defaults to None which means Invalid + # @param description String containing a description. Can be called over the dbus with GetDescription() + # @param writeable what would this do!? :). + # @param callback Function that will be called when someone else changes the value of this VeBusItem + # over the dbus. First parameter passed to callback will be our path, second the new + # value. This callback should return True to accept the change, False to reject it. + def __init__(self, bus, objectPath, value=None, description=None, writeable=False, + onchangecallback=None, gettextcallback=None, deletecallback=None, + valuetype=None): + dbus.service.Object.__init__(self, bus, objectPath) + self._onchangecallback = onchangecallback + self._gettextcallback = gettextcallback + self._value = value + self._description = description + self._writeable = writeable + self._deletecallback = deletecallback + self._type = valuetype + + # To force immediate deregistering of this dbus object, explicitly call __del__(). + def __del__(self): + # self._get_path() will raise an exception when retrieved after the + # call to .remove_from_connection, so we need a copy. + path = self._get_path() + if path == None: + return + if self._deletecallback is not None: + self._deletecallback(path) + self.remove_from_connection() + logging.debug("VeDbusItemExport %s has been removed" % path) + + def _get_path(self): + if len(self._locations) == 0: + return None + return self._locations[0][1] + + ## Sets the value. And in case the value is different from what it was, a signal + # will be emitted to the dbus. This function is to be used in the python code that + # is using this class to export values to the dbus. + # set value to None to indicate that it is Invalid + def local_set_value(self, newvalue): + changes = self._local_set_value(newvalue) + if changes is not None: + self.PropertiesChanged(changes) + + def _local_set_value(self, newvalue): + if self._value == newvalue: + return None + + self._value = newvalue + return { + 'Value': wrap_dbus_value(newvalue), + 'Text': self.GetText() + } + + def local_get_value(self): + return self._value + + # ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ==== + + ## Dbus exported method SetValue + # Function is called over the D-Bus by other process. It will first check (via callback) if new + # value is accepted. And it is, stores it and emits a changed-signal. + # @param value The new value. + # @return completion-code When successful a 0 is return, and when not a -1 is returned. + @dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i') + def SetValue(self, newvalue): + if not self._writeable: + return 1 # NOT OK + + newvalue = unwrap_dbus_value(newvalue) + + # If value type is enforced, cast it. If the type can be coerced + # python will do it for us. This allows ints to become floats, + # or bools to become ints. Additionally also allow None, so that + # a path may be invalidated. + if self._type is not None and newvalue is not None: + try: + newvalue = self._type(newvalue) + except (ValueError, TypeError): + return 1 # NOT OK + + if newvalue == self._value: + return 0 # OK + + # call the callback given to us, and check if new value is OK. + if (self._onchangecallback is None or + (self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))): + + self.local_set_value(newvalue) + return 0 # OK + + return 2 # NOT OK + + ## Dbus exported method GetDescription + # + # Returns the a description. + # @param language A language code (e.g. ISO 639-1 en-US). + # @param length Lenght of the language string. + # @return description + @dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s') + def GetDescription(self, language, length): + return self._description if self._description is not None else 'No description given' + + ## Dbus exported method GetValue + # Returns the value. + # @return the value when valid, and otherwise an empty array + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetValue(self): + return wrap_dbus_value(self._value) + + ## Dbus exported method GetText + # Returns the value as string of the dbus-object-path. + # @return text A text-value. '---' when local value is invalid + @dbus.service.method('com.victronenergy.BusItem', out_signature='s') + def GetText(self): + if self._value is None: + return '---' + + # Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we + # have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from + # the application itself, as all data from the D-Bus should have been unwrapped by now. + if self._gettextcallback is None and type(self._value) == dbus.Byte: + return str(int(self._value)) + + if self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId': + return "0x%X" % self._value + + if self._gettextcallback is None: + return str(self._value) + + return self._gettextcallback(self.__dbus_object_path__, self._value) + + ## The signal that indicates that the value has changed. + # Other processes connected to this BusItem object will have subscribed to the + # event when they want to track our state. + @dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}') + def PropertiesChanged(self, changes): + pass + +## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference +## to the object which method is to be called. +## Use this object to break circular references. +class weak_functor: + def __init__(self, f): + self._r = weakref.ref(f.__self__) + self._f = weakref.ref(f.__func__) + + def __call__(self, *args, **kargs): + r = self._r() + f = self._f() + if r == None or f == None: + return + f(r, *args, **kargs) From 6e9085066b3d4c9fe5745fa63c286cbf89488bfe Mon Sep 17 00:00:00 2001 From: Carsten Schmiemann Date: Tue, 30 Aug 2022 02:59:37 +0200 Subject: [PATCH 3/6] Modify dbus python service to read forward and reverse from node red --- .../meter_einspeisung.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/dbus-node-red-meter-einspeisung/meter_einspeisung.py b/dbus-node-red-meter-einspeisung/meter_einspeisung.py index ffa097d..ec646f6 100644 --- a/dbus-node-red-meter-einspeisung/meter_einspeisung.py +++ b/dbus-node-red-meter-einspeisung/meter_einspeisung.py @@ -98,7 +98,6 @@ class NodeRedMeterEinspeisung: #send data to DBus self._dbusservice['/Ac/Power'] = meter_data['einspeisung']['total_power'] # positive: consumption, negative: feed into grid - self._dbusservice['/Ac/Energy/Forward'] = meter_data['einspeisung']['total_kwh']; self._dbusservice['/Ac/Current'] = meter_data['einspeisung']['total_current'] self._dbusservice['/Ac/L1/Voltage'] = meter_data['einspeisung']['l1_voltage'] self._dbusservice['/Ac/L2/Voltage'] = meter_data['einspeisung']['l2_voltage'] @@ -112,9 +111,16 @@ class NodeRedMeterEinspeisung: self._dbusservice['/Ac/L1/Energy/Forward'] = meter_data['einspeisung']['l1_import'] self._dbusservice['/Ac/L2/Energy/Forward'] = meter_data['einspeisung']['l2_import'] self._dbusservice['/Ac/L3/Energy/Forward'] = meter_data['einspeisung']['l3_import'] + self._dbusservice['/Ac/L1/Energy/Reverse'] = meter_data['einspeisung']['l1_export'] + self._dbusservice['/Ac/L2/Energy/Reverse'] = meter_data['einspeisung']['l2_export'] + self._dbusservice['/Ac/L3/Energy/Reverse'] = meter_data['einspeisung']['l3_export'] + self._dbusservice['/Ac/Energy/Forward'] = meter_data['einspeisung']['total_import'] + self._dbusservice['/Ac/Energy/Reverse'] = meter_data['einspeisung']['total_export'] #logging logging.debug("House Consumption (/Ac/Power): %s" % (self._dbusservice['/Ac/Power'])) + logging.debug("House Forward (/Ac/Energy/Forward): %s" % (self._dbusservice['/Ac/Energy/Forward'])) + logging.debug("House Reverse (/Ac/Energy/Revers): %s" % (self._dbusservice['/Ac/Energy/Reverse'])) logging.debug("---"); # increment UpdateIndex - to show that new data is available @@ -165,9 +171,10 @@ def main(): servicename='com.victronenergy.grid', deviceinstance=40, paths={ - '/Ac/Power': {'initial': 0, 'textformat': _w}, - '/Ac/Energy/Forward': {'initial': 0, 'textformat': _kwh}, - '/Ac/Current': {'initial': 0, 'textformat': _a}, + '/Ac/Energy/Forward': {'initial': 0, 'textformat': _kwh}, # energy bought from the grid, summ'd over all phases + '/Ac/Energy/Reverse': {'initial': 0, 'textformat': _kwh}, # energy sold to the grid, summ'd over all phases + '/Ac/Power': {'initial': 0, 'textformat': _w}, # power accumulated over all phases for ess regulation + '/Ac/Current': {'initial': 0, 'textformat': _a}, # current accumulated over all phases '/Ac/L1/Voltage': {'initial': 0, 'textformat': _v}, '/Ac/L2/Voltage': {'initial': 0, 'textformat': _v}, '/Ac/L3/Voltage': {'initial': 0, 'textformat': _v}, @@ -180,6 +187,9 @@ def main(): '/Ac/L1/Energy/Forward': {'initial': 0, 'textformat': _kwh}, '/Ac/L2/Energy/Forward': {'initial': 0, 'textformat': _kwh}, '/Ac/L3/Energy/Forward': {'initial': 0, 'textformat': _kwh}, + '/Ac/L1/Energy/Reverse': {'initial': 0, 'textformat': _kwh}, + '/Ac/L2/Energy/Reverse': {'initial': 0, 'textformat': _kwh}, + '/Ac/L3/Energy/Reverse': {'initial': 0, 'textformat': _kwh}, }) logging.info('Connected to dbus, and switching over to gobject.MainLoop() (= event based)') From 6ad117b2b8b40fb8ba354658e5eaabb0abf09047 Mon Sep 17 00:00:00 2001 From: Carsten Schmiemann Date: Tue, 30 Aug 2022 03:00:34 +0200 Subject: [PATCH 4/6] added calculation for phase combination, only count + and/or - like EM24 to correct VRM display --- node-red-flows/meters.json | 388 +++++++++++++++++++++---------------- 1 file changed, 226 insertions(+), 162 deletions(-) diff --git a/node-red-flows/meters.json b/node-red-flows/meters.json index 99f31ca..2033d13 100644 --- a/node-red-flows/meters.json +++ b/node-red-flows/meters.json @@ -28,8 +28,8 @@ "topic": "", "payload": "", "payloadType": "date", - "x": 120, - "y": 300, + "x": 200, + "y": 400, "wires": [ [ "5c5702e7ebd71a77", @@ -59,8 +59,8 @@ "method": "get", "upload": false, "swaggerDoc": "", - "x": 100, - "y": 760, + "x": 120, + "y": 880, "wires": [ [ "c61fcb67b3a33b47" @@ -71,7 +71,7 @@ "id": "fbe8cca3419d8161", "type": "change", "z": "f96eea4d4a3a345d", - "name": "Set Headers", + "name": "set http header", "rules": [ { "t": "set", @@ -93,8 +93,8 @@ "from": "", "to": "", "reg": false, - "x": 840, - "y": 760, + "x": 1150, + "y": 880, "wires": [ [ "83f39f5649b78b86" @@ -105,26 +105,26 @@ "id": "83f39f5649b78b86", "type": "http response", "z": "f96eea4d4a3a345d", - "name": "", + "name": "serve as http api", "statusCode": "", "headers": {}, - "x": 1080, - "y": 760, + "x": 1420, + "y": 880, "wires": [] }, { "id": "c61fcb67b3a33b47", "type": "function", "z": "f96eea4d4a3a345d", - "name": "Build Object", - "func": "var einspeisung_total_power = parseFloat(global.get(\"einspeisung_total_power\"))\n\nvar einspeisung_l1_voltage = parseFloat(global.get(\"einspeisung_l1_voltage\"))\nvar einspeisung_l2_voltage = parseFloat(global.get(\"einspeisung_l2_voltage\"))\nvar einspeisung_l3_voltage = parseFloat(global.get(\"einspeisung_l3_voltage\"))\n\nvar einspeisung_l1_current = parseFloat(global.get(\"einspeisung_l1_current\"))\nvar einspeisung_l2_current = parseFloat(global.get(\"einspeisung_l2_current\"))\nvar einspeisung_l3_current = parseFloat(global.get(\"einspeisung_l3_current\"))\nvar einspeisung_total_current = einspeisung_l1_current + einspeisung_l2_current + einspeisung_l3_current\n\nvar einspeisung_l1_power = parseFloat(global.get(\"einspeisung_l1_power\"))\nvar einspeisung_l2_power = parseFloat(global.get(\"einspeisung_l2_power\"))\nvar einspeisung_l3_power = parseFloat(global.get(\"einspeisung_l3_power\"))\n\nvar einspeisung_l1_import = parseFloat(global.get(\"einspeisung_l1_import\") - global.get(\"einspeisung_l1_export\"));\nvar einspeisung_l2_import = parseFloat(global.get(\"einspeisung_l2_import\") - global.get(\"einspeisung_l2_export\"));\nvar einspeisung_l3_import = parseFloat(global.get(\"einspeisung_l3_import\") - global.get(\"einspeisung_l3_export\"));\n\nvar einspeisung_total_kWh = einspeisung_l1_import + einspeisung_l2_import + einspeisung_l3_import;\n\nmsg.payload.einspeisung = {total_kwh: einspeisung_total_kWh, total_power:einspeisung_total_power, total_current:einspeisung_total_current, l1_voltage:einspeisung_l1_voltage, l2_voltage:einspeisung_l2_voltage, l3_voltage:einspeisung_l3_voltage, l1_current:einspeisung_l1_current, l2_current:einspeisung_l2_current, l3_current:einspeisung_l3_current, l1_power:einspeisung_l1_power, l2_power:einspeisung_l2_power, l3_power:einspeisung_l3_power, l1_import:einspeisung_l1_import, l2_import:einspeisung_l2_import, l3_import:einspeisung_l3_import};\n\nreturn msg;\n", + "name": "Create java objects", + "func": "var einspeisung_total_power = parseFloat(global.get(\"einspeisung_total_power\"))\n\nvar einspeisung_l1_voltage = parseFloat(global.get(\"einspeisung_l1_voltage\"))\nvar einspeisung_l2_voltage = parseFloat(global.get(\"einspeisung_l2_voltage\"))\nvar einspeisung_l3_voltage = parseFloat(global.get(\"einspeisung_l3_voltage\"))\n\nvar einspeisung_l1_current = parseFloat(global.get(\"einspeisung_l1_current\"))\nvar einspeisung_l2_current = parseFloat(global.get(\"einspeisung_l2_current\"))\nvar einspeisung_l3_current = parseFloat(global.get(\"einspeisung_l3_current\"))\n\nvar einspeisung_l1_power = parseFloat(global.get(\"einspeisung_l1_power\"))\nvar einspeisung_l2_power = parseFloat(global.get(\"einspeisung_l2_power\"))\nvar einspeisung_l3_power = parseFloat(global.get(\"einspeisung_l3_power\"))\n\nvar einspeisung_l1_import = parseFloat(global.get(\"einspeisung_l1_import\"))\nvar einspeisung_l2_import = parseFloat(global.get(\"einspeisung_l2_import\"))\nvar einspeisung_l3_import = parseFloat(global.get(\"einspeisung_l3_import\"))\n\nvar einspeisung_l1_export = parseFloat(global.get(\"einspeisung_l1_export\"))\nvar einspeisung_l2_export = parseFloat(global.get(\"einspeisung_l2_export\"))\nvar einspeisung_l3_export = parseFloat(global.get(\"einspeisung_l3_export\"))\n\n//Totals\nvar einspeisung_total_current = einspeisung_l1_current + einspeisung_l2_current + einspeisung_l3_current\nvar einspeisung_total_import = parseFloat(global.get(\"einspeisung_total_import\"))\nvar einspeisung_total_export = parseFloat(global.get(\"einspeisung_total_export\"))\n\nmsg.payload.einspeisung = {total_power:einspeisung_total_power, total_current:einspeisung_total_current, total_import:einspeisung_total_import, total_export:einspeisung_total_export, l1_voltage:einspeisung_l1_voltage, l2_voltage:einspeisung_l2_voltage, l3_voltage:einspeisung_l3_voltage, l1_current:einspeisung_l1_current, l2_current:einspeisung_l2_current, l3_current:einspeisung_l3_current, l1_power:einspeisung_l1_power, l2_power:einspeisung_l2_power, l3_power:einspeisung_l3_power, l1_import:einspeisung_l1_import, l2_import:einspeisung_l2_import, l3_import:einspeisung_l3_import, l1_export:einspeisung_l1_export, l2_export:einspeisung_l2_export, l3_export:einspeisung_l3_export};\n\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 460, - "y": 760, + "x": 510, + "y": 880, "wires": [ [ "307db2619dcf6228" @@ -135,12 +135,12 @@ "id": "307db2619dcf6228", "type": "json", "z": "f96eea4d4a3a345d", - "name": "", + "name": "build json object", "property": "payload", "action": "", "pretty": false, - "x": 660, - "y": 760, + "x": 910, + "y": 880, "wires": [ [ "fbe8cca3419d8161" @@ -161,8 +161,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 320, - "y": 100, + "x": 560, + "y": 140, "wires": [ [ "f5853a536891d509" @@ -180,8 +180,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 700, - "y": 100, + "x": 940, + "y": 140, "wires": [ [] ] @@ -200,8 +200,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 320, - "y": 140, + "x": 560, + "y": 180, "wires": [ [ "a3b33ef1225b85e7" @@ -219,8 +219,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 700, - "y": 140, + "x": 940, + "y": 180, "wires": [ [] ] @@ -239,8 +239,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 320, - "y": 180, + "x": 560, + "y": 220, "wires": [ [ "b284d02cb539994f" @@ -258,8 +258,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 700, - "y": 180, + "x": 940, + "y": 220, "wires": [ [] ] @@ -278,8 +278,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 320, - "y": 220, + "x": 560, + "y": 260, "wires": [ [ "1da7740caa290264" @@ -297,8 +297,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 700, - "y": 220, + "x": 940, + "y": 260, "wires": [ [] ] @@ -317,8 +317,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 320, - "y": 260, + "x": 560, + "y": 300, "wires": [ [ "6c9ebd253a3c3b0d" @@ -336,8 +336,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 700, - "y": 260, + "x": 940, + "y": 300, "wires": [ [] ] @@ -356,8 +356,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 320, - "y": 300, + "x": 560, + "y": 340, "wires": [ [ "d848102461062b3f" @@ -375,8 +375,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 700, - "y": 300, + "x": 940, + "y": 340, "wires": [ [] ] @@ -395,8 +395,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 310, - "y": 340, + "x": 550, + "y": 380, "wires": [ [ "3e88cef8b8b508ac" @@ -414,8 +414,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 700, - "y": 340, + "x": 940, + "y": 380, "wires": [ [] ] @@ -434,8 +434,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 310, - "y": 380, + "x": 550, + "y": 420, "wires": [ [ "466e66a05eddb62f" @@ -453,8 +453,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 700, - "y": 380, + "x": 940, + "y": 420, "wires": [ [] ] @@ -473,8 +473,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 310, - "y": 420, + "x": 550, + "y": 460, "wires": [ [ "a0e6a154bfb85c09" @@ -492,8 +492,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 700, - "y": 420, + "x": 940, + "y": 460, "wires": [ [] ] @@ -512,7 +512,7 @@ "proxy": "", "authType": "", "senderr": false, - "x": 330, + "x": 560, "y": 40, "wires": [ [ @@ -531,7 +531,7 @@ "initialize": "", "finalize": "", "libs": [], - "x": 710, + "x": 940, "y": 40, "wires": [ [] @@ -551,8 +551,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 310, - "y": 460, + "x": 550, + "y": 500, "wires": [ [ "22bdca2522a4b3c9" @@ -570,8 +570,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 700, - "y": 460, + "x": 940, + "y": 500, "wires": [ [] ] @@ -590,8 +590,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 310, - "y": 500, + "x": 550, + "y": 540, "wires": [ [ "51dc86ef4b4e31fd" @@ -609,8 +609,8 @@ "initialize": "", "finalize": "", "libs": [], - "x": 700, - "y": 500, + "x": 940, + "y": 540, "wires": [ [] ] @@ -629,8 +629,8 @@ "proxy": "", "authType": "", "senderr": false, - "x": 310, - "y": 540, + "x": 550, + "y": 580, "wires": [ [ "804899fa9686be16" @@ -648,8 +648,125 @@ "initialize": "", "finalize": "", "libs": [], - "x": 700, - "y": 540, + "x": 940, + "y": 580, + "wires": [ + [] + ] + }, + { + "id": "a5f8ee01c537330b", + "type": "http request", + "z": "f96eea4d4a3a345d", + "name": "Export L1", + "method": "GET", + "ret": "txt", + "paytoqs": "ignore", + "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R150,d", + "tls": "", + "persist": false, + "proxy": "", + "authType": "", + "senderr": false, + "x": 550, + "y": 620, + "wires": [ + [ + "ba0021b15f94fc41" + ] + ] + }, + { + "id": "ba0021b15f94fc41", + "type": "function", + "z": "f96eea4d4a3a345d", + "name": "format and save to global var", + "func": "temp = parseFloat(msg.payload * 0.001).toFixed(2);\nglobal.set(\"einspeisung_l1_export\", temp)\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 940, + "y": 620, + "wires": [ + [] + ] + }, + { + "id": "1e1823db7c7b4edd", + "type": "http request", + "z": "f96eea4d4a3a345d", + "name": "Export L2", + "method": "GET", + "ret": "txt", + "paytoqs": "ignore", + "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R151,d", + "tls": "", + "persist": false, + "proxy": "", + "authType": "", + "senderr": false, + "x": 550, + "y": 660, + "wires": [ + [ + "68157d83b4f176f5" + ] + ] + }, + { + "id": "68157d83b4f176f5", + "type": "function", + "z": "f96eea4d4a3a345d", + "name": "format and save to global var", + "func": "temp = parseFloat(msg.payload * 0.001).toFixed(2);\nglobal.set(\"einspeisung_l2_export\", temp)\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 940, + "y": 660, + "wires": [ + [] + ] + }, + { + "id": "7857cce5b19d21ea", + "type": "http request", + "z": "f96eea4d4a3a345d", + "name": "Export L3", + "method": "GET", + "ret": "txt", + "paytoqs": "ignore", + "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R152,d", + "tls": "", + "persist": false, + "proxy": "", + "authType": "", + "senderr": false, + "x": 550, + "y": 700, + "wires": [ + [ + "4d12b6c71b023fc8" + ] + ] + }, + { + "id": "4d12b6c71b023fc8", + "type": "function", + "z": "f96eea4d4a3a345d", + "name": "format and save to global var", + "func": "temp = parseFloat(msg.payload * 0.001).toFixed(2);\nglobal.set(\"einspeisung_l3_export\", temp)\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 940, + "y": 700, "wires": [ [] ] @@ -675,7 +792,7 @@ "topic": "", "payload": "", "payloadType": "date", - "x": 120, + "x": 190, "y": 40, "wires": [ [ @@ -684,120 +801,67 @@ ] }, { - "id": "a5f8ee01c537330b", - "type": "http request", + "id": "18e719ada7da042a", + "type": "inject", "z": "f96eea4d4a3a345d", - "name": "Export L1", - "method": "GET", - "ret": "txt", - "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R150,d", - "tls": "", - "persist": false, - "proxy": "", - "authType": "", - "senderr": false, - "x": 310, - "y": 580, + "name": "Calc every minute", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "60", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "payload": "", + "payloadType": "date", + "x": 190, + "y": 760, "wires": [ [ - "ba0021b15f94fc41" + "983943d318a0ef33" ] ] }, { - "id": "ba0021b15f94fc41", + "id": "983943d318a0ef33", "type": "function", "z": "f96eea4d4a3a345d", - "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"einspeisung_l1_export\", temp)\nreturn msg;", + "name": "Phase sum of import and export like EM24", + "func": "//init counters if not defined\nif (global.get(\"einspeisung_total_import\") === undefined) {\n global.set(\"einspeisung_total_import\", 0);\n}\nif (global.get(\"einspeisung_total_export\") === undefined) {\n global.set(\"einspeisung_total_export\", 0);\n}\n//get counters from global variable\nvar total_import = parseFloat(global.get(\"einspeisung_total_import\"));\nvar total_export = parseFloat(global.get(\"einspeisung_total_export\"));\nvar phase_combined_last = parseFloat(global.get(\"einspeisung_total_phase_combined_last\"));\nvar check = \"Nothing\";\n\n//get measurements\nvar einspeisung_l1_import = parseFloat(global.get(\"einspeisung_l1_import\"));\nvar einspeisung_l2_import = parseFloat(global.get(\"einspeisung_l2_import\"));\nvar einspeisung_l3_import = parseFloat(global.get(\"einspeisung_l3_import\"));\n\nvar einspeisung_l1_export = parseFloat(global.get(\"einspeisung_l1_export\"));\nvar einspeisung_l2_export = parseFloat(global.get(\"einspeisung_l2_export\"));\nvar einspeisung_l3_export = parseFloat(global.get(\"einspeisung_l3_export\"));\n\n//get measurement of import and export\nvar phase_combined = (einspeisung_l1_import - einspeisung_l1_export) + (einspeisung_l2_import - einspeisung_l2_export) + (einspeisung_l3_import - einspeisung_l3_export);\n\nif (phase_combined < phase_combined_last) {\n total_export += Math.abs(phase_combined - phase_combined_last);\n check = \"Export: \" + Math.abs(phase_combined - phase_combined_last) + \" kWh\";\n}\nif (phase_combined > phase_combined_last) {\n total_import += Math.abs(phase_combined - phase_combined_last);\n check = \"Import: \" + Math.abs(phase_combined - phase_combined_last) + \" kWh\";\n}\n\nglobal.set(\"einspeisung_total_phase_combined_last\", phase_combined);\nglobal.set(\"einspeisung_total_import\", parseFloat(total_import).toFixed(2));\nglobal.set(\"einspeisung_total_export\", parseFloat(total_export).toFixed(2));\nmsg.payload = {total_import: total_import, total_export: total_export, phase_combined: phase_combined, phase_combined_last: phase_combined_last, check: check}\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 700, - "y": 580, - "wires": [ - [] - ] - }, - { - "id": "1e1823db7c7b4edd", - "type": "http request", - "z": "f96eea4d4a3a345d", - "name": "Export L2", - "method": "GET", - "ret": "txt", - "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R151,d", - "tls": "", - "persist": false, - "proxy": "", - "authType": "", - "senderr": false, - "x": 310, - "y": 620, + "x": 960, + "y": 760, "wires": [ [ - "68157d83b4f176f5" + "9b291dc80aff0a1a" ] ] }, { - "id": "68157d83b4f176f5", - "type": "function", + "id": "9b291dc80aff0a1a", + "type": "debug", "z": "f96eea4d4a3a345d", - "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"einspeisung_l2_export\", temp)\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 700, - "y": 620, - "wires": [ - [] - ] - }, - { - "id": "7857cce5b19d21ea", - "type": "http request", - "z": "f96eea4d4a3a345d", - "name": "Export L3", - "method": "GET", - "ret": "txt", - "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R152,d", - "tls": "", - "persist": false, - "proxy": "", - "authType": "", - "senderr": false, - "x": 310, - "y": 660, - "wires": [ - [ - "4d12b6c71b023fc8" - ] - ] - }, - { - "id": "4d12b6c71b023fc8", - "type": "function", - "z": "f96eea4d4a3a345d", - "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(1);\nglobal.set(\"einspeisung_l3_export\", temp)\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 700, - "y": 660, - "wires": [ - [] - ] + "name": "", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 1460, + "y": 760, + "wires": [] } ] \ No newline at end of file From a39ed57c7e3761a4a3c3b1417e9862478fe4550d Mon Sep 17 00:00:00 2001 From: Carsten Schmiemann Date: Tue, 30 Aug 2022 12:00:53 +0200 Subject: [PATCH 5/6] Create all meters from power, and reset to zero, because VRM gets confused --- node-red-flows/meters.json | 336 ++++++++++++++++++------------------- 1 file changed, 160 insertions(+), 176 deletions(-) diff --git a/node-red-flows/meters.json b/node-red-flows/meters.json index 2033d13..1d54d6f 100644 --- a/node-red-flows/meters.json +++ b/node-red-flows/meters.json @@ -28,7 +28,7 @@ "topic": "", "payload": "", "payloadType": "date", - "x": 200, + "x": 180, "y": 400, "wires": [ [ @@ -40,13 +40,7 @@ "972360a051abdf88", "21e06b0168fdee41", "e107fc40d203f46a", - "7586c0c0ec778dad", - "ef783dcc259e1398", - "f748fa2266fa4302", - "ece76e52d325bd6e", - "a5f8ee01c537330b", - "1e1823db7c7b4edd", - "7857cce5b19d21ea" + "7586c0c0ec778dad" ] ] }, @@ -408,7 +402,7 @@ "type": "function", "z": "f96eea4d4a3a345d", "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.1).toFixed(1);\nglobal.set(\"einspeisung_l1_power\", temp)\nreturn msg;", + "func": "temp = parseFloat(msg.payload * 0.1).toFixed(1);\nglobal.set(\"einspeisung_l1_power\", temp);\nmsg.payload = parseFloat(temp);\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", @@ -417,7 +411,9 @@ "x": 940, "y": 380, "wires": [ - [] + [ + "1cdb84780936c696" + ] ] }, { @@ -447,7 +443,7 @@ "type": "function", "z": "f96eea4d4a3a345d", "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.1).toFixed(1);\nglobal.set(\"einspeisung_l2_power\", temp)\nreturn msg;", + "func": "temp = parseFloat(msg.payload * 0.1).toFixed(1);\nglobal.set(\"einspeisung_l2_power\", temp);\nmsg.payload = parseFloat(temp);\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", @@ -456,7 +452,9 @@ "x": 940, "y": 420, "wires": [ - [] + [ + "074a7fdb9ba59d27" + ] ] }, { @@ -486,7 +484,7 @@ "type": "function", "z": "f96eea4d4a3a345d", "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.1).toFixed(1);\nglobal.set(\"einspeisung_l3_power\", temp)\nreturn msg;", + "func": "temp = parseFloat(msg.payload * 0.1).toFixed(1);\nglobal.set(\"einspeisung_l3_power\", temp);\nmsg.payload = parseFloat(temp);\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", @@ -495,7 +493,9 @@ "x": 940, "y": 460, "wires": [ - [] + [ + "6ff639af913d0a4d" + ] ] }, { @@ -525,7 +525,7 @@ "type": "function", "z": "f96eea4d4a3a345d", "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.1).toFixed(1);\nglobal.set(\"einspeisung_total_power\", temp)\nreturn msg;", + "func": "temp = parseFloat(msg.payload * 0.1).toFixed(1);\nglobal.set(\"einspeisung_total_power\", temp);\nmsg.payload = parseFloat(temp);\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", @@ -533,29 +533,9 @@ "libs": [], "x": 940, "y": 40, - "wires": [ - [] - ] - }, - { - "id": "ef783dcc259e1398", - "type": "http request", - "z": "f96eea4d4a3a345d", - "name": "Import L1", - "method": "GET", - "ret": "txt", - "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R117,d", - "tls": "", - "persist": false, - "proxy": "", - "authType": "", - "senderr": false, - "x": 550, - "y": 500, "wires": [ [ - "22bdca2522a4b3c9" + "1504cb346fc652fd" ] ] }, @@ -576,28 +556,6 @@ [] ] }, - { - "id": "f748fa2266fa4302", - "type": "http request", - "z": "f96eea4d4a3a345d", - "name": "Import L2", - "method": "GET", - "ret": "txt", - "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R118,d", - "tls": "", - "persist": false, - "proxy": "", - "authType": "", - "senderr": false, - "x": 550, - "y": 540, - "wires": [ - [ - "51dc86ef4b4e31fd" - ] - ] - }, { "id": "51dc86ef4b4e31fd", "type": "function", @@ -615,28 +573,6 @@ [] ] }, - { - "id": "ece76e52d325bd6e", - "type": "http request", - "z": "f96eea4d4a3a345d", - "name": "Import L3", - "method": "GET", - "ret": "txt", - "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R119,d", - "tls": "", - "persist": false, - "proxy": "", - "authType": "", - "senderr": false, - "x": 550, - "y": 580, - "wires": [ - [ - "804899fa9686be16" - ] - ] - }, { "id": "804899fa9686be16", "type": "function", @@ -654,28 +590,6 @@ [] ] }, - { - "id": "a5f8ee01c537330b", - "type": "http request", - "z": "f96eea4d4a3a345d", - "name": "Export L1", - "method": "GET", - "ret": "txt", - "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R150,d", - "tls": "", - "persist": false, - "proxy": "", - "authType": "", - "senderr": false, - "x": 550, - "y": 620, - "wires": [ - [ - "ba0021b15f94fc41" - ] - ] - }, { "id": "ba0021b15f94fc41", "type": "function", @@ -693,28 +607,6 @@ [] ] }, - { - "id": "1e1823db7c7b4edd", - "type": "http request", - "z": "f96eea4d4a3a345d", - "name": "Export L2", - "method": "GET", - "ret": "txt", - "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R151,d", - "tls": "", - "persist": false, - "proxy": "", - "authType": "", - "senderr": false, - "x": 550, - "y": 660, - "wires": [ - [ - "68157d83b4f176f5" - ] - ] - }, { "id": "68157d83b4f176f5", "type": "function", @@ -732,28 +624,6 @@ [] ] }, - { - "id": "7857cce5b19d21ea", - "type": "http request", - "z": "f96eea4d4a3a345d", - "name": "Export L3", - "method": "GET", - "ret": "txt", - "paytoqs": "ignore", - "url": "http://10.1.0.5/cgi-bin/readVal.exe?PDP,%20R152,d", - "tls": "", - "persist": false, - "proxy": "", - "authType": "", - "senderr": false, - "x": 550, - "y": 700, - "wires": [ - [ - "4d12b6c71b023fc8" - ] - ] - }, { "id": "4d12b6c71b023fc8", "type": "function", @@ -775,7 +645,7 @@ "id": "58942376d9fbdd07", "type": "inject", "z": "f96eea4d4a3a345d", - "name": "Poll every sec", + "name": "Poll every two sec", "props": [ { "p": "payload" @@ -785,14 +655,14 @@ "vt": "str" } ], - "repeat": "1", + "repeat": "2", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", - "x": 190, + "x": 200, "y": 40, "wires": [ [ @@ -801,10 +671,60 @@ ] }, { - "id": "18e719ada7da042a", + "id": "983943d318a0ef33", + "type": "function", + "z": "f96eea4d4a3a345d", + "name": "Calculate kWh of sum power", + "func": "//init counters if not defined\nif (global.get(\"einspeisung_total_import\") === undefined) {\n global.set(\"einspeisung_total_import\", 0);\n}\nif (global.get(\"einspeisung_total_export\") === undefined) {\n global.set(\"einspeisung_total_export\", 0);\n}\n//get counters from global variable\nvar total_import = global.get(\"einspeisung_total_import\");\nvar total_export = global.get(\"einspeisung_total_export\");\nvar check = \"Nothing\";\n\nif (msg.payload < 0) {\n total_export += Math.abs(msg.payload);\n check = \"Export: \" + Math.abs(msg.payload) + \" kWh\";\n}\nif (msg.payload > 0) {\n total_import += Math.abs(msg.payload);\n check = \"Import: \" + Math.abs(msg.payload) + \" kWh\";\n}\n\nglobal.set(\"einspeisung_total_import\", total_import);\nglobal.set(\"einspeisung_total_export\", total_export);\nmsg.payload = {total_import: total_import, total_export: total_export, check: check}\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1530, + "y": 40, + "wires": [ + [] + ] + }, + { + "id": "1504cb346fc652fd", + "type": "watt2kwh", + "z": "f96eea4d4a3a345d", + "format": "kwh", + "maximum": "10", + "maximumunit": "mins", + "name": "", + "x": 1250, + "y": 40, + "wires": [ + [ + "983943d318a0ef33" + ] + ] + }, + { + "id": "e621c16c10a5eb1d", + "type": "function", + "z": "f96eea4d4a3a345d", + "name": "Init variables", + "func": "global.set(\"einspeisung_total_import\", 0);\nglobal.set(\"einspeisung_total_export\", 0);\nglobal.set(\"einspeisung_l1_import\", 0);\nglobal.set(\"einspeisung_l2_import\", 0);\nglobal.set(\"einspeisung_l3_import\", 0);\nglobal.set(\"einspeisung_l1_export\", 0);\nglobal.set(\"einspeisung_l2_export\", 0);\nglobal.set(\"einspeisung_l3_export\", 0);\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1440, + "y": 580, + "wires": [ + [] + ] + }, + { + "id": "d905c25120cfdfd3", "type": "inject", "z": "f96eea4d4a3a345d", - "name": "Calc every minute", + "name": "Inject once", "props": [ { "p": "payload" @@ -814,54 +734,118 @@ "vt": "str" } ], - "repeat": "60", + "repeat": "", "crontab": "", "once": false, "onceDelay": 0.1, "topic": "", "payload": "", "payloadType": "date", - "x": 190, - "y": 760, + "x": 1250, + "y": 580, "wires": [ [ - "983943d318a0ef33" + "e621c16c10a5eb1d" ] ] }, { - "id": "983943d318a0ef33", + "id": "1cdb84780936c696", + "type": "watt2kwh", + "z": "f96eea4d4a3a345d", + "format": "kwh", + "maximum": "10", + "maximumunit": "mins", + "name": "", + "x": 1230, + "y": 380, + "wires": [ + [ + "0b9537e05d7149d3" + ] + ] + }, + { + "id": "074a7fdb9ba59d27", + "type": "watt2kwh", + "z": "f96eea4d4a3a345d", + "format": "kwh", + "maximum": "10", + "maximumunit": "mins", + "name": "", + "x": 1230, + "y": 420, + "wires": [ + [ + "59bf60635bd06584" + ] + ] + }, + { + "id": "6ff639af913d0a4d", + "type": "watt2kwh", + "z": "f96eea4d4a3a345d", + "format": "kwh", + "maximum": "10", + "maximumunit": "mins", + "name": "", + "x": 1230, + "y": 460, + "wires": [ + [ + "87d6bf141ff90325" + ] + ] + }, + { + "id": "0b9537e05d7149d3", "type": "function", "z": "f96eea4d4a3a345d", - "name": "Phase sum of import and export like EM24", - "func": "//init counters if not defined\nif (global.get(\"einspeisung_total_import\") === undefined) {\n global.set(\"einspeisung_total_import\", 0);\n}\nif (global.get(\"einspeisung_total_export\") === undefined) {\n global.set(\"einspeisung_total_export\", 0);\n}\n//get counters from global variable\nvar total_import = parseFloat(global.get(\"einspeisung_total_import\"));\nvar total_export = parseFloat(global.get(\"einspeisung_total_export\"));\nvar phase_combined_last = parseFloat(global.get(\"einspeisung_total_phase_combined_last\"));\nvar check = \"Nothing\";\n\n//get measurements\nvar einspeisung_l1_import = parseFloat(global.get(\"einspeisung_l1_import\"));\nvar einspeisung_l2_import = parseFloat(global.get(\"einspeisung_l2_import\"));\nvar einspeisung_l3_import = parseFloat(global.get(\"einspeisung_l3_import\"));\n\nvar einspeisung_l1_export = parseFloat(global.get(\"einspeisung_l1_export\"));\nvar einspeisung_l2_export = parseFloat(global.get(\"einspeisung_l2_export\"));\nvar einspeisung_l3_export = parseFloat(global.get(\"einspeisung_l3_export\"));\n\n//get measurement of import and export\nvar phase_combined = (einspeisung_l1_import - einspeisung_l1_export) + (einspeisung_l2_import - einspeisung_l2_export) + (einspeisung_l3_import - einspeisung_l3_export);\n\nif (phase_combined < phase_combined_last) {\n total_export += Math.abs(phase_combined - phase_combined_last);\n check = \"Export: \" + Math.abs(phase_combined - phase_combined_last) + \" kWh\";\n}\nif (phase_combined > phase_combined_last) {\n total_import += Math.abs(phase_combined - phase_combined_last);\n check = \"Import: \" + Math.abs(phase_combined - phase_combined_last) + \" kWh\";\n}\n\nglobal.set(\"einspeisung_total_phase_combined_last\", phase_combined);\nglobal.set(\"einspeisung_total_import\", parseFloat(total_import).toFixed(2));\nglobal.set(\"einspeisung_total_export\", parseFloat(total_export).toFixed(2));\nmsg.payload = {total_import: total_import, total_export: total_export, phase_combined: phase_combined, phase_combined_last: phase_combined_last, check: check}\nreturn msg;", + "name": "Calculate kWh of phase 1 power", + "func": "//init counters if not defined\nif (global.get(\"einspeisung_l1_import\") === undefined) {\n global.set(\"einspeisung_l1_import\", 0);\n}\nif (global.get(\"einspeisung_l1_export\") === undefined) {\n global.set(\"einspeisung_l1_export\", 0);\n}\n//get counters from global variable\nvar total_import = global.get(\"einspeisung_l1_import\");\nvar total_export = global.get(\"einspeisung_l1_export\");\nvar check = \"Nothing\";\n\nif (msg.payload < 0) {\n total_export += Math.abs(msg.payload);\n check = \"Export: \" + Math.abs(msg.payload) + \" kWh\";\n}\nif (msg.payload > 0) {\n total_import += Math.abs(msg.payload);\n check = \"Import: \" + Math.abs(msg.payload) + \" kWh\";\n}\n\nglobal.set(\"einspeisung_l1_import\", total_import);\nglobal.set(\"einspeisung_l1_export\", total_export);\nmsg.payload = {total_import: total_import, total_export: total_export, check: check}\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 960, - "y": 760, + "x": 1500, + "y": 380, "wires": [ - [ - "9b291dc80aff0a1a" - ] + [] ] }, { - "id": "9b291dc80aff0a1a", - "type": "debug", + "id": "59bf60635bd06584", + "type": "function", "z": "f96eea4d4a3a345d", - "name": "", - "active": true, - "tosidebar": true, - "console": false, - "tostatus": false, - "complete": "false", - "statusVal": "", - "statusType": "auto", - "x": 1460, - "y": 760, - "wires": [] + "name": "Calculate kWh of phase 2 power", + "func": "//init counters if not defined\nif (global.get(\"einspeisung_l2_import\") === undefined) {\n global.set(\"einspeisung_l2_import\", 0);\n}\nif (global.get(\"einspeisung_l2_export\") === undefined) {\n global.set(\"einspeisung_l2_export\", 0);\n}\n//get counters from global variable\nvar total_import = global.get(\"einspeisung_l2_import\");\nvar total_export = global.get(\"einspeisung_l2_export\");\nvar check = \"Nothing\";\n\nif (msg.payload < 0) {\n total_export += Math.abs(msg.payload);\n check = \"Export: \" + Math.abs(msg.payload) + \" kWh\";\n}\nif (msg.payload > 0) {\n total_import += Math.abs(msg.payload);\n check = \"Import: \" + Math.abs(msg.payload) + \" kWh\";\n}\n\nglobal.set(\"einspeisung_l2_import\", total_import);\nglobal.set(\"einspeisung_l2_export\", total_export);\nmsg.payload = {total_import: total_import, total_export: total_export, check: check}\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1500, + "y": 420, + "wires": [ + [] + ] + }, + { + "id": "87d6bf141ff90325", + "type": "function", + "z": "f96eea4d4a3a345d", + "name": "Calculate kWh of phase 3 power", + "func": "//init counters if not defined\nif (global.get(\"einspeisung_l3_import\") === undefined) {\n global.set(\"einspeisung_l3_import\", 0);\n}\nif (global.get(\"einspeisung_l3_export\") === undefined) {\n global.set(\"einspeisung_l3_export\", 0);\n}\n//get counters from global variable\nvar total_import = global.get(\"einspeisung_l3_import\");\nvar total_export = global.get(\"einspeisung_l3_export\");\nvar check = \"Nothing\";\n\nif (msg.payload < 0) {\n total_export += Math.abs(msg.payload);\n check = \"Export: \" + Math.abs(msg.payload) + \" kWh\";\n}\nif (msg.payload > 0) {\n total_import += Math.abs(msg.payload);\n check = \"Import: \" + Math.abs(msg.payload) + \" kWh\";\n}\n\nglobal.set(\"einspeisung_l3_import\", total_import);\nglobal.set(\"einspeisung_l3_export\", total_export);\nmsg.payload = {total_import: total_import, total_export: total_export, check: check}\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1500, + "y": 460, + "wires": [ + [] + ] } ] \ No newline at end of file From 9e9556b8ea9ed4f5376e8ecf3e9d6f1f80b2824a Mon Sep 17 00:00:00 2001 From: Carsten Schmiemann Date: Wed, 31 Aug 2022 00:05:03 +0200 Subject: [PATCH 6/6] Clean up and add comments --- node-red-flows/meters.json | 156 +++++++++++-------------------------- 1 file changed, 47 insertions(+), 109 deletions(-) diff --git a/node-red-flows/meters.json b/node-red-flows/meters.json index 1d54d6f..79be5ba 100644 --- a/node-red-flows/meters.json +++ b/node-red-flows/meters.json @@ -539,108 +539,6 @@ ] ] }, - { - "id": "22bdca2522a4b3c9", - "type": "function", - "z": "f96eea4d4a3a345d", - "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(2);\nglobal.set(\"einspeisung_l1_import\", temp)\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 940, - "y": 500, - "wires": [ - [] - ] - }, - { - "id": "51dc86ef4b4e31fd", - "type": "function", - "z": "f96eea4d4a3a345d", - "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(2);\nglobal.set(\"einspeisung_l2_import\", temp)\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 940, - "y": 540, - "wires": [ - [] - ] - }, - { - "id": "804899fa9686be16", - "type": "function", - "z": "f96eea4d4a3a345d", - "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(2);\nglobal.set(\"einspeisung_l3_import\", temp)\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 940, - "y": 580, - "wires": [ - [] - ] - }, - { - "id": "ba0021b15f94fc41", - "type": "function", - "z": "f96eea4d4a3a345d", - "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(2);\nglobal.set(\"einspeisung_l1_export\", temp)\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 940, - "y": 620, - "wires": [ - [] - ] - }, - { - "id": "68157d83b4f176f5", - "type": "function", - "z": "f96eea4d4a3a345d", - "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(2);\nglobal.set(\"einspeisung_l2_export\", temp)\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 940, - "y": 660, - "wires": [ - [] - ] - }, - { - "id": "4d12b6c71b023fc8", - "type": "function", - "z": "f96eea4d4a3a345d", - "name": "format and save to global var", - "func": "temp = parseFloat(msg.payload * 0.001).toFixed(2);\nglobal.set(\"einspeisung_l3_export\", temp)\nreturn msg;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 940, - "y": 700, - "wires": [ - [] - ] - }, { "id": "58942376d9fbdd07", "type": "inject", @@ -675,7 +573,7 @@ "type": "function", "z": "f96eea4d4a3a345d", "name": "Calculate kWh of sum power", - "func": "//init counters if not defined\nif (global.get(\"einspeisung_total_import\") === undefined) {\n global.set(\"einspeisung_total_import\", 0);\n}\nif (global.get(\"einspeisung_total_export\") === undefined) {\n global.set(\"einspeisung_total_export\", 0);\n}\n//get counters from global variable\nvar total_import = global.get(\"einspeisung_total_import\");\nvar total_export = global.get(\"einspeisung_total_export\");\nvar check = \"Nothing\";\n\nif (msg.payload < 0) {\n total_export += Math.abs(msg.payload);\n check = \"Export: \" + Math.abs(msg.payload) + \" kWh\";\n}\nif (msg.payload > 0) {\n total_import += Math.abs(msg.payload);\n check = \"Import: \" + Math.abs(msg.payload) + \" kWh\";\n}\n\nglobal.set(\"einspeisung_total_import\", total_import);\nglobal.set(\"einspeisung_total_export\", total_export);\nmsg.payload = {total_import: total_import, total_export: total_export, check: check}\nreturn msg;", + "func": "//init counters if not defined\nif (global.get(\"einspeisung_total_import\") === undefined) {\n global.set(\"einspeisung_total_import\", 0);\n}\nif (global.get(\"einspeisung_total_export\") === undefined) {\n global.set(\"einspeisung_total_export\", 0);\n}\n//get counters from global variable\nvar total_import = global.get(\"einspeisung_total_import\");\nvar total_export = global.get(\"einspeisung_total_export\");\nvar check = \"Nothing\";\n\n//check if usage (payload) is positive=import or negative=export\n//add diff to import or export counters\nif (msg.payload < 0) {\n total_export += Math.abs(msg.payload);\n check = \"Export: \" + Math.abs(msg.payload) + \" kWh\";\n}\nif (msg.payload > 0) {\n total_import += Math.abs(msg.payload);\n check = \"Import: \" + Math.abs(msg.payload) + \" kWh\";\n}\n\n//write result back to global vars and send a msg for debug\nglobal.set(\"einspeisung_total_import\", total_import);\nglobal.set(\"einspeisung_total_export\", total_export);\nmsg.payload = {total_import: total_import, total_export: total_export, check: check}\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", @@ -714,7 +612,7 @@ "initialize": "", "finalize": "", "libs": [], - "x": 1440, + "x": 880, "y": 580, "wires": [ [] @@ -724,7 +622,7 @@ "id": "d905c25120cfdfd3", "type": "inject", "z": "f96eea4d4a3a345d", - "name": "Inject once", + "name": "Reset or init all counters (Set to Zero)", "props": [ { "p": "payload" @@ -741,7 +639,7 @@ "topic": "", "payload": "", "payloadType": "date", - "x": 1250, + "x": 260, "y": 580, "wires": [ [ @@ -802,7 +700,7 @@ "type": "function", "z": "f96eea4d4a3a345d", "name": "Calculate kWh of phase 1 power", - "func": "//init counters if not defined\nif (global.get(\"einspeisung_l1_import\") === undefined) {\n global.set(\"einspeisung_l1_import\", 0);\n}\nif (global.get(\"einspeisung_l1_export\") === undefined) {\n global.set(\"einspeisung_l1_export\", 0);\n}\n//get counters from global variable\nvar total_import = global.get(\"einspeisung_l1_import\");\nvar total_export = global.get(\"einspeisung_l1_export\");\nvar check = \"Nothing\";\n\nif (msg.payload < 0) {\n total_export += Math.abs(msg.payload);\n check = \"Export: \" + Math.abs(msg.payload) + \" kWh\";\n}\nif (msg.payload > 0) {\n total_import += Math.abs(msg.payload);\n check = \"Import: \" + Math.abs(msg.payload) + \" kWh\";\n}\n\nglobal.set(\"einspeisung_l1_import\", total_import);\nglobal.set(\"einspeisung_l1_export\", total_export);\nmsg.payload = {total_import: total_import, total_export: total_export, check: check}\nreturn msg;", + "func": "//init counters if not defined\nif (global.get(\"einspeisung_l1_import\") === undefined) {\n global.set(\"einspeisung_l1_import\", 0);\n}\nif (global.get(\"einspeisung_l1_export\") === undefined) {\n global.set(\"einspeisung_l1_export\", 0);\n}\n//get counters from global variable\nvar total_import = global.get(\"einspeisung_l1_import\");\nvar total_export = global.get(\"einspeisung_l1_export\");\nvar check = \"Nothing\";\n\n//check if usage (payload) is positive=import or negative=export\n//add diff to import or export counters\nif (msg.payload < 0) {\n total_export += Math.abs(msg.payload);\n check = \"Export: \" + Math.abs(msg.payload) + \" kWh\";\n}\nif (msg.payload > 0) {\n total_import += Math.abs(msg.payload);\n check = \"Import: \" + Math.abs(msg.payload) + \" kWh\";\n}\n//write result back to global vars and send a msg for debug\nglobal.set(\"einspeisung_l1_import\", total_import);\nglobal.set(\"einspeisung_l1_export\", total_export);\nmsg.payload = {total_import: total_import, total_export: total_export, check: check}\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", @@ -819,7 +717,7 @@ "type": "function", "z": "f96eea4d4a3a345d", "name": "Calculate kWh of phase 2 power", - "func": "//init counters if not defined\nif (global.get(\"einspeisung_l2_import\") === undefined) {\n global.set(\"einspeisung_l2_import\", 0);\n}\nif (global.get(\"einspeisung_l2_export\") === undefined) {\n global.set(\"einspeisung_l2_export\", 0);\n}\n//get counters from global variable\nvar total_import = global.get(\"einspeisung_l2_import\");\nvar total_export = global.get(\"einspeisung_l2_export\");\nvar check = \"Nothing\";\n\nif (msg.payload < 0) {\n total_export += Math.abs(msg.payload);\n check = \"Export: \" + Math.abs(msg.payload) + \" kWh\";\n}\nif (msg.payload > 0) {\n total_import += Math.abs(msg.payload);\n check = \"Import: \" + Math.abs(msg.payload) + \" kWh\";\n}\n\nglobal.set(\"einspeisung_l2_import\", total_import);\nglobal.set(\"einspeisung_l2_export\", total_export);\nmsg.payload = {total_import: total_import, total_export: total_export, check: check}\nreturn msg;", + "func": "//init counters if not defined\nif (global.get(\"einspeisung_l2_import\") === undefined) {\n global.set(\"einspeisung_l2_import\", 0);\n}\nif (global.get(\"einspeisung_l2_export\") === undefined) {\n global.set(\"einspeisung_l2_export\", 0);\n}\n//get counters from global variable\nvar total_import = global.get(\"einspeisung_l2_import\");\nvar total_export = global.get(\"einspeisung_l2_export\");\nvar check = \"Nothing\";\n\n//check if usage (payload) is positive=import or negative=export\n//add diff to import or export counters\nif (msg.payload < 0) {\n total_export += Math.abs(msg.payload);\n check = \"Export: \" + Math.abs(msg.payload) + \" kWh\";\n}\nif (msg.payload > 0) {\n total_import += Math.abs(msg.payload);\n check = \"Import: \" + Math.abs(msg.payload) + \" kWh\";\n}\n\n//write result back to global vars and send a msg for debug\nglobal.set(\"einspeisung_l2_import\", total_import);\nglobal.set(\"einspeisung_l2_export\", total_export);\nmsg.payload = {total_import: total_import, total_export: total_export, check: check}\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", @@ -836,7 +734,7 @@ "type": "function", "z": "f96eea4d4a3a345d", "name": "Calculate kWh of phase 3 power", - "func": "//init counters if not defined\nif (global.get(\"einspeisung_l3_import\") === undefined) {\n global.set(\"einspeisung_l3_import\", 0);\n}\nif (global.get(\"einspeisung_l3_export\") === undefined) {\n global.set(\"einspeisung_l3_export\", 0);\n}\n//get counters from global variable\nvar total_import = global.get(\"einspeisung_l3_import\");\nvar total_export = global.get(\"einspeisung_l3_export\");\nvar check = \"Nothing\";\n\nif (msg.payload < 0) {\n total_export += Math.abs(msg.payload);\n check = \"Export: \" + Math.abs(msg.payload) + \" kWh\";\n}\nif (msg.payload > 0) {\n total_import += Math.abs(msg.payload);\n check = \"Import: \" + Math.abs(msg.payload) + \" kWh\";\n}\n\nglobal.set(\"einspeisung_l3_import\", total_import);\nglobal.set(\"einspeisung_l3_export\", total_export);\nmsg.payload = {total_import: total_import, total_export: total_export, check: check}\nreturn msg;", + "func": "//init counters if not defined\nif (global.get(\"einspeisung_l3_import\") === undefined) {\n global.set(\"einspeisung_l3_import\", 0);\n}\nif (global.get(\"einspeisung_l3_export\") === undefined) {\n global.set(\"einspeisung_l3_export\", 0);\n}\n//get counters from global variable\nvar total_import = global.get(\"einspeisung_l3_import\");\nvar total_export = global.get(\"einspeisung_l3_export\");\nvar check = \"Nothing\";\n\n//check if usage (payload) is positive=import or negative=export\n//add diff to import or export counters\nif (msg.payload < 0) {\n total_export += Math.abs(msg.payload);\n check = \"Export: \" + Math.abs(msg.payload) + \" kWh\";\n}\nif (msg.payload > 0) {\n total_import += Math.abs(msg.payload);\n check = \"Import: \" + Math.abs(msg.payload) + \" kWh\";\n}\n\n//write result back to global vars and send a msg for debug\nglobal.set(\"einspeisung_l3_import\", total_import);\nglobal.set(\"einspeisung_l3_export\", total_export);\nmsg.payload = {total_import: total_import, total_export: total_export, check: check}\nreturn msg;", "outputs": 1, "noerr": 0, "initialize": "", @@ -847,5 +745,45 @@ "wires": [ [] ] + }, + { + "id": "8fe908e8e4e30699", + "type": "comment", + "z": "f96eea4d4a3a345d", + "name": "Write all values into global vars, to export them via a JSON-HTTP-API", + "info": "Write all values into global vars,\nto export them via a JSON-HTTP-API", + "x": 790, + "y": 80, + "wires": [] + }, + { + "id": "c648f182292dd923", + "type": "comment", + "z": "f96eea4d4a3a345d", + "name": "Read all global vars an create a JObject so we can poll it via HTTP", + "info": "Read all global vars an create a JObject so we can poll it via HTTP", + "x": 700, + "y": 820, + "wires": [] + }, + { + "id": "b38818b5fb6846af", + "type": "comment", + "z": "f96eea4d4a3a345d", + "name": "You can save global vars to a context file, if you enable \"Context storage\" in your Node-RED settings.json", + "info": "You can save global vars to a context file, if you enable \"Context storage\" in your Node-RED settings.json", + "x": 850, + "y": 720, + "wires": [] + }, + { + "id": "2413b4c69ccefdfb", + "type": "comment", + "z": "f96eea4d4a3a345d", + "name": "With Context storage all global vars are persisted bedween Node RED restarts", + "info": "With Context storage all global vars are persisted bedween Node RED restarts", + "x": 740, + "y": 760, + "wires": [] } ] \ No newline at end of file