node-red-contrib-victron-vedirect-usb 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ This node uses a [VE.Direct USB connection](https://www.victronenergy.com/accessories/ve-direct-to-usb-interface)
2
+ to grab the communication on the serial port and trnaslates it into usable data.
3
+
4
+ A typical use case would be to run Node-RED on a Raspberry Pi and
5
+ connect to the VE.Direct port of a Victron Energy device. E.g. a
6
+ SmartShunt, BMV, Inverter or MPPT.
7
+
8
+ # Usage
9
+
10
+ Once the node gets deployed it keeps on reading and stores the values as they
11
+ get read from the serial port. It only outputs on 'inject', so it is needed to
12
+ trigger output via an _inject_ node to the node. Typically you would configure
13
+ that to repeat on an interval of a few seconds.
14
+
15
+ # Configuration
16
+
17
+ Select the port to use from the dropdown. The dropdown is generated on the fly,
18
+ so make sure that the USB part of the cable is connected to the system running
19
+ Node-RED.
20
+
21
+ # Output
22
+
23
+ The output depends on the connected product, but is based on the
24
+ [VE.Direct-Protocol-3.23.pdf](https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.32.pdf).
25
+
26
+ The `msg.payload` holds the used VE.Direct label, the units, description and value. E.g.:
27
+
28
+ ```
29
+ ...
30
+ PID: {"value":"0xA389","description":"ProductID","units":""},
31
+ V: {"value":7814,"description":"Main or channel 1 (battery) voltage","units":"mV"},
32
+ I: {"value":0,"description":"Main or channel 1 battery current","units":"mA"}
33
+ ...
34
+ ```
35
+
36
+ The above example is abbreviated. It typically consists of more labels.
37
+
38
+ # License
39
+
40
+ License is _GPL-3.0-or-later_.
41
+
42
+ # About
43
+
44
+ The code is based on https://github.com/bencevans/ve.direct of Ben Evans.
@@ -0,0 +1,70 @@
1
+ [
2
+ {
3
+ "id": "2b846580860dc65c",
4
+ "type": "tab",
5
+ "label": "Flow 2",
6
+ "disabled": false,
7
+ "info": "",
8
+ "env": []
9
+ },
10
+ {
11
+ "id": "1e16a853db4ee92d",
12
+ "type": "victron-vedirect-usb",
13
+ "z": "2b846580860dc65c",
14
+ "name": "",
15
+ "port": "/dev/ttyUSB0",
16
+ "x": 390,
17
+ "y": 320,
18
+ "wires": [
19
+ [
20
+ "a145b12a5d20c03f"
21
+ ]
22
+ ]
23
+ },
24
+ {
25
+ "id": "57af26c6c87b120e",
26
+ "type": "inject",
27
+ "z": "2b846580860dc65c",
28
+ "name": "",
29
+ "props": [
30
+ {
31
+ "p": "payload"
32
+ },
33
+ {
34
+ "p": "topic",
35
+ "vt": "str"
36
+ }
37
+ ],
38
+ "repeat": "5",
39
+ "crontab": "",
40
+ "once": true,
41
+ "onceDelay": 0.1,
42
+ "topic": "",
43
+ "payload": "",
44
+ "payloadType": "date",
45
+ "x": 210,
46
+ "y": 320,
47
+ "wires": [
48
+ [
49
+ "1e16a853db4ee92d"
50
+ ]
51
+ ]
52
+ },
53
+ {
54
+ "id": "a145b12a5d20c03f",
55
+ "type": "debug",
56
+ "z": "2b846580860dc65c",
57
+ "name": "VE.Direct parsed",
58
+ "active": true,
59
+ "tosidebar": true,
60
+ "console": false,
61
+ "tostatus": false,
62
+ "complete": "payload",
63
+ "targetType": "msg",
64
+ "statusVal": "",
65
+ "statusType": "auto",
66
+ "x": 590,
67
+ "y": 320,
68
+ "wires": []
69
+ }
70
+ ]
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "node-red-contrib-victron-vedirect-usb",
3
+ "version": "0.0.1",
4
+ "description": "Access the Victron equipment with a USB VE.Direct cable",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "standard --fix"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/dirkjanfaber/node-red-contrib-victron-vedirect-usb.git"
12
+ },
13
+ "keywords": [
14
+ "node-red",
15
+ "victron",
16
+ "vedirect"
17
+ ],
18
+ "node-red": {
19
+ "nodes": {
20
+ "victron-vedirect-usb": "src/nodes/vedirect-usb.js"
21
+ },
22
+ "version": ">=3.0.2"
23
+ },
24
+ "engines": {
25
+ "node": ">=14.17.4"
26
+ },
27
+ "author": "Dirk-Jan Faber <dfaber@victronenergy.com>",
28
+ "license": "GPL-3.0-or-later",
29
+ "bugs": {
30
+ "url": "https://github.com/dirkjanfaber/node-red-contrib-victron-vedirect-usb/issues"
31
+ },
32
+ "homepage": "https://github.com/dirkjanfaber/node-red-contrib-victron-vedirect-usb#readme",
33
+ "dependencies": {
34
+ "@serialport/bindings": "^10.0.0",
35
+ "serialport": "^10.0.0"
36
+ }
37
+ }
@@ -0,0 +1,30 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
+ <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
+ viewBox="0 0 92.7 62.2" style="enable-background:new 0 0 92.7 62.2;" xml:space="preserve">
5
+ <style type="text/css">
6
+ .st0{fill:#FFFFFF;}
7
+ </style>
8
+ <title>v-symbol</title>
9
+ <path class="st0" d="M23.9,7.7c-3.4-1.1-7.1-0.3-9.8,2.1c0,0-5.9,4-7,20.6c0,0-0.3,2.8,2.1,3.2c0,0,2.5,0.5,2.8-1.6s0.3-12.6,4.9-18
10
+ c0.4-0.5,1-1,1.7-1.2c1-0.7,1.8-1.7,2.5-2.7C21.8,9,22.8,8.2,23.9,7.7"/>
11
+ <path class="st0" d="M32.7,7.3c0,0-4.4-1.4-9,2.2c0,0-5.5,3.4-6.9,16.9c-0.2,1.7-0.3,3.3-0.2,5c0.1,1.2,1.1,2,2.2,2.1
12
+ c0,0,2.5,0.2,2.6-2.1l0.2-3.2c0,0,0.4-9,4-13.5l0.9,0.7C27.7,12,29.9,9.2,32.7,7.3L32.7,7.3z"/>
13
+ <path class="st0" d="M41.6,7.3c-3.7-0.7-7.4,0.5-10,3.2c0,0-4.2,3.7-5.4,13c0,0-0.5,5-0.6,7.4c0,1.3,0.9,2.4,2.2,2.6
14
+ c1.3,0.1,2.4-0.9,2.5-2.2c0-0.1,0-0.1,0-0.2c0,0,0.2-11.3,4.4-16.5l1,0.6C35.6,15.3,37.4,10.1,41.6,7.3L41.6,7.3z"/>
15
+ <path class="st0" d="M48.2,7.1c-3.9,0.1-7.4,2.3-9.3,5.7c0,0-4,5.8-4,17.6c0,0-0.2,2.8,2,3.1c1.4,0.1,2.6-1,2.6-2.4
16
+ c0-0.1,0-0.3,0-0.4c0,0,0.1-11.7,4.6-16.4c0,0,4,4.7,4.3,15.9c0,0,0.1,7.7,1.6,11.9c0.9-3.7,1.4-7.5,1.5-11.3c0-0.8,0.3-1.5,0.9-2
17
+ c0,0,0.7-0.3,0.5-1.5c0,0-1-9.7-4.2-15.3c0,0,6.9-0.5,8.2,13.3c0,1.2,0.4,2.3,1.1,3.3c0.6,0.5,0.9,1.2,0.9,2c0.1,2.8,0,5.5-0.4,8.2
18
+ c-0.1,1.1,0.1,2.3,0.5,3.3c1-3,1.4-6.2,1.3-9.3c0,0-0.2-3.7,1.6-5c0,0-0.1-10.5-4.3-16.2C55.4,8.7,51.9,7.1,48.2,7.1L48.2,7.1z"/>
19
+ <path class="st0" d="M68.8,54.4c3.4,1.1,7.1,0.3,9.8-2.1c0,0,5.9-4,7-20.6c0,0,0.3-2.8-2.1-3.2c0,0-2.5-0.5-2.8,1.6
20
+ s-0.3,12.6-4.9,18c-0.4,0.5-1,1-1.7,1.2c-1,0.7-1.8,1.7-2.5,2.7C70.8,53.1,69.9,53.8,68.8,54.4"/>
21
+ <path class="st0" d="M59.9,54.8c0,0,4.4,1.4,9-2.2c0,0,5.5-3.4,6.9-16.9c0.2-1.7,0.3-3.3,0.2-5c-0.1-1.2-1.1-2.1-2.2-2.1
22
+ c0,0-2.5-0.2-2.6,2.1l-0.2,3.2c0,0-0.4,9-4,13.5L66,46.8C64.9,50,62.8,52.9,59.9,54.8L59.9,54.8z"/>
23
+ <path class="st0" d="M51.1,54.8c3.7,0.7,7.4-0.5,10-3.2c0,0,4.2-3.7,5.4-13c0,0,0.5-5,0.6-7.4c0-1.3-0.9-2.4-2.2-2.6
24
+ c-1.3-0.1-2.4,0.9-2.5,2.2c0,0.1,0,0.1,0,0.2c0,0-0.1,11.3-4.4,16.5l-1-0.6C57.1,46.7,55.3,52,51.1,54.8L51.1,54.8z"/>
25
+ <path class="st0" d="M44.4,55c3.9-0.1,7.4-2.3,9.3-5.7c0,0,4-5.8,4-17.6c0,0,0.2-2.8-2-3.1c-1.4-0.1-2.6,1-2.6,2.4
26
+ c0,0.1,0,0.3,0,0.4c0,0-0.1,11.7-4.6,16.4c0,0-4-4.7-4.3-15.9c0,0-0.1-7.7-1.6-11.9c-0.9,3.7-1.4,7.5-1.5,11.3c0,0.8-0.3,1.5-0.9,2
27
+ c0,0-0.7,0.3-0.5,1.5c0,0,1,9.7,4.2,15.3c0,0-6.9,0.5-8.2-13.3c0-1.2-0.4-2.3-1.1-3.3c-0.6-0.5-0.9-1.2-0.9-2
28
+ c-0.1-2.8,0-5.5,0.4-8.2c0-1.1-0.1-2.3-0.5-3.3c-1,3-1.4,6.2-1.2,9.3c0,0,0.2,3.7-1.6,5c0,0,0.2,10.5,4.3,16.2
29
+ C37.2,53.4,40.7,55,44.4,55L44.4,55z"/>
30
+ </svg>
@@ -0,0 +1,84 @@
1
+ <script type="text/javascript">
2
+
3
+ var portsUrl = (RED.settings.httpNodeRoot || RED.settings.httpAdminRoot || "").replace(/\/$/, "") + '/victron/vedirect-ports/';
4
+
5
+ function buildEditPanel(node) {
6
+ var buildOption = function buildOption(port) {
7
+ return $('<option/>').val(port.path).text(port.path).data(port);
8
+ };
9
+
10
+ var populateSelect = function populateSelect(selector, ports) {
11
+ selector[0].options.length = 0;
12
+ ports.forEach(function (port) {
13
+ selector.append(buildOption(port));
14
+ });
15
+ selector.trigger('change');
16
+ };
17
+
18
+ $.ajax(portsUrl, {
19
+ beforeSend: function(jqXHR) {
20
+ var auth_tokens = RED.settings.get("auth-tokens");
21
+ if (auth_tokens) {
22
+ jqXHR.setRequestHeader("Authorization","Bearer "+auth_tokens.access_token);
23
+ }
24
+ }
25
+ })
26
+ .done(data => {
27
+ if (data.length !== 0) {
28
+ populateSelect($('#node-input-port'), data);
29
+ }
30
+ if (node.port) { $('#node-input-port').val(node.port) }
31
+
32
+ }).fail(function () {
33
+ return showStatusMessage("Unable to access the service endpoint.");
34
+ });
35
+
36
+ }
37
+ RED.nodes.registerType('victron-vedirect-usb',{
38
+ category: 'Victron Energy',
39
+ paletteLabel: 'VE.Direct USB',
40
+ color: '#f7ab3e',
41
+ defaults: {
42
+ name: {value: ""},
43
+ port: {value: "/dev/ttyUSB0"} },
44
+ inputs: 1,
45
+ outputs: 1,
46
+ icon: "victronenergy.svg",
47
+ label: function() {
48
+ return this.name||"VE.Direct USB";
49
+ },
50
+ oneditprepare: function oneditprepare() {
51
+ buildEditPanel(this);
52
+ }
53
+ });
54
+ </script>
55
+
56
+ <script type="text/html" data-template-name="victron-vedirect-usb">
57
+ <div class="form-row">
58
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
59
+ <input type="text" id="node-input-name" placeholder="Name">
60
+ </div>
61
+ <div class="form-row">
62
+ <label for="node-input-port"><i class="fa fa-tag"></i> Port</label>
63
+ <select id="node-input-port" required>
64
+ </select>
65
+ </div>
66
+ </script>
67
+
68
+ <script type="text/html" data-help-name="victron-vedirect-usb">
69
+ <p>A node for VE.Direct USB communication with Victron devices.</p>
70
+
71
+ <h3>Configuration</h3>
72
+
73
+ <p>Only needed configuration is to select the port where the ttyUSB dongle
74
+ is connected to. Make sure that you have rights for reading the dongle
75
+ (this might need you to be in group <tt>dialup</tt>).
76
+ </p>
77
+
78
+ <h3>Input</h3>
79
+
80
+ <p>On each input, the read output gets send out. So use an repeating
81
+ inject node to get output every x seconds.
82
+ </p>
83
+
84
+ </script>
@@ -0,0 +1,56 @@
1
+ module.exports = function (RED) {
2
+ 'use strict'
3
+ const VEDirect = require('../services/vedirect')
4
+ const bindings = require('@serialport/bindings')
5
+
6
+ function VEDirectUSB (config) {
7
+ RED.nodes.createNode(this, config)
8
+
9
+ const node = this
10
+ const dataReader = new VEDirect(config.port)
11
+
12
+ dataReader.on('data', (data) => {
13
+ const nodeContext = this.context()
14
+ const msg = {
15
+ topic: 'VE.direct'
16
+ }
17
+
18
+ if (data.H1) {
19
+ nodeContext.set('d1', data)
20
+ msg.payload = Object.assign({}, data, nodeContext.get('d2'))
21
+ } else {
22
+ nodeContext.set('d2', data)
23
+ msg.payload = Object.assign({}, data, nodeContext.get('d1'))
24
+ }
25
+ })
26
+
27
+ dataReader.on('error', (error) => {
28
+ console.error(error)
29
+ node.warn(error)
30
+ })
31
+
32
+ node.on('input', function (msg) {
33
+ const nodeContext = this.context()
34
+ msg.payload = Object.assign({}, nodeContext.get('d1'), nodeContext.get('d2'))
35
+ node.send(msg)
36
+ })
37
+
38
+ node.on('close', function () {
39
+ dataReader.serial.close()
40
+ })
41
+ }
42
+
43
+ RED.nodes.registerType('victron-vedirect-usb', VEDirectUSB)
44
+
45
+ RED.httpNode.get('/victron/vedirect-ports', (req, res) => {
46
+ function list (ports) {
47
+ res.setHeader('Content-Type', 'application/json')
48
+ return res.send(ports)
49
+ }
50
+
51
+ bindings.list().then(list, err => {
52
+ console.log(err)
53
+ process.exit(1)
54
+ })
55
+ })
56
+ }
@@ -0,0 +1,484 @@
1
+ const { Transform } = require('stream')
2
+
3
+ const checksum = (blockBuffer) => {
4
+ return blockBuffer.reduce((prev, curr) => {
5
+ return (prev + curr) & 255
6
+ }, 0)
7
+ }
8
+
9
+ const parseValues = (frame) => {
10
+ for (const key in frame) {
11
+ switch (key) {
12
+ case 'V':
13
+ frame[key] = {
14
+ value: frame[key],
15
+ description: 'Main or channel 1 (battery) voltage',
16
+ units: 'mV'
17
+ }
18
+ break
19
+ case 'V2':
20
+ frame[key] = {
21
+ value: frame[key],
22
+ description: 'Channel 2 (battery) voltage',
23
+ units: 'mV'
24
+ }
25
+ break
26
+ case 'V3':
27
+ frame[key] = {
28
+ value: frame[key],
29
+ description: 'Channel 3 (battery) voltage',
30
+ units: 'mV'
31
+ }
32
+ break
33
+ case 'VS':
34
+ frame[key] = {
35
+ value: frame[key],
36
+ description: 'Auxiliary (starter) voltage',
37
+ units: 'mV'
38
+ }
39
+ break
40
+ case 'VM':
41
+ frame[key] = {
42
+ value: frame[key],
43
+ description: 'Mid-point voltage of the battery bank',
44
+ units: 'mV'
45
+ }
46
+ break
47
+ case 'DM':
48
+ frame[key] = {
49
+ value: frame[key],
50
+ description: 'Mid-point deviation of the battery bank',
51
+ units: '‰'
52
+ }
53
+ break
54
+ case 'VPV':
55
+ frame[key] = {
56
+ value: frame[key],
57
+ description: 'Panel voltage',
58
+ units: 'mV'
59
+ }
60
+ break
61
+ case 'PPV':
62
+ frame[key] = {
63
+ value: frame[key],
64
+ description: 'Panel power',
65
+ units: 'W'
66
+ }
67
+ break
68
+ case 'I':
69
+ frame[key] = {
70
+ value: frame[key],
71
+ description: 'Main or channel 1 battery current',
72
+ units: 'mA'
73
+ }
74
+ break
75
+ case 'I2':
76
+ frame[key] = {
77
+ value: frame[key],
78
+ description: 'Channel 2 battery current',
79
+ units: 'mA'
80
+ }
81
+ break
82
+ case 'I3':
83
+ frame[key] = {
84
+ value: frame[key],
85
+ description: 'Channel 3 battery current',
86
+ units: 'mA'
87
+ }
88
+ break
89
+ case 'IL':
90
+ frame[key] = {
91
+ value: frame[key],
92
+ description: 'Load current',
93
+ units: 'mA'
94
+ }
95
+ break
96
+ case 'LOAD':
97
+ frame[key] = {
98
+ value: frame[key],
99
+ description: 'Load output state (ON/OFF)',
100
+ units: 'W'
101
+ }
102
+ break
103
+ case 'T':
104
+ frame[key] = {
105
+ value: frame[key],
106
+ description: 'Battery temperature',
107
+ units: '°C'
108
+ }
109
+ break
110
+ case 'P':
111
+ frame[key] = {
112
+ value: frame[key],
113
+ description: 'Instantaneous power',
114
+ units: 'W'
115
+ }
116
+ break
117
+ case 'CE':
118
+ frame[key] = {
119
+ value: frame[key],
120
+ description: 'Consumed Amp Hours',
121
+ units: 'mAh'
122
+ }
123
+ break
124
+ case 'SOC':
125
+ frame[key] = {
126
+ value: frame[key],
127
+ description: 'State-of-charge',
128
+ units: '‰'
129
+ }
130
+ break
131
+ case 'TTG':
132
+ frame[key] = {
133
+ value: frame[key],
134
+ description: 'Time-to-go',
135
+ units: 'Minutes'
136
+ }
137
+ break
138
+ case 'Alarm':
139
+ frame[key] = {
140
+ value: frame[key],
141
+ description: 'Alarm condition active',
142
+ units: ''
143
+ }
144
+ break
145
+ case 'Relay':
146
+ frame[key] = {
147
+ value: frame[key],
148
+ description: 'Relay state',
149
+ units: ''
150
+ }
151
+ break
152
+ case 'AR':
153
+ frame[key] = {
154
+ value: frame[key],
155
+ description: 'Alarm reason',
156
+ units: ''
157
+ }
158
+ break
159
+ case 'OR':
160
+ frame[key] = {
161
+ value: frame[key],
162
+ description: 'Off reason',
163
+ units: ''
164
+ }
165
+ break
166
+ case 'H1':
167
+ frame[key] = {
168
+ value: frame[key],
169
+ description: 'Depth of the deepest discharge',
170
+ units: 'mAh'
171
+ }
172
+ break
173
+ case 'H2':
174
+ frame[key] = {
175
+ value: frame[key],
176
+ description: 'Depth of the last discharge',
177
+ units: 'mAh'
178
+ }
179
+ break
180
+ case 'H3':
181
+ frame[key] = {
182
+ value: frame[key],
183
+ description: 'Depth of the average discharge',
184
+ units: 'mAh'
185
+ }
186
+ break
187
+ case 'H4':
188
+ frame[key] = {
189
+ value: frame[key],
190
+ description: 'Number of charge cycles',
191
+ units: ''
192
+ }
193
+ break
194
+ case 'H5':
195
+ frame[key] = {
196
+ value: frame[key],
197
+ description: 'Number of full discharges',
198
+ units: ''
199
+ }
200
+ break
201
+ case 'H6':
202
+ frame[key] = {
203
+ value: frame[key],
204
+ description: 'Cumulative Amp Hours drawn',
205
+ units: 'mAh'
206
+ }
207
+ break
208
+ case 'H7':
209
+ frame[key] = {
210
+ value: frame[key],
211
+ description: 'Minimum main (battery) voltage',
212
+ units: 'mV'
213
+ }
214
+ break
215
+ case 'H8':
216
+ frame[key] = {
217
+ value: frame[key],
218
+ description: 'Maximum main (battery) voltage',
219
+ units: 'mV'
220
+ }
221
+ break
222
+ case 'H9':
223
+ frame[key] = {
224
+ value: frame[key],
225
+ description: 'Number of seconds since last full charge',
226
+ units: 'Seconds'
227
+ }
228
+ break
229
+ case 'H10':
230
+ frame[key] = {
231
+ value: frame[key],
232
+ description: 'Number of automatic synchronizations',
233
+ units: ''
234
+ }
235
+ break
236
+ case 'H11':
237
+ frame[key] = {
238
+ value: frame[key],
239
+ description: 'Number of low main voltage alarms',
240
+ units: ''
241
+ }
242
+ break
243
+ case 'H12':
244
+ frame[key] = {
245
+ value: frame[key],
246
+ description: 'Number of high main voltage alarms',
247
+ units: ''
248
+ }
249
+ break
250
+ case 'H13':
251
+ frame[key] = {
252
+ value: frame[key],
253
+ description: 'Number of low auxiliary voltage alarms',
254
+ units: ''
255
+ }
256
+ break
257
+ case 'H14':
258
+ frame[key] = {
259
+ value: frame[key],
260
+ description: 'Number of high auxiliary voltage alarms',
261
+ units: ''
262
+ }
263
+ break
264
+ case 'H15':
265
+ frame[key] = {
266
+ value: frame[key],
267
+ description: 'Minimum auxiliary (battery) voltage',
268
+ units: 'mV'
269
+ }
270
+ break
271
+ case 'H16':
272
+ frame[key] = {
273
+ value: frame[key],
274
+ description: 'Maximum auxiliary (battery) voltage',
275
+ units: 'mV'
276
+ }
277
+ break
278
+ case 'H17':
279
+ frame[key] = {
280
+ value: frame[key],
281
+ description: 'Amount of discharged energy (BMV) / Amount of produced energy (DC monitor)',
282
+ units: '0.01 kWh'
283
+ }
284
+ break
285
+ case 'H18':
286
+ frame[key] = {
287
+ value: frame[key],
288
+ description: 'Amount of charged energy (BMV) / Amount of consumed energy (DC monitor)',
289
+ units: '0.01 kWh'
290
+ }
291
+ break
292
+ case 'H19':
293
+ frame[key] = {
294
+ value: frame[key],
295
+ description: 'Yield total (user resettable counter)',
296
+ units: '0.01 kWh'
297
+ }
298
+ break
299
+ case 'H20':
300
+ frame[key] = {
301
+ value: frame[key],
302
+ description: 'Yield today',
303
+ units: '0.01 kWh'
304
+ }
305
+ break
306
+ case 'H21':
307
+ frame[key] = {
308
+ value: frame[key],
309
+ description: 'Maximum power today',
310
+ units: 'W'
311
+ }
312
+ break
313
+ case 'H22':
314
+ frame[key] = {
315
+ value: frame[key],
316
+ description: 'Yield yesterday',
317
+ units: '0.01 kWh'
318
+ }
319
+ break
320
+ case 'H23':
321
+ frame[key] = {
322
+ value: frame[key],
323
+ description: 'Maximum power yesterday',
324
+ units: 'W'
325
+ }
326
+ break
327
+ case 'ERR':
328
+ frame[key] = {
329
+ value: frame[key],
330
+ description: 'Error code',
331
+ units: ''
332
+ }
333
+ break
334
+ case 'CS':
335
+ frame[key] = {
336
+ value: frame[key],
337
+ description: 'State of operation',
338
+ units: ''
339
+ }
340
+ break
341
+ case 'BMV':
342
+ frame[key] = {
343
+ value: frame[key],
344
+ description: 'Model description (deprecated)',
345
+ units: ''
346
+ }
347
+ break
348
+ case 'FW':
349
+ frame[key] = {
350
+ value: frame[key],
351
+ description: 'Firmware version (16 bit)',
352
+ units: ''
353
+ }
354
+ break
355
+ case 'FWE':
356
+ frame[key] = {
357
+ value: frame[key],
358
+ description: 'Firmware version (24 bit)',
359
+ units: ''
360
+ }
361
+ break
362
+ case 'PID':
363
+ frame[key] = {
364
+ value: frame[key],
365
+ description: 'ProductID',
366
+ units: ''
367
+ }
368
+ break
369
+ case 'SER#':
370
+ frame[key] = {
371
+ value: frame[key],
372
+ description: 'Serial number',
373
+ units: ''
374
+ }
375
+ break
376
+ case 'HSDS':
377
+ frame[key] = {
378
+ value: frame[key],
379
+ description: 'Day sequence number (0..364)',
380
+ units: ''
381
+ }
382
+ break
383
+ case 'MODE':
384
+ frame[key] = {
385
+ value: frame[key],
386
+ description: 'Device mode',
387
+ units: ''
388
+ }
389
+ break
390
+ case 'AC_OUT_V':
391
+ frame[key] = {
392
+ value: frame[key],
393
+ description: 'AC output voltage',
394
+ units: '0.01 V'
395
+ }
396
+ break
397
+ case 'AC_OUT_I':
398
+ frame[key] = {
399
+ value: frame[key],
400
+ description: 'AC output current',
401
+ units: '0.1 A'
402
+ }
403
+ break
404
+ case 'AC_OUT_S':
405
+ frame[key] = {
406
+ value: frame[key],
407
+ description: 'AC output apparent power',
408
+ units: 'VA'
409
+ }
410
+ break
411
+ case 'WARN':
412
+ frame[key] = {
413
+ value: frame[key],
414
+ description: 'Warning reason',
415
+ units: ''
416
+ }
417
+ break
418
+ case 'MPPT':
419
+ frame[key] = {
420
+ value: frame[key],
421
+ description: 'Tracker operation mode',
422
+ units: ''
423
+ }
424
+ break
425
+ case 'MON':
426
+ frame[key] = {
427
+ value: frame[key],
428
+ description: 'DC monitor mode',
429
+ units: ''
430
+ }
431
+ break
432
+ default:
433
+ frame[key] = {
434
+ value: frame[key],
435
+ description: 'Non implemented key',
436
+ units: ''
437
+ }
438
+ break
439
+ }
440
+ frame[key].value = parseFloat(frame[key].value) || frame[key].value
441
+ if (frame[key].value === '0') {
442
+ frame[key].value = 0
443
+ }
444
+ }
445
+
446
+ return frame
447
+ }
448
+
449
+ class VEDirectParser extends Transform {
450
+ constructor () {
451
+ super({
452
+ readableObjectMode: true
453
+ })
454
+
455
+ this.buf = Buffer.alloc(0)
456
+ this.blk = {}
457
+ }
458
+
459
+ _transform (chunk, _, cb) {
460
+ const [key, val] = chunk.toString().split('\t')
461
+
462
+ if (key[0] === ':') {
463
+ return cb()
464
+ }
465
+
466
+ this.buf = Buffer.concat([this.buf, Buffer.from([0x0d, 0x0a]), chunk])
467
+
468
+ if (key === 'Checksum') {
469
+ if (checksum(this.buf) === 0) {
470
+ this.push(parseValues(this.blk))
471
+ }
472
+
473
+ this.buf = Buffer.alloc(0)
474
+ this.blk = {}
475
+ } else {
476
+ this.blk[key] = val
477
+ }
478
+
479
+ cb()
480
+ }
481
+ }
482
+
483
+ module.exports = VEDirectParser
484
+ module.exports.checksum = checksum
@@ -0,0 +1,32 @@
1
+ const { SerialPort } = require('serialport')
2
+ const { DelimiterParser } = require('@serialport/parser-delimiter')
3
+ const VEDirectParser = require('../services/parser')
4
+ const { EventEmitter } = require('events')
5
+
6
+ class VEDirect extends EventEmitter {
7
+ constructor (path) {
8
+ super()
9
+
10
+ this.serial = new SerialPort({
11
+ path,
12
+ baudRate: 19200,
13
+ dataBits: 8,
14
+ parity: 'none'
15
+ })
16
+
17
+ this.rl = new DelimiterParser({
18
+ delimiter: Buffer.from([0x0d, 0x0a], 'hex'),
19
+ includeDelimiter: false
20
+ })
21
+
22
+ this.ve = new VEDirectParser()
23
+
24
+ this.ve.on('data', (data) => {
25
+ this.emit('data', data)
26
+ })
27
+
28
+ this.serial.pipe(this.rl).pipe(this.ve)
29
+ }
30
+ }
31
+
32
+ module.exports = VEDirect