bt-sensors-plugin-sk 1.1.0-beta.2.2.1.4 → 1.1.0-beta.2.2.1.5
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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bt-sensors-plugin-sk",
|
|
3
|
-
"version": "1.1.0-beta.2.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
|
|
3
|
+
"version": "1.1.0-beta.2.2.1.5",
|
|
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",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"dbus-next": "^0.10.2",
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Sensor class for monitoring Kilovault HLX+ batteries.
|
|
3
|
+
|
|
4
|
+
Status from the battery is collected via the BLE notify method. A complete status
|
|
5
|
+
message consists of several fragments, which must be pieced back together. The
|
|
6
|
+
format of the status message is described below. All data fields are hexidecimal
|
|
7
|
+
character strings in little-endian order (e.g. "0F34" is four characters, not just two
|
|
8
|
+
bytes, and represents 0x340F (13327 decimal)).
|
|
9
|
+
|
|
10
|
+
Index Length Description
|
|
11
|
+
0 1 indicator (0xb0) for start of message
|
|
12
|
+
1 4 Voltage, in millivolts
|
|
13
|
+
5 4 ???
|
|
14
|
+
9 8 Current (milliamps), two's complement for negative values
|
|
15
|
+
17 8 Energy Capacity (milliwatt-hours)
|
|
16
|
+
25 4 Number of charge cycles
|
|
17
|
+
29 4 State of Charge (SOC) (%)
|
|
18
|
+
33 4 Temperature * 10 (deci-°K)
|
|
19
|
+
37 4 status ???
|
|
20
|
+
41 4 AFE status ???
|
|
21
|
+
45 4 Cell 1 Voltage (millivolts)
|
|
22
|
+
49 4 Cell 2 Voltage (millivolts)
|
|
23
|
+
53 4 Cell 3 Voltage (millivolts)
|
|
24
|
+
57 4 Cell 4 Voltage (millivolts)
|
|
25
|
+
61 16 all zeroes
|
|
26
|
+
77 16 all zeroes
|
|
27
|
+
93 16 all zeroes
|
|
28
|
+
109 4 16-bit CRC of bytes 1 - 108, represented in big-endian order
|
|
29
|
+
113 8 eight 'R's; i.e. 'RRRRRRRR'
|
|
30
|
+
|
|
31
|
+
The format was derived from:
|
|
32
|
+
https://github.com/fancygaphtrn/esphome/tree/master/my_components/kilovault_bms_ble
|
|
33
|
+
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const BTSensor = require("../BTSensor");
|
|
37
|
+
class KilovaultHLXPlus extends BTSensor{
|
|
38
|
+
constructor(device, config, gattConfig) {
|
|
39
|
+
super(device, config, gattConfig)
|
|
40
|
+
this.accumulated_buffer = Buffer.alloc(0)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static async identify(device){
|
|
44
|
+
const regex = /^\d\d\-(12|24|36)00HLX\+\d{4}/
|
|
45
|
+
// This regex will match factory-assigned names (e.g. "21-2400HLX+0013").
|
|
46
|
+
// If you have renamed the battery, you will need to manually select the sensor
|
|
47
|
+
// type during configuration.
|
|
48
|
+
|
|
49
|
+
const name = await this.getDeviceProp(device,'Name')
|
|
50
|
+
if (name && name.match(regex))
|
|
51
|
+
return this
|
|
52
|
+
else
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
hasGATT(){
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
emitGATT(){
|
|
60
|
+
// Nothing to do here. HLX+ only reports via BLE notify, not BLE read
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async init(){
|
|
64
|
+
await super.init()
|
|
65
|
+
|
|
66
|
+
this.addMetadatum("voltage","V","Battery Voltage",
|
|
67
|
+
(buffer)=>{return Number(buffer.readInt16LE(0)) / 1000})
|
|
68
|
+
this.addMetadatum("current","A","Battery Current",
|
|
69
|
+
(buffer)=>{return buffer.readInt32LE(4) / 1000})
|
|
70
|
+
this.addMetadatum("energy","AHr","Battery Capacity",
|
|
71
|
+
(buffer)=>{return buffer.readInt32LE(8) / 1000})
|
|
72
|
+
this.addMetadatum("cycles","","Number of Charge Cycles",
|
|
73
|
+
(buffer)=>{return buffer.readInt16LE(12)})
|
|
74
|
+
this.addMetadatum("soc","ratio","Battery State of Charge",
|
|
75
|
+
(buffer)=>{return buffer.readInt16LE(14)})
|
|
76
|
+
this.addMetadatum("temperature","K","Battery Temperature",
|
|
77
|
+
(buffer)=>{return buffer.readInt16LE(16)/10 })
|
|
78
|
+
|
|
79
|
+
this.addMetadatum("status","","Battery Status",
|
|
80
|
+
(buffer)=>{return buffer.readInt16LE(18) })
|
|
81
|
+
this.addMetadatum("AFEStatus","","Battery AFE Status",
|
|
82
|
+
(buffer)=>{return buffer.readInt16LE(20) })
|
|
83
|
+
|
|
84
|
+
this.addMetadatum("cell1_voltage","V","Cell 1 Voltage",
|
|
85
|
+
(buffer)=>{return buffer.readInt16LE(22) / 1000})
|
|
86
|
+
this.addMetadatum("cell2_voltage","V","Cell 2 Voltage",
|
|
87
|
+
(buffer)=>{return buffer.readInt16LE(24) / 1000})
|
|
88
|
+
this.addMetadatum("cell3_voltage","V","Cell 3 Voltage",
|
|
89
|
+
(buffer)=>{return buffer.readInt16LE(26) / 1000})
|
|
90
|
+
this.addMetadatum("cell4_voltage","V","Cell 4 Voltage",
|
|
91
|
+
(buffer)=>{return buffer.readInt16LE(28) / 1000})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Concatentate chunks received by notification into a complete message.
|
|
95
|
+
// A message begins with 0xb0, is 121 bytes long, and ends with eight 'R's.
|
|
96
|
+
// Preceding the string of eight 'R's, is a 16-bit CRC, calculated using
|
|
97
|
+
// bytes 1 - 108.
|
|
98
|
+
//
|
|
99
|
+
reassemble(chunk) {
|
|
100
|
+
try {
|
|
101
|
+
if (this.accumulated_buffer.length > 121) {
|
|
102
|
+
// If no complete message by now, we must have accumulated some garbage,
|
|
103
|
+
// so start over.
|
|
104
|
+
this.accumulated_buffer = Buffer.alloc(0)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Add the new chunk to the end of the buffer.
|
|
108
|
+
this.accumulated_buffer = Buffer.concat([this.accumulated_buffer, chunk])
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
console.log("buffer error: ", err)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
// Discard contents of buffer before the first 0xb0.
|
|
116
|
+
const startByte = this.accumulated_buffer.indexOf(0xb0)
|
|
117
|
+
if (startByte == -1) return
|
|
118
|
+
this.accumulated_buffer = this.accumulated_buffer.subarray(startByte)
|
|
119
|
+
|
|
120
|
+
// At this point, the buffer begins with 0xb0, which could be the start of
|
|
121
|
+
// a message, or, if something was lost, part of the CRC. If part of the CRC,
|
|
122
|
+
// this (partial) message will eventually be discarded.
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
console.log("buffer error: ", err)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// Look for 'RRRRRRRR' (eight 'R's), marking the end of the message.
|
|
130
|
+
const firstR = this.accumulated_buffer.indexOf('RRRRRRRR')
|
|
131
|
+
if (firstR == -1) return // haven't received end of message yet
|
|
132
|
+
|
|
133
|
+
// Copy the message to <message>, ignoring the 0xb0 at the beginning and
|
|
134
|
+
// the 'R's at the end.
|
|
135
|
+
const msg_buffer = this.accumulated_buffer.subarray(1, firstR)
|
|
136
|
+
|
|
137
|
+
// Remove the message from the buffer.
|
|
138
|
+
this.accumulated_buffer = this.accumulated_buffer.subarray(firstR+8)
|
|
139
|
+
|
|
140
|
+
// We might now have a complete message. The actual message is in bytes 0-107,
|
|
141
|
+
// and the CRC is in bytes 108-111.
|
|
142
|
+
if (msg_buffer.length != 112) return
|
|
143
|
+
|
|
144
|
+
const rcvd_crc = Buffer.from(msg_buffer.subarray(108).toString(), 'hex').readUInt16BE()
|
|
145
|
+
const message = Buffer.from(msg_buffer.subarray(0, 108).toString(), 'hex')
|
|
146
|
+
|
|
147
|
+
// Calculate and Verify the CRC.
|
|
148
|
+
let calc_crc = 0
|
|
149
|
+
for (const byte of message) {
|
|
150
|
+
calc_crc += byte
|
|
151
|
+
}
|
|
152
|
+
calc_crc = calc_crc & 0xffff
|
|
153
|
+
|
|
154
|
+
if (rcvd_crc != calc_crc) throw new Error("invalid CRC, buffer:" + msg_buffer.toString())
|
|
155
|
+
|
|
156
|
+
// Parse the message and emit the values.
|
|
157
|
+
this.emitData("voltage", message)
|
|
158
|
+
this.emitData("current", message)
|
|
159
|
+
this.emitData("cycles", message)
|
|
160
|
+
this.emitData("soc", message)
|
|
161
|
+
this.emitData("temperature", message)
|
|
162
|
+
this.emitData("energy", message)
|
|
163
|
+
|
|
164
|
+
this.emitData("status", message)
|
|
165
|
+
this.emitData("AFEStatus", message)
|
|
166
|
+
|
|
167
|
+
this.emitData("cell1_voltage", message)
|
|
168
|
+
this.emitData("cell2_voltage", message)
|
|
169
|
+
this.emitData("cell3_voltage", message)
|
|
170
|
+
this.emitData("cell4_voltage", message)
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
console.log("buffer error: ", err)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
initGATTConnection(){
|
|
178
|
+
return new Promise((resolve,reject )=>{ this.device.connect().then(async ()=>{
|
|
179
|
+
if (!this.gattServer) {
|
|
180
|
+
this.gattServer = await this.device.gatt()
|
|
181
|
+
this.battService = await this.gattServer.getPrimaryService("0000ffe0-0000-1000-8000-00805f9b34fb")
|
|
182
|
+
this.battCharacteristic = await this.battService.getCharacteristic("0000ffe4-0000-1000-8000-00805f9b34fb")
|
|
183
|
+
}
|
|
184
|
+
resolve(this)
|
|
185
|
+
}) .catch((e)=>{ reject(e.message) }) })
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
initGATTNotifications() {
|
|
189
|
+
Promise.resolve(this.battCharacteristic.startNotifications().then(()=>{
|
|
190
|
+
this.battCharacteristic.on('valuechanged', buffer => {
|
|
191
|
+
this.reassemble(buffer)
|
|
192
|
+
})
|
|
193
|
+
}))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async stopListening(){
|
|
197
|
+
super.stopListening()
|
|
198
|
+
if (this.battCharacteristic && await this.battCharacteristic.isNotifying()) {
|
|
199
|
+
await this.battCharacteristic.stopNotifications()
|
|
200
|
+
this.battCharacteristic=null
|
|
201
|
+
}
|
|
202
|
+
if (await this.device.isConnected()){
|
|
203
|
+
await this.device.disconnect()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
module.exports=KilovaultHLXPlus
|