bt-sensors-plugin-sk 1.2.1 → 1.2.2
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/BTSensor.js +0 -3
- package/README.md +33 -10
- package/index.js +9 -2
- package/package.json +2 -2
- package/plugin_defaults.json +21 -2
- package/public/images/SmartShunt_500_nw.png +0 -0
- package/sensor_classes/Junctek.js +148 -0
- package/sensor_classes/KilovaultHLXPlus.js +1 -1
- package/sensor_classes/RemoranWave3.js +217 -0
- package/sensor_classes/Renogy/RenogySensor.js +3 -2
- package/sensor_classes/ShenzhenLiOnBMS.js +26 -16
- package/sensor_classes/Victron/VictronImages.js +1 -1
- package/sensor_classes/Victron/VictronSensor.js +4 -2
- package/sensor_classes/VictronBatteryMonitor.js +2 -2
- package/sensor_classes/VictronDCDCConverter.js +1 -1
- package/sensor_classes/VictronLynxSmartBMS.js +5 -1
- package/sensor_classes/VictronOrionXS.js +1 -1
- package/sensor_classes/VictronSmartLithium.js +1 -1
- package/vsl_patch_17_06_25.patch +13 -0
package/BTSensor.js
CHANGED
|
@@ -787,8 +787,6 @@ class BTSensor extends EventEmitter {
|
|
|
787
787
|
/**
|
|
788
788
|
* Listen to sensor.
|
|
789
789
|
* ::listen() sets up listeners for property changes thru ::propertiesChanged(props)
|
|
790
|
-
* If GATT connections are available and active, function inits the GATT connection and
|
|
791
|
-
* optional GATT connection interval
|
|
792
790
|
*/
|
|
793
791
|
|
|
794
792
|
|
|
@@ -842,7 +840,6 @@ class BTSensor extends EventEmitter {
|
|
|
842
840
|
if (!(path===undefined))
|
|
843
841
|
this.app.handleMessage(id,
|
|
844
842
|
{
|
|
845
|
-
// $source: source,
|
|
846
843
|
updates:
|
|
847
844
|
[{ meta: [{path: preparePath(this, path), value: { units: pathMeta?.unit }}]}]
|
|
848
845
|
})
|
package/README.md
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
# Bluetooth Sensors for [Signal K](http://www.signalk.org)
|
|
2
|
-
# Version 1.2.
|
|
2
|
+
# Version 1.2.2
|
|
3
3
|
|
|
4
|
-
## WHAT
|
|
5
|
-
|
|
6
|
-
BT Sensors Plugin for Signalk is a lightweight BLE (Bluetooth Low Energy) framework for listening and connecting to Bluetooth sensors on your boat. After discovery and configuration the plugin sends deltas to Signalk paths with values your sensor reports. <br>
|
|
7
|
-
|
|
8
|
-
It runs on any 2.0 or greater SignalK installation but on Linux only. It's been tested on Desktop and headless RPis, OpenPlotter, and Cerbo GX/Ekrano.
|
|
9
|
-
|
|
10
|
-
A typical use case is a Bluetooth thermometer like the Xiaomi LYWSD03MMC, an inexpensive Bluetooth thermometer that runs on a 3V watch battery that can report the current temperature and humidity in your refrigerator or cabin or wherever you want to stick it (no judgement.) <br>
|
|
4
|
+
## WHAT'S NEW SINCE VERSION 1.2.1
|
|
11
5
|
|
|
12
|
-
|
|
6
|
+
- Junctek BMS and Remoran Wave 3 support (Note: both are not currently field tested)
|
|
13
7
|
|
|
14
|
-
|
|
8
|
+
- Fixes to ShenzhenLiOn BMS, Victron Orion XS, Victron DC DC Converter, Victron Smart Lithium Classes
|
|
15
9
|
|
|
16
10
|
## WHAT'S NEW SINCE VERSION 1.1.x
|
|
17
11
|
|
|
@@ -25,6 +19,19 @@ It's pretty easy to write and deploy your own sensor class for any currently uns
|
|
|
25
19
|
|
|
26
20
|
- Support for multiple simultaneous GATT connections.
|
|
27
21
|
|
|
22
|
+
|
|
23
|
+
## WHAT IT IS
|
|
24
|
+
|
|
25
|
+
BT Sensors Plugin for Signalk is a lightweight BLE (Bluetooth Low Energy) framework for listening and connecting to Bluetooth sensors on your boat. After discovery and configuration the plugin sends deltas to Signalk paths with values your sensor reports. <br>
|
|
26
|
+
|
|
27
|
+
It runs on any 2.0 or greater SignalK installation but on Linux only. It's been tested on Desktop and headless RPis, OpenPlotter, and Cerbo GX/Ekrano.
|
|
28
|
+
|
|
29
|
+
A typical use case is a Bluetooth thermometer like the Xiaomi LYWSD03MMC, an inexpensive Bluetooth thermometer that runs on a 3V watch battery that can report the current temperature and humidity in your refrigerator or cabin or wherever you want to stick it (no judgement.) <br>
|
|
30
|
+
|
|
31
|
+
The reported temperature can then be displayed on a Signalk app like Kip, WilhelmSK or, with appropriate mapping to NMEA-2000, a NMEA 2000 Multi-function display.
|
|
32
|
+
|
|
33
|
+
It's pretty easy to write and deploy your own sensor class for any currently unsupported sensor. More on that in [the development README](./sensor_classes/DEVELOPMENT.md).
|
|
34
|
+
|
|
28
35
|
## SUPPORTED SENSORS
|
|
29
36
|
|
|
30
37
|
### NOTE
|
|
@@ -44,6 +51,8 @@ It's pretty easy to write and deploy your own sensor class for any currently uns
|
|
|
44
51
|
|Redodo| Rebranded LiTime |
|
|
45
52
|
|Kilovault| [Kilovault HLX+ smart batteries ](https://sunwatts.com/content/manual/KiloVault_HLX_PLUS_Datasheet_06252021%20%281%29.pdf?srsltid=AfmBOooY-cGnC_Qm6V1T9Vg5oZzBCJurS0AOGoWqWeyy-dwz2vA-l1Jb) (Note: Kilovault appears to be out of business as of March 2024) |
|
|
46
53
|
|[Lancol](www.Lancol.com)| [Micro 10C 12V Car Battery Monitor](https://www.lancol.com/product/12v-bluetooth-4-0-battery-tester-micro-10-c/)|
|
|
54
|
+
|[Junctek](https://www.junteks.com)|[Junctek BMS](https://www.junteks.com/pages/product/index) |
|
|
55
|
+
|[Remoran](https://remoran.eu)| [Remoran Wave.3](https://remoran.eu/wave.html)|
|
|
47
56
|
|
|
48
57
|
|
|
49
58
|
### Environmental
|
|
@@ -121,6 +130,19 @@ Finally, restart SK. Plugin should appear in your server plugins list.<br>
|
|
|
121
130
|
> NOTE: "~/.signalk" is the default signalk home on Linux. If you're
|
|
122
131
|
> getting permissions errors executing npm link, try executing "npm link" under sudo.
|
|
123
132
|
|
|
133
|
+
## KNOWN ISSUES
|
|
134
|
+
|
|
135
|
+
### Configuration Panel
|
|
136
|
+
|
|
137
|
+
- Safari 18.1 on OsX produces errors on load that kill the configuration screen. No known cause. Upgrade to most recent Safari or use Chrome.
|
|
138
|
+
- Unsaved sensor configuration changes are lost after selecting a different sensor. Be sure to Save changes for now.
|
|
139
|
+
- Renogy Rover Client, Victron GX, Victron Smart Battery Protect, and Victron VE Bus sensor classes have no default paths currently. Users will need to manually input.
|
|
140
|
+
|
|
141
|
+
### Runtime
|
|
142
|
+
|
|
143
|
+
- IMPORTANT Set `Scan for new devices interval` to `0` after configuration is complete. The plugin will run but in Bluetooth-rich environments, or if you have a long range BT 5.3 device, the system Bluetooth stack may fail after 4 hours or so.
|
|
144
|
+
- There's no way that I know of to remove a SK Path without restarting the server. So if any active paths are changed by the plugin, you'll still see them hanging around in the data browser growing stale until you restart the server.
|
|
145
|
+
|
|
124
146
|
## CONFIGURATION
|
|
125
147
|
|
|
126
148
|
After installing and restarting Signalk you should see a "BT Sensors Plugin" option in the Signalk->Server->Plugin Config page.<br><br>
|
|
@@ -266,6 +288,7 @@ Many thanks to all those who contributed to the project either with code or test
|
|
|
266
288
|
- Arjen
|
|
267
289
|
- SDLee
|
|
268
290
|
- Jordan
|
|
291
|
+
- Jan of SKipper App fame
|
|
269
292
|
|
|
270
293
|
It takes a village. Or more appropriately, an armada. Okay, regatta. But you get the idea.
|
|
271
294
|
|
package/index.js
CHANGED
|
@@ -172,10 +172,19 @@ module.exports = function (app) {
|
|
|
172
172
|
});
|
|
173
173
|
router.post('/removeSensorData', async (req, res) => {
|
|
174
174
|
app.debug(req.body)
|
|
175
|
+
const sensor = sensorMap.get(req.body.mac_address)
|
|
176
|
+
if (!sensor) {
|
|
177
|
+
res.status(404).json({message: "Sensor not found"})
|
|
178
|
+
return
|
|
179
|
+
}
|
|
175
180
|
const i = deviceConfigs.findIndex((p)=>p.mac_address==req.body.mac_address)
|
|
176
181
|
if (i>=0){
|
|
177
182
|
deviceConfigs.splice(i,1)
|
|
178
183
|
}
|
|
184
|
+
|
|
185
|
+
if (sensor.isActive())
|
|
186
|
+
await sensor.stopListening()
|
|
187
|
+
|
|
179
188
|
if (sensorMap.has(req.body.mac_address))
|
|
180
189
|
sensorMap.delete(req.body.mac_address)
|
|
181
190
|
app.savePluginOptions(
|
|
@@ -569,7 +578,6 @@ module.exports = function (app) {
|
|
|
569
578
|
progressID = setInterval(()=>{
|
|
570
579
|
channel.broadcast({"progress":++progress, "maxTimeout": maxTimeout, "deviceCount":foundConfiguredDevices, "totalDevices": deviceConfigs.length},"progress")
|
|
571
580
|
if ( foundConfiguredDevices==deviceConfigs.length){
|
|
572
|
-
app.debug("progress complete")
|
|
573
581
|
progressID,progressTimeoutID = null
|
|
574
582
|
clearTimeout(progressTimeoutID)
|
|
575
583
|
clearInterval(progressID)
|
|
@@ -578,7 +586,6 @@ module.exports = function (app) {
|
|
|
578
586
|
},1000);
|
|
579
587
|
if (progressTimeoutID==null)
|
|
580
588
|
progressTimeoutID = setTimeout(()=> {
|
|
581
|
-
app.debug("progress timed out ")
|
|
582
589
|
if (progressID) {
|
|
583
590
|
|
|
584
591
|
clearInterval(progressID);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bt-sensors-plugin-sk",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "Bluetooth Sensors for Signalk
|
|
3
|
+
"version": "1.2.2",
|
|
4
|
+
"description": "Bluetooth Sensors for Signalk - see https://www.npmjs.com/package/bt-sensors-plugin-sk for a list of supported classes",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@rjsf/bootstrap-4": "^5.24.11",
|
package/plugin_defaults.json
CHANGED
|
@@ -96,6 +96,17 @@
|
|
|
96
96
|
"default": "electrical.batteries.{batteryID}.voltage"
|
|
97
97
|
},
|
|
98
98
|
|
|
99
|
+
"power":
|
|
100
|
+
{
|
|
101
|
+
"unit": "W",
|
|
102
|
+
"default": "electrical.batteries.{batteryID}.power"
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
"impedance":
|
|
106
|
+
{
|
|
107
|
+
"unit": "Ohm",
|
|
108
|
+
"default": "electrical.batteries.{batteryID}.impedance"
|
|
109
|
+
},
|
|
99
110
|
"temperature":{
|
|
100
111
|
"unit": "K",
|
|
101
112
|
"default": "electrical.batteries.{batteryID}.temperature"
|
|
@@ -107,13 +118,21 @@
|
|
|
107
118
|
|
|
108
119
|
"capacity":{
|
|
109
120
|
"remaining":{
|
|
110
|
-
"unit":"
|
|
121
|
+
"unit":"C",
|
|
111
122
|
"default": "electrical.batteries.{batteryID}.capacity.remaining"
|
|
112
123
|
},
|
|
113
124
|
"actual":{
|
|
114
|
-
"unit":"
|
|
125
|
+
"unit":"C",
|
|
115
126
|
"default": "electrical.batteries.{batteryID}.capacity.actual"
|
|
116
127
|
},
|
|
128
|
+
"discharge":{
|
|
129
|
+
"unit":"KWh",
|
|
130
|
+
"default": "electrical.batteries.{batteryID}.capacity.dischargeSinceFull"
|
|
131
|
+
},
|
|
132
|
+
"charge":{
|
|
133
|
+
"unit":"KWh",
|
|
134
|
+
"default": "electrical.batteries.{batteryID}.capacity.totalCharge"
|
|
135
|
+
},
|
|
117
136
|
"stateOfCharge":{
|
|
118
137
|
"unit":"ratio",
|
|
119
138
|
"default": "electrical.batteries.{batteryID}.capacity.stateOfCharge"
|
|
Binary file
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
const BTSensor = require("../BTSensor");
|
|
4
|
+
|
|
5
|
+
function bytesToBase10String(bytes){
|
|
6
|
+
let s = ""
|
|
7
|
+
for (let byte of bytes){
|
|
8
|
+
s+=byte.toString(16)
|
|
9
|
+
}
|
|
10
|
+
return s
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class JunctekBMS extends BTSensor{
|
|
14
|
+
static Domain = BTSensor.SensorDomains.electrical
|
|
15
|
+
|
|
16
|
+
constructor(device, config, gattConfig) {
|
|
17
|
+
super(device, config, gattConfig)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static async identify(device){
|
|
21
|
+
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
hasGATT(){
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async initSchema(){
|
|
32
|
+
super.initSchema()
|
|
33
|
+
this.addDefaultParam("batteryID")
|
|
34
|
+
|
|
35
|
+
this.addDefaultPath("voltage","electrical.batteries.voltage")
|
|
36
|
+
|
|
37
|
+
this.addDefaultPath("current","electrical.batteries.current")
|
|
38
|
+
|
|
39
|
+
this.addDefaultPath("power","electrical.batteries.power")
|
|
40
|
+
|
|
41
|
+
this.addDefaultPath("cycles",'electrical.batteries.cycles')
|
|
42
|
+
|
|
43
|
+
this.addDefaultPath("soc",'electrical.batteries.capacity.stateOfCharge')
|
|
44
|
+
this.addDefaultPath("remainingAh",'electrical.batteries.capacity.remaining')
|
|
45
|
+
this.addDefaultPath("timeRemaining",'electrical.batteries.capacity.timeRemaining')
|
|
46
|
+
this.addDefaultPath("discharge",'electrical.batteries.capacity.dischargeSinceFull')
|
|
47
|
+
this.addDefaultPath("charge",'electrical.batteries.capacity.charge')
|
|
48
|
+
this.addDefaultPath("temperature",'electrical.batteries.temperature')
|
|
49
|
+
this.addDefaultPath("actualCapacity",'electrical.batteries.capacity.actual')
|
|
50
|
+
this.addMetadatum('impedance','mOhm', 'measured resistance')
|
|
51
|
+
.default='electrical.batteries.{batteryID}.impedance'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
emitFrom(buffer){
|
|
55
|
+
var value=[], chargeDirection = 1
|
|
56
|
+
|
|
57
|
+
for (let byte of buffer){
|
|
58
|
+
if (byte==0xBB) {
|
|
59
|
+
value=[]
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
if (byte==0xEE){
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
value.push[byte]
|
|
67
|
+
|
|
68
|
+
if (parseInt(byte.toString(16))==NaN){ //not a base-10 number. seriously. that's how Junctek does this.
|
|
69
|
+
const v = parseInt(bytesToBase10String(value))
|
|
70
|
+
switch (byte){
|
|
71
|
+
case 0xC0:{
|
|
72
|
+
emitData("voltage",v/100)
|
|
73
|
+
}
|
|
74
|
+
case 0xC1:{
|
|
75
|
+
emitData("current",(v/100)*chargeDirection)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case 0xD1:{
|
|
79
|
+
if (byte==0)
|
|
80
|
+
chargeDirection=-1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case 0xD2:{
|
|
84
|
+
emitData("remainingAh",v/1000)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case 0xD3:{
|
|
88
|
+
emitData("discharge",v/100000)
|
|
89
|
+
}
|
|
90
|
+
case 0xD4:{
|
|
91
|
+
emitData("charge",v/100000)
|
|
92
|
+
}
|
|
93
|
+
case 0xD6:{
|
|
94
|
+
emitData("timeRemaining",v*60)
|
|
95
|
+
}
|
|
96
|
+
case 0xD7:{
|
|
97
|
+
emitData("impedance",v/100)
|
|
98
|
+
}
|
|
99
|
+
case 0xD8:{
|
|
100
|
+
emitData("power",(v/100)*chargeDirection)
|
|
101
|
+
}
|
|
102
|
+
case 0xD9:{
|
|
103
|
+
emitData("temperature",v + 173.15) //assume C not F
|
|
104
|
+
}
|
|
105
|
+
case 0xB1:{
|
|
106
|
+
emitData("capacityActual",v /10 )
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
emitGATT(){
|
|
113
|
+
this.battCharacteristic.readValue()
|
|
114
|
+
.then((buffer)=>{
|
|
115
|
+
this.emitFrom(buffer)
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
initGATTConnection(){
|
|
119
|
+
return new Promise((resolve,reject )=>{ this.deviceConnect().then(async ()=>{
|
|
120
|
+
if (!this.gattServer) {
|
|
121
|
+
this.gattServer = await this.device.gatt()
|
|
122
|
+
this.battService = await this.gattServer.getPrimaryService("0000fff0-0000-1000-8000-00805f9b34fb")
|
|
123
|
+
this.battCharacteristic = await this.battService.getCharacteristic("0000ffe1-0000-1000-8000-00805f9b34fb")
|
|
124
|
+
}
|
|
125
|
+
resolve(this)
|
|
126
|
+
}) .catch((e)=>{ reject(e.message) }) })
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
initGATTNotifications() {
|
|
130
|
+
Promise.resolve(this.battCharacteristic.startNotifications().then(()=>{
|
|
131
|
+
this.battCharacteristic.on('valuechanged', buffer => {
|
|
132
|
+
this.emitFrom(buffer)
|
|
133
|
+
})
|
|
134
|
+
}))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async stopListening(){
|
|
138
|
+
super.stopListening()
|
|
139
|
+
if (this.battCharacteristic && await this.battCharacteristic.isNotifying()) {
|
|
140
|
+
await this.battCharacteristic.stopNotifications()
|
|
141
|
+
this.battCharacteristic=null
|
|
142
|
+
}
|
|
143
|
+
if (await this.device.isConnected()){
|
|
144
|
+
await this.device.disconnect()
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
module.exports=JunctekBMS
|
|
@@ -78,7 +78,7 @@ class KilovaultHLXPlus extends BTSensor{
|
|
|
78
78
|
this.addDefaultPath("cycles",'electrical.batteries.cycles')
|
|
79
79
|
.read=(buffer)=>{return buffer.readInt16LE(12)}
|
|
80
80
|
|
|
81
|
-
this.addDefaultPath("soc",'electrical.batteries.capacity
|
|
81
|
+
this.addDefaultPath("soc",'electrical.batteries.capacity.stateOfCharge')
|
|
82
82
|
.read=(buffer)=>{return buffer.readInt16LE(14)}
|
|
83
83
|
|
|
84
84
|
this.addDefaultPath("temperature",'electrical.batteries.temperature')
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
|
|
2
|
+
function arduinoDateDecode (elapsedSeconds) {
|
|
3
|
+
const date = new Date("2000-01-01")
|
|
4
|
+
date.setTime(date.getTime() + 1000 * elapsedSeconds)
|
|
5
|
+
return date
|
|
6
|
+
}
|
|
7
|
+
const errors= {
|
|
8
|
+
0: "Undefined",
|
|
9
|
+
1: "Invalid Battery",
|
|
10
|
+
2: "Overheat",
|
|
11
|
+
3: "Overheat Shutdown",
|
|
12
|
+
4: "Generator lead 1 disconnected",
|
|
13
|
+
5: "Generator lead 2 disconnected",
|
|
14
|
+
6: "Generator lead 3 disconnected",
|
|
15
|
+
7: "Short Circuit"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const eventTypes = {
|
|
19
|
+
0: "Reboot",
|
|
20
|
+
1: "Invalid Battery",
|
|
21
|
+
2: "Overheat",
|
|
22
|
+
3: "Overheat Shutdown",
|
|
23
|
+
4: "Generator lead 1 disconnected",
|
|
24
|
+
5: "Generator lead 2 disconnected",
|
|
25
|
+
6: "Generator lead 3 disconnected",
|
|
26
|
+
7: "Short Circuit",
|
|
27
|
+
255: "Debug"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const states= ["Charging Needed", "Charging", "Floating", "Idle"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
const BTSensor = require("../BTSensor");
|
|
34
|
+
class RemoranWave3 extends BTSensor{
|
|
35
|
+
static Domain = BTSensor.SensorDomains.electrical
|
|
36
|
+
serviceUUID = "81D08DF0-C0F8-422A-9D9D-E4379BB1EA3B"
|
|
37
|
+
info1CharUUID = "62C91222-FAFE-4F6E-95F0-AFC02BD19F2E"
|
|
38
|
+
info2CharUUID = "f5d12d34-4390-486c-b906-24ea8906af71"
|
|
39
|
+
eventUuid = "f12a8e25-59f7-42f2-b7ae-ba96fb25c13c"
|
|
40
|
+
|
|
41
|
+
static async identify(device){
|
|
42
|
+
|
|
43
|
+
const name = await this.getDeviceProp(device,"Name")
|
|
44
|
+
if (name == 'Remoran Wave.3')
|
|
45
|
+
return this
|
|
46
|
+
else
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
hasGATT(){
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
usingGATT(){
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
emitInfo1Data(buffer){
|
|
56
|
+
if (buffer.length < 20) {
|
|
57
|
+
app.debug(`Bad buffer size ${buffer.length}. Buffer size must be 20 bytes or more.`)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
emitData("versionNumber", buffer.readUInt8(0))
|
|
61
|
+
const errors = buffer.readUInt8(2)
|
|
62
|
+
const errorState = []
|
|
63
|
+
for (var i = 0; i < 8; ++i) {
|
|
64
|
+
var c = 1 << i;
|
|
65
|
+
r & c && errorState.push(errors[i])
|
|
66
|
+
}
|
|
67
|
+
emitData("errors", errorState)
|
|
68
|
+
emitData("state", states[buffer.readUInt8(3)])
|
|
69
|
+
emitData("rpm", buffer.readUInt32LE(4))
|
|
70
|
+
emitData( "voltage" , buffer.readFloatLE(8))
|
|
71
|
+
emitData("current", buffer.readFloatLE(12))
|
|
72
|
+
emitData( "power", buffer.readFloatLE(16))
|
|
73
|
+
|
|
74
|
+
if (buffer.length > 23) {
|
|
75
|
+
emitData( "temp", buffer.readFloatLE(20).toFixed(1))
|
|
76
|
+
emitData( "uptime", buffer.readUInt32(24))
|
|
77
|
+
if (versionNumber>1 && buffer.size > 31) {
|
|
78
|
+
emitData("energy", buffer.readFloatLE(32).toFixed(1))
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
emitInfo2Data(buffer){
|
|
84
|
+
if (buffer.size < 12) {
|
|
85
|
+
app.debug(`Bad buffer size ${buffer.length}. Buffer size must be 12 bytes or more.`)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
emitData("versionNumber", buffer.getUint8(0))
|
|
89
|
+
emitData("temp", buffer.readFloat32LE(4))
|
|
90
|
+
emitData("uptime", buffer.readUInt32(8))
|
|
91
|
+
emitData("lastBootTime", arduinoDateDecode(buffer.getUInt32LE(12)))
|
|
92
|
+
emitData("energy", buffer.readFloat32LE(16))
|
|
93
|
+
}
|
|
94
|
+
emitEventData(buffer){
|
|
95
|
+
if (buffer.size < 14) {
|
|
96
|
+
app.debug(`Bad buffer size ${buffer.length}. Buffer size must be 14 bytes or more.`)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
this.emitData("event",
|
|
100
|
+
{
|
|
101
|
+
firstDate: arduinoDateDecode(buffer.readUInt32LE(0)),
|
|
102
|
+
lastDate: arduinoDateDecode(buffer.readUInt32LE(4)),
|
|
103
|
+
eventType: buffer.readUInt16LE(8),
|
|
104
|
+
count: buffer.readUInt16LE(10),
|
|
105
|
+
index: buffer.readUInt16LE(12),
|
|
106
|
+
eventDesc: eventTypes[i]
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
emitGATT(){
|
|
111
|
+
this.info1Characteristic.readValue()
|
|
112
|
+
.then((buffer)=>
|
|
113
|
+
this.emitInfo1Data( buffer)
|
|
114
|
+
)
|
|
115
|
+
this.info2Characteristic.readValue()
|
|
116
|
+
.then((buffer)=>
|
|
117
|
+
this.emitInfo2Data(buffer)
|
|
118
|
+
)
|
|
119
|
+
this.eventCharacteristic.readValue()
|
|
120
|
+
.then((buffer)=>
|
|
121
|
+
this.emitEventData(buffer)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
}
|
|
125
|
+
initSchema(){
|
|
126
|
+
super.initSchema()
|
|
127
|
+
this.addDefaultParam("id")
|
|
128
|
+
.default="RemoranWave3"
|
|
129
|
+
|
|
130
|
+
this.getGATTParams()["useGATT"].default=true
|
|
131
|
+
|
|
132
|
+
this.addMetadatum('errorCodes','', 'charger error codes (array)')
|
|
133
|
+
.default= "electrical.chargers.{id}.errorCodes"
|
|
134
|
+
|
|
135
|
+
this.addMetadatum('state','', 'charger state')
|
|
136
|
+
.default= "electrical.chargers.{id}.state"
|
|
137
|
+
|
|
138
|
+
this.addMetadatum('voltage','V', 'battery voltage')
|
|
139
|
+
.default= "electrical.chargers.{id}.battery.voltage"
|
|
140
|
+
|
|
141
|
+
this.addMetadatum('current','A', 'battery current')
|
|
142
|
+
.default= "electrical.chargers.{id}.battery.current"
|
|
143
|
+
|
|
144
|
+
this.addMetadatum('power','W', 'battery power')
|
|
145
|
+
.default= "electrical.chargers.{id}.battery.power"
|
|
146
|
+
|
|
147
|
+
this.addMetadatum('temp', 'K', 'charger temperature')
|
|
148
|
+
.default= "electrical.chargers.{id}.temperature"
|
|
149
|
+
|
|
150
|
+
this.addMetadatum('energy', 'wh', 'energy created today in Wh')
|
|
151
|
+
.default= "electrical.chargers.{id}.energy"
|
|
152
|
+
|
|
153
|
+
this.addMetadatum('event', '', 'charger event')
|
|
154
|
+
.default= "electrical.chargers.{id}.event"
|
|
155
|
+
|
|
156
|
+
this.addMetadatum('lastBootTime', 's', 'last boot time')
|
|
157
|
+
.default= "electrical.chargers.{id}.lastBootTime"
|
|
158
|
+
|
|
159
|
+
this.addMetadatum('rpm', '', 'revolutions per minute')
|
|
160
|
+
.default= "sensors.{macAndName}.rpm"
|
|
161
|
+
|
|
162
|
+
this.addMetadatum('uptime', 's', 'charger/sensor uptime')
|
|
163
|
+
.default= "sensors.{macAndName}.uptime"
|
|
164
|
+
|
|
165
|
+
this.addMetadatum('versionNumber', '', 'charger/sensor version number')
|
|
166
|
+
.default= "sensors.{macAndName}.version"
|
|
167
|
+
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
initGATTConnection(){
|
|
172
|
+
return new Promise((resolve,reject )=>{ this.deviceConnect().then(async ()=>{
|
|
173
|
+
if (!this.gattServer) {
|
|
174
|
+
this.gattServer = await this.device.gatt()
|
|
175
|
+
this.service = await this.gattServer.getPrimaryService(this.serviceUUID)
|
|
176
|
+
this.info1Characteristic = await this.service.getCharacteristic(this.info1CharUUID)
|
|
177
|
+
this.info2Characteristic = await this.service.getPrimaryService(this.info2CharUUID)
|
|
178
|
+
this.eventCharacteristic = await this.service.getCharacteristic(this.eventUUID)
|
|
179
|
+
resolve(this)
|
|
180
|
+
}}) .catch((e)=>{ reject(e.message) }) })
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
initGATTNotifications() {
|
|
184
|
+
Promise.resolve(this.info1Characteristic.startNotifications().then(()=>{
|
|
185
|
+
this.info1Characteristic.on('valuechanged', buffer => {
|
|
186
|
+
this.emitInfo1Data(buffer)
|
|
187
|
+
})
|
|
188
|
+
}))
|
|
189
|
+
Promise.resolve(this.info2Characteristic.startNotifications().then(()=>{
|
|
190
|
+
this.info2Characteristic.on('valuechanged', buffer => {
|
|
191
|
+
this.emitInfo2Data(buffer)
|
|
192
|
+
})
|
|
193
|
+
}))
|
|
194
|
+
Promise.resolve(this.eventCharacteristic.startNotifications().then(()=>{
|
|
195
|
+
this.eventCharacteristic.on('valuechanged', buffer => {
|
|
196
|
+
this.emitEventData(buffer)
|
|
197
|
+
})
|
|
198
|
+
}))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async stopNotifications(characteristic){
|
|
202
|
+
if (characteristic && await characteristic.isNotifying()) {
|
|
203
|
+
await characteristic.stopNotifications()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async stopListening(){
|
|
207
|
+
super.stopListening()
|
|
208
|
+
await stopNotifations(this.info1Characteristic)
|
|
209
|
+
await stopNotifations(this.info2Characteristic)
|
|
210
|
+
await stopNotifations(this.eventCharacteristic)
|
|
211
|
+
if (await this.device.isConnected()){
|
|
212
|
+
await this.device.disconnect()
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
module.exports=RemoranWave3
|
|
217
|
+
|
|
@@ -86,7 +86,7 @@ class RenogySensor extends BTSensor{
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
initGATTInterval(){
|
|
89
|
-
|
|
89
|
+
this.emitGATT()
|
|
90
90
|
this.intervalID = setInterval(()=>{
|
|
91
91
|
this.emitGATT()
|
|
92
92
|
}, 1000*(this?.pollFreq??60) )
|
|
@@ -121,7 +121,8 @@ class RenogySensor extends BTSensor{
|
|
|
121
121
|
async stopListening(){
|
|
122
122
|
super.stopListening()
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
if (this.readChar)
|
|
125
|
+
await this.readChar.stopNotifications()
|
|
125
126
|
|
|
126
127
|
if (await this.device.isConnected()){
|
|
127
128
|
await this.device.disconnect()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Class to support Batteries with embedded Shenzhen Li-ion Battery Bodyguard Technology BMS
|
|
3
|
-
* Brands include Redodo, Litime PowerQueen and possibly others
|
|
3
|
+
* Brands include Redodo, Litime, PowerQueen and possibly others
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
|
|
@@ -34,19 +34,21 @@ class ShenzhenLiONBMS extends BTSensor{
|
|
|
34
34
|
static async identify(device){
|
|
35
35
|
return null
|
|
36
36
|
}
|
|
37
|
+
async sendCommand(cmd){
|
|
38
|
+
await this.txCharacteristic.writeValueWithResponse( Buffer.from(cmd))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async queryBatteryCommand(){
|
|
42
|
+
await this.sendCommand(this.constructor.Commands.query_battery_status)
|
|
43
|
+
}
|
|
44
|
+
|
|
37
45
|
hasGATT(){
|
|
38
46
|
return true
|
|
39
47
|
}
|
|
40
48
|
usingGATT(){
|
|
41
49
|
return true
|
|
42
50
|
}
|
|
43
|
-
emitGATT(){
|
|
44
|
-
this.characteristic.readValue()
|
|
45
|
-
.then((buffer)=>
|
|
46
|
-
this.emitValuesFrom( buffer)
|
|
47
|
-
)
|
|
48
51
|
|
|
49
|
-
}
|
|
50
52
|
initSchema(){
|
|
51
53
|
super.initSchema()
|
|
52
54
|
this.getGATTParams()["useGATT"].default=true
|
|
@@ -82,7 +84,7 @@ class ShenzhenLiONBMS extends BTSensor{
|
|
|
82
84
|
|
|
83
85
|
for(let cellNum=0; cellNum < this?.numberOfCells??4; cellNum++) {
|
|
84
86
|
this.addMetadatum(`cell${cellNum+1}Voltage`,'V', `cell #${cellNum+1} voltage`,
|
|
85
|
-
(buff)=>{return buff.readInt16LE(16+(cellNum*2)) })
|
|
87
|
+
(buff)=>{return buff.readInt16LE(16+(cellNum*2)) /1000})
|
|
86
88
|
.default=`electrical.batteries.{batteryID}.cells.${cellNum+1}.voltage`
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -97,11 +99,11 @@ class ShenzhenLiONBMS extends BTSensor{
|
|
|
97
99
|
(buff)=>{return buff.readInt16LE(54) + 273.15})
|
|
98
100
|
.default="electrical.batteries.{batteryID}.bms.temperature"
|
|
99
101
|
|
|
100
|
-
this.addDefaultPath('
|
|
101
|
-
.read=(buff)=>{return
|
|
102
|
+
this.addDefaultPath('remaining','electrical.batteries.capacity.remaining')
|
|
103
|
+
.read=(buff)=>{return (buff.readUInt16LE(62)/100)*3600}
|
|
102
104
|
|
|
103
|
-
this.addDefaultPath('
|
|
104
|
-
.read=(buff)=>{return
|
|
105
|
+
this.addDefaultPath('actual','electrical.batteries.capacity.actual')
|
|
106
|
+
.read=(buff)=>{return (buff.readUInt16LE(64)/100)*3600}
|
|
105
107
|
|
|
106
108
|
this.addMetadatum('heat','', 'discharge disabled due to app button = 00000080, heater_error = 00000002',
|
|
107
109
|
(buff)=>{return buff.slice(68,72).reverse().join("")})
|
|
@@ -138,7 +140,6 @@ class ShenzhenLiONBMS extends BTSensor{
|
|
|
138
140
|
this.addDefaultPath( 'soc',"electrical.batteries.capacity.stateOfCharge")
|
|
139
141
|
.read=(buff)=>{return buff.readUInt16LE(90)/100}
|
|
140
142
|
|
|
141
|
-
|
|
142
143
|
this.getJSONSchema().properties.params.required=["batteryID", "numberOfCells" ]
|
|
143
144
|
}
|
|
144
145
|
|
|
@@ -155,16 +156,25 @@ class ShenzhenLiONBMS extends BTSensor{
|
|
|
155
156
|
resolve(this)
|
|
156
157
|
})})
|
|
157
158
|
}
|
|
159
|
+
|
|
160
|
+
async initGATTInterval(){
|
|
161
|
+
await this.initGATTNotifications()
|
|
162
|
+
}
|
|
163
|
+
|
|
158
164
|
async initGATTNotifications() {
|
|
159
|
-
await this.txCharacteristic.writeValue( Buffer.from(this.constructor.Commands.query_battery_status))
|
|
160
165
|
await this.rxCharacteristic.startNotifications()
|
|
161
166
|
this.rxCharacteristic.on('valuechanged', buffer => {
|
|
162
167
|
this.emitValuesFrom(buffer)
|
|
163
168
|
})
|
|
169
|
+
this.intervalID=setInterval(
|
|
170
|
+
async ()=>{
|
|
171
|
+
await this.queryBatteryCommand()
|
|
172
|
+
}, (this?.pollFreq??10)*1000
|
|
173
|
+
)
|
|
164
174
|
}
|
|
165
175
|
|
|
166
176
|
async stopListening(){
|
|
167
|
-
super.stopListening()
|
|
177
|
+
super.stopListening() //clears IntervalID as it happens
|
|
168
178
|
if (this.rxCharacteristic && await this.rxCharacteristic.isNotifying()) {
|
|
169
179
|
await this.rxCharacteristic.stopNotifications()
|
|
170
180
|
this.rxCharacteristic=null
|
|
@@ -174,4 +184,4 @@ class ShenzhenLiONBMS extends BTSensor{
|
|
|
174
184
|
}
|
|
175
185
|
}
|
|
176
186
|
}
|
|
177
|
-
module.exports=ShenzhenLiONBMS
|
|
187
|
+
module.exports=ShenzhenLiONBMS
|
|
@@ -130,10 +130,12 @@ function sleep(x) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
getDescription(){
|
|
133
|
-
//return `<img src="https://www.victronenergy.com/_next/image?url=https%3A%2F%2Fwww.victronenergy.com%2Fupload%2Fproducts%2FSmartShunt%2520500_nw.png&w=1080&q=70"" height="150"
|
|
133
|
+
//return `<img src="https://www.victronenergy.com/_next/image?url=https%3A%2F%2Fwww.victronenergy.com%2Fupload%2Fproducts%2FSmartShunt%2520500_nw.png&w=1080&q=70"" height="150" align=”top” ></img>`
|
|
134
134
|
|
|
135
135
|
|
|
136
|
-
return `<img src="../bt-sensors-plugin-sk/images/${this.getImage()}"
|
|
136
|
+
return `<img src="../bt-sensors-plugin-sk/images/${this.getImage()}" width="200" style="float: left;
|
|
137
|
+
margin: 20px;" ></img>
|
|
138
|
+
To get the encryption key for your device, follow the instructions <a href=https://communityarchive.victronenergy.com/questions/187303/victron-bluetooth-advertising-protocol.html target="_victron_encrypt">here</a>`
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
}
|
|
@@ -56,7 +56,7 @@ class VictronBatteryMonitor extends VictronSensor{
|
|
|
56
56
|
|
|
57
57
|
const alarmMD = this.addMetadatum('alarm','', 'alarm',
|
|
58
58
|
(buff,offset=0)=>{return buff.readInt16LE(offset)})
|
|
59
|
-
alarmMD.default='
|
|
59
|
+
alarmMD.default='electrical.batteries.{batteryID}.alarm'
|
|
60
60
|
alarmMD.notify=true
|
|
61
61
|
|
|
62
62
|
this.addMetadatum( 'consumed','Ah', 'amp-hours consumed',
|
|
@@ -196,7 +196,7 @@ class VictronBatteryMonitor extends VictronSensor{
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
getDescription(){
|
|
199
|
-
return super.getDescription()
|
|
199
|
+
return `${super.getDescription()}.<p><p>After setting the encryption key, Save and reselect to configure the value of the Aux field (Secondary Battery, Midpoint or Battery Temperature)`
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
async stopListening(){
|
|
@@ -10,7 +10,7 @@ class VictronDCDCConverter extends VictronSensor{
|
|
|
10
10
|
|
|
11
11
|
initSchema(){
|
|
12
12
|
super.initSchema()
|
|
13
|
-
this.
|
|
13
|
+
this.addDefaultParam("id")
|
|
14
14
|
|
|
15
15
|
this.addMetadatum('deviceState','', 'device state',
|
|
16
16
|
(buff)=>{return VC.OperationMode.get(buff.readUInt8(0))})
|
|
@@ -24,9 +24,11 @@ class VictronLynxSmartBMS extends VictronSensor{
|
|
|
24
24
|
|
|
25
25
|
initSchema(){
|
|
26
26
|
super.initSchema()
|
|
27
|
+
this.addDefaultParam("batteryID")
|
|
27
28
|
this.addMetadatum('error','', 'error code',
|
|
28
29
|
(buff)=>{return buff.readUInt8(0)})
|
|
29
|
-
|
|
30
|
+
.default="electrical.batteries.{batteryID}.errorCode"
|
|
31
|
+
|
|
30
32
|
this.addDefaultPath('ttg','electrical.batteries.capacity.timeRemaining')
|
|
31
33
|
.read=(buff)=>{return this.NaNif(buff.readUInt16LE(1),0xFFFF)*60}
|
|
32
34
|
this.addDefaultPath('voltage','electrical.batteries.voltage')
|
|
@@ -36,10 +38,12 @@ class VictronLynxSmartBMS extends VictronSensor{
|
|
|
36
38
|
|
|
37
39
|
this.addMetadatum('ioStatus','','IO Status', //TODO
|
|
38
40
|
(buff)=>{return buff.readUInt16LE(7)})
|
|
41
|
+
.default="electrical.batteries.{batteryID}.IOStatus"
|
|
39
42
|
|
|
40
43
|
this.addMetadatum('warningsAndAlarms','','warnings and alarms')
|
|
41
44
|
|
|
42
45
|
this.addDefaultPath('soc','electrical.batteries.capacity.stateOfCharge')
|
|
46
|
+
|
|
43
47
|
this.addMetadatum('consumedAh','Ah','amp-hours consumed')
|
|
44
48
|
.default="electrical.batteries.{batteryID}.capacity.ampHoursConsumed"
|
|
45
49
|
|
|
@@ -34,7 +34,7 @@ class VictronOrionXS extends VictronSensor{
|
|
|
34
34
|
(buff)=>{return this.NaNif(buff.readUInt16LE(8),0xFFFF)/10})
|
|
35
35
|
.default="electrical.chargers.{id}.input.current"
|
|
36
36
|
this.addMetadatum('deviceOffReason','', 'device off reason',
|
|
37
|
-
(buff)=>{return VC.OffReasons(buff.readUInt32BE(10))})
|
|
37
|
+
(buff)=>{return VC.OffReasons.get(buff.readUInt32BE(10))})
|
|
38
38
|
.default="electrical.chargers.{id}.offReason"
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -50,7 +50,7 @@ class VictronSmartLithium extends VictronSensor{
|
|
|
50
50
|
(buff)=>{return buff.readUInt16LE(4)})
|
|
51
51
|
.default="electrical.batteries.{batteryID}.errors"
|
|
52
52
|
|
|
53
|
-
for (let i=0;i
|
|
53
|
+
for (let i=0;i<8; i++){
|
|
54
54
|
this.addMetadatum(`cell${i+1}Voltage`,'V', `cell ${i+1} voltage`)
|
|
55
55
|
.default=`electrical.batteries.{batteryID}.cell${i+1}.voltage`
|
|
56
56
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
diff --git a/sensor_classes/VictronSmartLithium.js b/sensor_classes/VictronSmartLithium.js
|
|
2
|
+
index 0ed70ad..2988374 100644
|
|
3
|
+
--- a/sensor_classes/VictronSmartLithium.js
|
|
4
|
+
+++ b/sensor_classes/VictronSmartLithium.js
|
|
5
|
+
@@ -50,7 +50,7 @@ class VictronSmartLithium extends VictronSensor{
|
|
6
|
+
(buff)=>{return buff.readUInt16LE(4)})
|
|
7
|
+
.default="electrical.batteries.{batteryID}.errors"
|
|
8
|
+
|
|
9
|
+
- for (let i=0;i++;i<8){
|
|
10
|
+
+ for (let i=0;i<8; i++){
|
|
11
|
+
this.addMetadatum(`cell${i+1}Voltage`,'V', `cell ${i+1} voltage`)
|
|
12
|
+
.default=`electrical.batteries.{batteryID}.cell${i+1}.voltage`
|
|
13
|
+
}
|