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 +44 -0
- package/examples/Read from VE.Direct USB.json +70 -0
- package/package.json +37 -0
- package/src/nodes/icons/victronenergy.svg +30 -0
- package/src/nodes/vedirect-usb.html +84 -0
- package/src/nodes/vedirect-usb.js +56 -0
- package/src/services/parser.js +484 -0
- package/src/services/vedirect.js +32 -0
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
|