bt-sensors-plugin-sk 1.2.1 → 1.2.3

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 CHANGED
@@ -465,13 +465,26 @@ class BTSensor extends EventEmitter {
465
465
  throw new Error("::initGATTNotifications() should be implemented by the BTSensor subclass")
466
466
  }
467
467
 
468
- deviceConnect() {
468
+ deviceConnect(reconnect=false) {
469
469
 
470
470
 
471
471
  return connectQueue.enqueue( async ()=>{
472
472
  this.debug(`Connecting... ${this.getName()}`)
473
- await this.device.connect()
473
+ await this.device.helper.callMethod('Connect')
474
+
474
475
  this.debug(`Connected to ${this.getName()}`)
476
+ if (!reconnect) {
477
+ this.device.helper.on('PropertiesChanged', (propertiesChanged) => {
478
+ if ('Connected' in propertiesChanged) {
479
+ const { value } = propertiesChanged.Connected
480
+ if (value) {
481
+ this.device.emit('connect', { connected: true })
482
+ } else {
483
+ this.device.emit('disconnect', { connected: false })
484
+ }
485
+ }
486
+ })
487
+ }
475
488
 
476
489
  try {
477
490
 
@@ -787,8 +800,6 @@ class BTSensor extends EventEmitter {
787
800
  /**
788
801
  * Listen to sensor.
789
802
  * ::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
803
  */
793
804
 
794
805
 
@@ -842,7 +853,6 @@ class BTSensor extends EventEmitter {
842
853
  if (!(path===undefined))
843
854
  this.app.handleMessage(id,
844
855
  {
845
- // $source: source,
846
856
  updates:
847
857
  [{ meta: [{path: preparePath(this, path), value: { units: pathMeta?.unit }}]}]
848
858
  })
package/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # Bluetooth Sensors for [Signal K](http://www.signalk.org)
2
- # Version 1.2.0
2
+ # Version 1.2.3
3
3
 
4
- ## WHAT IT IS
4
+ ## WHAT'S NEW SINCE VERSION 1.2.2
5
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>
6
+ Bug fixes Remoran Wave.3, JunctekBMS, and ShenzhenLiOn
7
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.
8
+ # Version 1.2.2
9
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>
10
+ ## WHAT'S NEW SINCE VERSION 1.2.1
11
11
 
12
- 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.
12
+ - Junctek BMS and Remoran Wave 3 support (Note: both are not currently field tested)
13
13
 
14
- 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).
14
+ - Fixes to ShenzhenLiOn BMS, Victron Orion XS, Victron DC DC Converter, Victron Smart Lithium Classes
15
15
 
16
16
  ## WHAT'S NEW SINCE VERSION 1.1.x
17
17
 
@@ -25,6 +25,19 @@ It's pretty easy to write and deploy your own sensor class for any currently uns
25
25
 
26
26
  - Support for multiple simultaneous GATT connections.
27
27
 
28
+
29
+ ## WHAT IT IS
30
+
31
+ 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>
32
+
33
+ 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.
34
+
35
+ 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>
36
+
37
+ 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.
38
+
39
+ 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).
40
+
28
41
  ## SUPPORTED SENSORS
29
42
 
30
43
  ### NOTE
@@ -44,6 +57,8 @@ It's pretty easy to write and deploy your own sensor class for any currently uns
44
57
  |Redodo| Rebranded LiTime |
45
58
  |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
59
  |[Lancol](www.Lancol.com)| [Micro 10C 12V Car Battery Monitor](https://www.lancol.com/product/12v-bluetooth-4-0-battery-tester-micro-10-c/)|
60
+ |[Junctek](https://www.junteks.com)|[Junctek BMS](https://www.junteks.com/pages/product/index) |
61
+ |[Remoran](https://remoran.eu)| [Remoran Wave.3](https://remoran.eu/wave.html)|
47
62
 
48
63
 
49
64
  ### Environmental
@@ -121,6 +136,19 @@ Finally, restart SK. Plugin should appear in your server plugins list.<br>
121
136
  > NOTE: "~/.signalk" is the default signalk home on Linux. If you're
122
137
  > getting permissions errors executing npm link, try executing "npm link" under sudo.
123
138
 
139
+ ## KNOWN ISSUES
140
+
141
+ ### Configuration Panel
142
+
143
+ - 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.
144
+ - Unsaved sensor configuration changes are lost after selecting a different sensor. Be sure to Save changes for now.
145
+ - 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.
146
+
147
+ ### Runtime
148
+
149
+ - 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.
150
+ - 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.
151
+
124
152
  ## CONFIGURATION
125
153
 
126
154
  After installing and restarting Signalk you should see a "BT Sensors Plugin" option in the Signalk->Server->Plugin Config page.<br><br>
@@ -266,6 +294,7 @@ Many thanks to all those who contributed to the project either with code or test
266
294
  - Arjen
267
295
  - SDLee
268
296
  - Jordan
297
+ - Jan of SKipper App fame
269
298
 
270
299
  It takes a village. Or more appropriately, an armada. Okay, regatta. But you get the idea.
271
300
 
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.1",
4
- "description": "Bluetooth Sensors for Signalk -- support for Victron devices, RuuviTag, Xiaomi, ATC and Inkbird, Ultrasonic wind meters, Mopeka tank readers, Renogy Battery and Solar Controllers, Aranet4 environment sensors, SwitchBot temp and humidity sensors, KilovaultHLXPlus smart batteries, and Govee GVH51xx temp sensors",
3
+ "version": "1.2.3",
4
+ "description": "Bluetooth Sensors for Signalk - see https://www.npmjs.com/package/bt-sensors-plugin-sk#supported-sensors for a list of supported sensors",
5
5
  "main": "index.js",
6
6
  "dependencies": {
7
7
  "@rjsf/bootstrap-4": "^5.24.11",
@@ -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":"Ah",
121
+ "unit":"C",
111
122
  "default": "electrical.batteries.{batteryID}.capacity.remaining"
112
123
  },
113
124
  "actual":{
114
- "unit":"Ah",
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"
@@ -52,10 +52,10 @@ class JBDBMS extends BTSensor {
52
52
  (buffer)=>{return buffer.readInt16BE(6) / 100}
53
53
 
54
54
  this.addDefaultPath('remainingCapacity','electrical.batteries.capacity.remaining')
55
- .read=(buffer)=>{return buffer.readUInt16BE(8) / 100}
55
+ .read=(buffer)=>{return (buffer.readUInt16BE(8) / 100)*3600}
56
56
 
57
57
  this.addDefaultPath('capacity','electrical.batteries.capacity.actual')
58
- .read=(buffer)=>{return buffer.readUInt16BE(10) / 100}
58
+ .read=(buffer)=>{return (buffer.readUInt16BE(10) / 100)*3600}
59
59
 
60
60
  this.addDefaultPath('cycles','electrical.batteries.cycles' )
61
61
  .read=(buffer)=>{return buffer.readUInt16BE(12)}
@@ -139,7 +139,7 @@ class JBDBMS extends BTSensor {
139
139
  let datasize = -1
140
140
  const timer = setTimeout(() => {
141
141
  clearTimeout(timer)
142
- reject(new Error(`Response timed out from JBDBMS device ${this.getName()}. `));
142
+ reject(new Error(`Response timed out (+30s) from JBDBMS device ${this.getName()}. `));
143
143
  }, 30000);
144
144
 
145
145
  const valChanged = async (buffer) => {
@@ -147,13 +147,12 @@ class JBDBMS extends BTSensor {
147
147
  if (buffer[0]!==0xDD || buffer.length < 5 || buffer[1] !== command)
148
148
  reject(`Invalid buffer from ${this.getName()}, not processing.`)
149
149
  else
150
- datasize=buffer[2]
150
+ datasize=buffer[3]
151
151
  }
152
152
  buffer.copy(result,offset)
153
- if (buffer[buffer.length-1]==0x77 && offset+buffer.length-6==datasize){
153
+ if (buffer[buffer.length-1]==0x77 && offset+buffer.length-7==datasize){
154
154
 
155
155
  result = Uint8Array.prototype.slice.call(result, 0, offset+buffer.length)
156
- this.debug(result)
157
156
  this.rxChar.removeAllListeners()
158
157
  clearTimeout(timer)
159
158
  if (!checkSum(result))
@@ -196,6 +195,20 @@ async getAndEmitCellVoltages(){
196
195
  }
197
196
 
198
197
  initGATTInterval(){
198
+ this.device.on("disconnect", ()=>{
199
+ if (this.isActive()) {
200
+ this.debug(`Device disconnected. Attempting to reconnect to ${this.getName()}`)
201
+ try {
202
+ this.deviceConnect(true).then(()=>{
203
+ this.debug(`Device reconnected -- ${this.getName()}`)
204
+ })
205
+ }
206
+ catch (e) {
207
+ this.debug(`Error while reconnecting to ${this.getName()}`)
208
+ }
209
+ }
210
+ })
211
+ this.emitGATT()
199
212
  this.initGATTNotifications()
200
213
  }
201
214
 
@@ -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
+ emit("voltage",v/100)
73
+ }
74
+ case 0xC1:{
75
+ emit("current",(v/100)*chargeDirection)
76
+ }
77
+
78
+ case 0xD1:{
79
+ if (byte==0)
80
+ chargeDirection=-1
81
+ }
82
+
83
+ case 0xD2:{
84
+ emit("remainingAh",v/1000)
85
+ }
86
+
87
+ case 0xD3:{
88
+ emit("discharge",v/100000)
89
+ }
90
+ case 0xD4:{
91
+ emit("charge",v/100000)
92
+ }
93
+ case 0xD6:{
94
+ emit("timeRemaining",v*60)
95
+ }
96
+ case 0xD7:{
97
+ emit("impedance",v/100)
98
+ }
99
+ case 0xD8:{
100
+ emit("power",(v/100)*chargeDirection)
101
+ }
102
+ case 0xD9:{
103
+ emit("temperature",v + 173.15) //assume C not F
104
+ }
105
+ case 0xB1:{
106
+ emit("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,stateOfCharge')
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,228 @@
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
+ this.debug(`emitting info 1 data`)
57
+ if (buffer.length < 20) {
58
+ app.debug(`Bad buffer size ${buffer.length}. Buffer size must be 20 bytes or more.`)
59
+ return
60
+ }
61
+ this.emit("versionNumber", buffer.readUInt8(0))
62
+ const errors = buffer.readUInt8(2)
63
+ const errorState = []
64
+ for (var i = 0; i < 8; ++i) {
65
+ var c = 1 << i;
66
+ errors & c && errorState.push(errors[i])
67
+ }
68
+ this.emit("errors", errorState)
69
+ this.emit("state", states[buffer.readUInt8(3)])
70
+ this.emit("rpm", buffer.readUInt32LE(4))
71
+ this.emit( "voltage" , buffer.readFloatLE(8))
72
+ this.emit("current", buffer.readFloatLE(12))
73
+ this.emit( "power", buffer.readFloatLE(16))
74
+
75
+ if (buffer.length > 23) {
76
+ this.emit( "temp", ((buffer.readFloatLE(20))+273.15))
77
+ this.emit( "uptime", buffer.readUInt32LE(24))
78
+ if (versionNumber>1 && buffer.size > 31) {
79
+ this.emit("energy", buffer.readFloatLE(32))
80
+ }
81
+ }
82
+
83
+ }
84
+ emitInfo2Data(buffer){
85
+ this.debug(`emitting info 2 data`)
86
+
87
+ if (buffer.size < 12) {
88
+ app.debug(`Bad buffer size ${buffer.length}. Buffer size must be 12 bytes or more.`)
89
+ return
90
+ }
91
+ this.emit("versionNumber", buffer.readUInt8(0))
92
+ this.emit("temp", ((buffer.readFloatLE(4))+273.15))
93
+ this.emit("uptime", buffer.readUInt32LE(8))
94
+ this.emit("lastBootTime", arduinoDateDecode(buffer.readUInt32LE(12)))
95
+ this.emit("energy", buffer.readFloatLE(16))
96
+ }
97
+ emitEventData(buffer){
98
+ this.debug(`emitting event data`)
99
+ if (buffer.length < 14) {
100
+ this.debug(buffer)
101
+ app.debug(`Bad buffer size ${buffer.length}. Buffer size must be 14 bytes or more.`)
102
+ return
103
+ }
104
+ const eventType = buffer.readUInt16LE(8)
105
+ var eventDesc = eventType.toString()
106
+ if (Object.hasOwn(eventTypes,eventType))
107
+ eventDesc = eventTypes[eventType]
108
+
109
+
110
+ this.emit("event",
111
+ {
112
+ firstDate: arduinoDateDecode(buffer.readUInt32LE(0)),
113
+ lastDate: arduinoDateDecode(buffer.readUInt32LE(4)),
114
+ eventType: eventType,
115
+ count: buffer.readUInt16LE(10),
116
+ index: buffer.readUInt16LE(12),
117
+ eventDesc: eventDesc
118
+ }
119
+ )
120
+ }
121
+ emitGATT(){
122
+ this.info1Characteristic.readValue()
123
+ .then((buffer)=>
124
+ this.emitInfo1Data( buffer)
125
+ )
126
+ this.info2Characteristic.readValue()
127
+ .then((buffer)=>
128
+ this.emitInfo2Data(buffer)
129
+ )
130
+ this.eventCharacteristic.readValue()
131
+ .then((buffer)=>
132
+ this.emitEventData(buffer)
133
+ )
134
+
135
+ }
136
+ initSchema(){
137
+ super.initSchema()
138
+ this.addDefaultParam("id")
139
+ .default="RemoranWave3"
140
+
141
+ this.getGATTParams()["useGATT"].default=true
142
+
143
+ this.addMetadatum('errorCodes','', 'charger error codes (array)')
144
+ .default= "electrical.chargers.{id}.errorCodes"
145
+
146
+ this.addMetadatum('state','', 'charger state')
147
+ .default= "electrical.chargers.{id}.state"
148
+
149
+ this.addMetadatum('voltage','V', 'battery voltage')
150
+ .default= "electrical.chargers.{id}.battery.voltage"
151
+
152
+ this.addMetadatum('current','A', 'battery current')
153
+ .default= "electrical.chargers.{id}.battery.current"
154
+
155
+ this.addMetadatum('power','W', 'battery power')
156
+ .default= "electrical.chargers.{id}.battery.power"
157
+
158
+ this.addMetadatum('temp', 'K', 'charger temperature')
159
+ .default= "electrical.chargers.{id}.temperature"
160
+
161
+ this.addMetadatum('energy', 'wh', 'energy created today in Wh')
162
+ .default= "electrical.chargers.{id}.energy"
163
+
164
+ this.addMetadatum('event', '', 'charger event')
165
+ .default= "electrical.chargers.{id}.event"
166
+
167
+ this.addMetadatum('lastBootTime', 's', 'last boot time')
168
+ .default= "electrical.chargers.{id}.lastBootTime"
169
+
170
+ this.addMetadatum('rpm', '', 'revolutions per minute')
171
+ .default= "sensors.{macAndName}.rpm"
172
+
173
+ this.addMetadatum('uptime', 's', 'charger/sensor uptime')
174
+ .default= "sensors.{macAndName}.uptime"
175
+
176
+ this.addMetadatum('versionNumber', '', 'charger/sensor version number')
177
+ .default= "sensors.{macAndName}.version"
178
+
179
+ }
180
+
181
+
182
+ initGATTConnection(){
183
+ return new Promise((resolve,reject )=>{ this.deviceConnect().then(async ()=>{
184
+ if (!this.gattServer) {
185
+ this.gattServer = await this.device.gatt()
186
+ this.service = await this.gattServer.getPrimaryService(this.serviceUUID)
187
+ this.info1Characteristic = await this.service.getCharacteristic(this.info1CharUUID)
188
+ this.info2Characteristic = await this.service.getCharacteristic(this.info2CharUUID)
189
+ this.eventCharacteristic = await this.service.getCharacteristic(this.eventUUID)
190
+ resolve(this)
191
+ }}) .catch((e)=>{ this.debug(e); reject(e.message) }) })
192
+ }
193
+
194
+ initGATTNotifications() {
195
+ Promise.resolve(this.info1Characteristic.startNotifications().then(()=>{
196
+ this.info1Characteristic.on('valuechanged', buffer => {
197
+ this.emitInfo1Data(buffer)
198
+ })
199
+ }))
200
+ Promise.resolve(this.info2Characteristic.startNotifications().then(()=>{
201
+ this.info2Characteristic.on('valuechanged', buffer => {
202
+ this.emitInfo2Data(buffer)
203
+ })
204
+ }))
205
+ Promise.resolve(this.eventCharacteristic.startNotifications().then(()=>{
206
+ this.eventCharacteristic.on('valuechanged', buffer => {
207
+ this.emitEventData(buffer)
208
+ })
209
+ }))
210
+ }
211
+
212
+ async stopNotifications(characteristic){
213
+ if (characteristic && await characteristic.isNotifying()) {
214
+ await characteristic.stopNotifications()
215
+ }
216
+ }
217
+ async stopListening(){
218
+ super.stopListening()
219
+ await this.stopNotifications(this?.info1Characteristic)
220
+ await this.stopNotifications(this?.info2Characteristic)
221
+ await this.stopNotifications(this?.eventCharacteristic)
222
+ if (await this.device.isConnected()){
223
+ await this.device.disconnect()
224
+ }
225
+ }
226
+ }
227
+ module.exports=RemoranWave3
228
+
@@ -13,7 +13,8 @@ CHARGING_STATE:
13
13
  3: 'Equalizing',
14
14
  4: 'Boost',
15
15
  5: 'Floating',
16
- 6: 'Current limiting'
16
+ 6: 'Current limiting',
17
+ 8: 'Not charging'
17
18
  },
18
19
 
19
20
  LOAD_STATE: {
@@ -57,9 +57,6 @@ class RenogySensor extends BTSensor{
57
57
  )
58
58
  }
59
59
 
60
- emitGATT(){
61
- }
62
-
63
60
  getModelName(){
64
61
  return this?.modelID??`${this.constructor.name} Unknown model`
65
62
  }
@@ -86,7 +83,6 @@ class RenogySensor extends BTSensor{
86
83
  }
87
84
 
88
85
  initGATTInterval(){
89
- this.emitGATT()
90
86
  this.intervalID = setInterval(()=>{
91
87
  this.emitGATT()
92
88
  }, 1000*(this?.pollFreq??60) )
@@ -121,7 +117,8 @@ class RenogySensor extends BTSensor{
121
117
  async stopListening(){
122
118
  super.stopListening()
123
119
 
124
- await this.readChar.stopNotifications()
120
+ if (this.readChar)
121
+ await this.readChar.stopNotifications()
125
122
 
126
123
  if (await this.device.isConnected()){
127
124
  await this.device.disconnect()
@@ -6,53 +6,113 @@ const RenogySensor = require("./Renogy/RenogySensor.js");
6
6
  const RC=require("./Renogy/RenogyConstants.js")
7
7
 
8
8
  class RenogyRoverClient extends RenogySensor {
9
-
9
+ /*
10
+ "batteryType": "electrical.charger.battery.type",
11
+ "batteryPercentage": "electrical.charger.battery.charge",
12
+ "batteryVoltage": "electrical.charger.battery.voltage",
13
+ "batteryCurrent": "electrical.charger.battery.current",
14
+ "controllerTemperature": "electrical.charger.temperature",
15
+ "batteryTemperature": "electrical.charger.battery.temperature",
16
+ "loadVoltage": "electrical.charger.load.voltage",
17
+ "loadCurrent": "electrical.charger.load.current",
18
+ "loadPower": "electrical.charger.load.power",
19
+ "pvVoltage": "electrical.charger.solar.voltage",
20
+ "pvCurrent": "electrical.charger.solar.current",
21
+ "pvPower": "electrical.charger.solar.power",
22
+ "maxChargingPowerToday": "electrical.charger.today.max",
23
+ "maxDischargingPowerToday": "electrical.charger.discharging.maximum",
24
+ "chargingAmpHoursToday": "electrical.charger.charged.today",
25
+ "powerGenerationToday": "electrical.charger.power.today",
26
+ "powerGenerationTotal": "electrical.charger.power.total",
27
+ "loadStatus": "electrical.charger.load.status",
28
+ "chargingStatus": "electrical.charger.status"
29
+ */
10
30
 
11
31
  initSchema(){
12
32
  //Buffer(73) [1, 3, 68, 32, 32, 82, 78, 71, 45, 67, 84, 82, 76, 45, 87, 78, 68, 51, 48, 7, 140, 0, 132, 0, 126, 0, 120, 0, 111, 0, 106, 100, 50, 0, 5, 0, 120, 0, 120, 0, 28, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 5, 0, 5, 2, 148, 0, 5, 206, 143, 34, 228, buffer: ArrayBuffer(8192), byteLength: 73, byteOffset: 6144, length: 73, Symbol(Symbol.toStringTag): 'Uint8Array']
13
33
  super.initSchema()
14
34
  this.addMetadatum('batteryType', '', "battery type")
35
+ .default="electrical.chargers.{id}.battery.type"
15
36
  this.addMetadatum('batteryPercentage', 'ratio', "battery percentage",
16
37
  (buffer)=>{return buffer.readUInt16BE(3) })
38
+ .default="electrical.chargers.{id}.battery.soc"
39
+
17
40
  this.addMetadatum('batteryVoltage', 'V', "battery voltage",
18
41
  (buffer)=>{return buffer.readUInt16BE((5))/10})
42
+ .default="electrical.chargers.{id}.battery.voltage"
43
+
19
44
  this.addMetadatum('batteryCurrent', 'A', 'battery current',
20
45
  (buffer)=>{return buffer.readUInt16BE((7))/100})
46
+ .default="electrical.chargers.{id}.battery.current"
47
+
21
48
  this.addMetadatum('controllerTemperature', 'K', 'controller temperature',
22
49
  (buffer)=>{return buffer.readInt8((9))+273.15})
50
+ .default="electrical.chargers.{id}.controller.temperature"
51
+
23
52
  this.addMetadatum('batteryTemperature', 'K', 'battery temperature',
24
53
  (buffer)=>{return buffer.readInt8((10))+273.15})
54
+ .default="electrical.chargers.{id}.battery.temperature"
55
+
25
56
  this.addMetadatum('loadVoltage', 'V', 'load voltage',
26
57
  (buffer)=>{return buffer.readUInt16BE((11))/10})
58
+ .default="electrical.chargers.{id}.load.voltage"
59
+
27
60
  this.addMetadatum('loadCurrent', 'A', 'load current',
28
61
  (buffer)=>{return buffer.readUInt16BE((13))/100})
62
+ .default="electrical.chargers.{id}.load.current"
29
63
  this.addMetadatum('loadPower', 'W', 'load power',
30
64
  (buffer)=>{return buffer.readUInt16BE((15))})
65
+ .default="electrical.chargers.{id}.load.power"
31
66
  this.addMetadatum('pvVoltage', 'V', 'pv voltage',
32
67
  (buffer)=>{return buffer.readUInt16BE((17))/10})
68
+ .default="electrical.chargers.{id}.solar.voltage"
33
69
  this.addMetadatum('pvCurrent', 'A', 'pv current',
34
70
  (buffer)=>{return buffer.readUInt16BE((19))/100})
71
+ .default="electrical.chargers.{id}.solar.current"
35
72
  this.addMetadatum('pvPower', 'W', 'pv power',
36
73
  (buffer)=>{return buffer.readUInt16BE(21)})
74
+ .default="electrical.chargers.{id}.solar.power"
37
75
  this.addMetadatum('maxChargingPowerToday', 'W', 'max charging power today',
38
76
  (buffer)=>{return buffer.readUInt16BE(33)})
77
+ .default="electrical.chargers.{id}.charge.max.today"
39
78
  this.addMetadatum('maxDischargingPowerToday', 'W', 'max discharging power today',
40
79
  (buffer)=>{return buffer.readUInt16BE(35)})
80
+ .default="electrical.chargers.{id}.discharge.max.today"
41
81
  this.addMetadatum('chargingAmpHoursToday', 'Ah', 'charging amp hours today',
42
82
  (buffer)=>{return buffer.readUInt16BE(37)})
83
+ .default="electrical.chargers.{id}.charge.ampHours.today"
84
+
43
85
  this.addMetadatum('dischargingAmpHoursToday', 'Ah', 'discharging amp hours today',
44
86
  (buffer)=>{return buffer.readUInt16BE(39)})
87
+ .default="electrical.chargers.{id}.discharge.ampHours.today"
88
+
45
89
  this.addMetadatum('powerGenerationToday', 'W', 'power generation today',
46
90
  (buffer)=>{return buffer.readUInt16BE(41)})
91
+ .default="electrical.chargers.{id}.power.generated.today"
92
+
47
93
  this.addMetadatum('powerConsumptionToday', 'W', 'power consumption today',
48
94
  (buffer)=>{return buffer.readUInt16BE(43)})
95
+ .default="electrical.chargers.{id}.power.consumed.today"
96
+
49
97
  this.addMetadatum('powerGenerationTotal', 'W', 'power generation total',
50
98
  (buffer)=>{return buffer.readUInt32BE(59)})
99
+ .default="electrical.chargers.{id}.power.generated.total"
100
+
51
101
  this.addMetadatum('loadStatus', '', 'load status',
52
102
  (buffer)=>{return RC.LOAD_STATE[buffer.readUInt8(67)>>7]})
103
+ .default="electrical.chargers.{id}.load.status"
53
104
 
54
105
  this.addMetadatum('chargingStatus', '', 'charging status',
55
- (buffer)=>{return RC.CHARGING_STATE[buffer.readUInt8(68)]})
106
+ (buffer)=>{
107
+ const cs = buffer.readUInt8(68)
108
+ if (Object.hasOwn(RC.CHARGING_STATE,cs))
109
+ return RC.CHARGING_STATE[cs]
110
+ else
111
+ return null
112
+ })
113
+
114
+ .default="electrical.chargers.{id}.charge.status"
115
+
56
116
  }
57
117
 
58
118
  retrieveDeviceID(){
@@ -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('remainingAh','electrical.batteries.capacity.remaining')
101
- .read=(buff)=>{return this.buff.readUInt16LE(62)/100}
102
+ this.addDefaultPath('remaining','electrical.batteries.capacity.remaining')
103
+ .read=(buff)=>{return (buff.readUInt16LE(62)/100)*3600}
102
104
 
103
- this.addDefaultPath('actualAh','electrical.batteries.capacity.actual')
104
- .read=(buff)=>{return this.buff.readUInt16LE(64)/100}
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
@@ -1,6 +1,6 @@
1
1
  Images= {
2
2
  generic: "victron-min-1.jpg ",
3
- shunt: "Victron-SmartShunt.jpg",
3
+ shunt: "SmartShunt_500_nw.png",
4
4
  bmv: "BMV-712-Smart-Front.webp",
5
5
  smartSolar: "smartsolarMPPT7515.png"
6
6
  }
@@ -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" object-fit="cover" ></img>`
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()}" height="300" object-fit="cover" ></img>`
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='"electrical.batteries.{batteryID}.alarm'
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.addDefaultPath("id")
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++;i<8){
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
+ }