bt-sensors-plugin-sk 1.1.0-beta.2.2.1.4 → 1.1.0-beta.2.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.
@@ -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
@@ -0,0 +1,39 @@
1
+ const BTSensor = require("../BTSensor");
2
+
3
+ class LancolVoltageMeter extends BTSensor{
4
+
5
+ static async identify(device){
6
+
7
+ const name = await this.getDeviceProp(device,"Name")
8
+ const regex = /^Lancol *[0-9]{1,3}\.[0-9]{1,2}V/
9
+
10
+ if (name && name.match(regex))
11
+ return this
12
+ else
13
+ return null
14
+
15
+ }
16
+
17
+ async init(){
18
+ await super.init()
19
+ this.addMetadatum('voltage','V', 'battery voltage')
20
+ }
21
+
22
+ getManufacturer(){
23
+ return "Lancol"
24
+ }
25
+ async propertiesChanged(props){
26
+ super.propertiesChanged(props)
27
+
28
+ if (props.Name) {
29
+ const regex = /[+-]?([0-9]*[.])?[0-9]+/
30
+ const r = this.valueIfVariant(props.Name).match(regex)
31
+ if (r) {
32
+ this.emit("voltage",parseFloat([...r][0]))
33
+ } else {
34
+ this.debug(`Unable to parse Name property for ${this.getName()}: ${props.Name}`)
35
+ }
36
+ }
37
+ }
38
+ }
39
+ module.exports=LancolVoltageMeter
@@ -0,0 +1,109 @@
1
+ const BTHomeServiceData = require("./BTHome/BTHomeServiceData");
2
+ const AbstractBTHomeSensor = require("./BTHome/AbstractBTHomeSensor");
3
+
4
+ /**
5
+ * Sensor class representing the Shelly BLU H&T.
6
+ *
7
+ * This sensor is publishing data utilising the BTHome format and inherits from {@link AbstractBTHomeSensor}.
8
+ */
9
+ class ShellySBHT003C extends AbstractBTHomeSensor {
10
+ /**
11
+ * The shortened local name as advertised by the Shelly BLU H&T.
12
+ * @type {string}
13
+ */
14
+ static SHORTENED_LOCAL_NAME = "SBHT-003C";
15
+
16
+ async init() {
17
+ await super.init();
18
+ this.initMetadata();
19
+ }
20
+
21
+ /**
22
+ * @typedef ButtonPressEvent {string}
23
+ */
24
+ /**
25
+ * The Shelly BLU H&T only supports single press and hold press options.
26
+ * @type {Readonly<{PRESS: string, HOLD_PRESS: string}>}
27
+ */
28
+ static ButtonPressEvent = Object.freeze({
29
+ PRESS: "press",
30
+ HOLD_PRESS: "hold_press",
31
+ });
32
+
33
+ /**
34
+ * Returns the `ShellySBHT003C` sensor class if the specified device has been identified as Shelly BLU H&T.
35
+ *
36
+ * @param device The Bluetooth device to be identified.
37
+ * @returns {Promise<ShellySBHT003C|null>} Returns the sensor class if the device has been identified, or null.
38
+ */
39
+ static async identify(device) {
40
+ if (
41
+ (await ShellySBHT003C.hasBtHomeServiceData(device)) &&
42
+ (await ShellySBHT003C.hasName(
43
+ device,
44
+ ShellySBHT003C.SHORTENED_LOCAL_NAME,
45
+ ))
46
+ ) {
47
+ return ShellySBHT003C;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * Parses the relevant button press events for the Shelly BLU H&T from the specified BTHome data. This device only
54
+ * uses a subset of all available BTHome button press events.
55
+ *
56
+ * @param btHomeData {BTHomeServiceData.BthomeServiceData} The BTHome data provided by the device.
57
+ * @returns {ShellySBHT003C.ButtonPressEvent|null} The device's button state.
58
+ * @see https://shelly-api-docs.shelly.cloud/docs-ble/Devices/ht/#button-press-events
59
+ * @see https://bthome.io/format/
60
+ */
61
+ static parseShellySBHT003CButton(btHomeData) {
62
+ const buttonEvent = ShellySBHT003C.parseButton(btHomeData);
63
+ if (buttonEvent) {
64
+ if (buttonEvent === BTHomeServiceData.ButtonEventType.PRESS) {
65
+ return ShellySBHT003C.ButtonPressEvent.PRESS;
66
+ /*
67
+ * Prior to firmware version 1.0.20, the hold press event is indicated by 0xFE, which does
68
+ * not conform with the BTHome standard.
69
+ */
70
+ }
71
+ if (
72
+ buttonEvent === BTHomeServiceData.ButtonEventType.HOLD_PRESS ||
73
+ buttonEvent === 0xfe
74
+ ) {
75
+ return ShellySBHT003C.ButtonPressEvent.HOLD_PRESS;
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+
81
+ initMetadata() {
82
+ this.addMetadatum(
83
+ "battery",
84
+ "ratio",
85
+ "battery level",
86
+ ShellySBHT003C.parseBatteryLevel,
87
+ );
88
+ this.addMetadatum(
89
+ "temp",
90
+ "K",
91
+ "temperature",
92
+ ShellySBHT003C.parseTemperature,
93
+ );
94
+ this.addMetadatum(
95
+ "humidity",
96
+ "ratio",
97
+ "humidity",
98
+ ShellySBHT003C.parseHumidity,
99
+ );
100
+ this.addMetadatum(
101
+ "button",
102
+ "enum",
103
+ "button",
104
+ ShellySBHT003C.parseShellySBHT003CButton,
105
+ );
106
+ }
107
+ }
108
+
109
+ module.exports = ShellySBHT003C;
@@ -78,13 +78,13 @@ class VictronBatteryMonitor extends VictronSensor{
78
78
  break;
79
79
  case VC.AuxMode.MIDPOINT_VOLTAGE:
80
80
  this.addMetadatum('midpointVoltage','V', 'midpoint battery voltage',
81
- (buff,offset=0)=>{return buff.readInt16LE(offset)/100},
81
+ (buff,offset=0)=>{return buff.readUInt16LE(offset)/100},
82
82
  '6597ed7d-4bda-4c1e-af4b-551c4cf74769')
83
83
  break;
84
84
 
85
85
  case VC.AuxMode.TEMPERATURE:
86
86
  this.addMetadatum('temperature','K', 'House battery temperature',
87
- (buff,offset=0)=>{return (buff.readInt16LE(offset)/1000)+273.15},
87
+ (buff,offset=0)=>{return (buff.readUInt16LE(offset)/100)},
88
88
  '6597ed7d-4bda-4c1e-af4b-551c4cf74769')
89
89
  break;
90
90
  default:
@@ -114,7 +114,7 @@ class VictronBatteryMonitor extends VictronSensor{
114
114
  default:
115
115
  break
116
116
  }
117
- this.emit("current", (this.NaNif(int24.readInt24LE(decData, 8)>>2,0x3FFFFF))/1000)
117
+ this.emit("current", (this.NaNif(int24.readInt24LE(decData, 8)>>2,0x1FFFFF))/1000)
118
118
  this.emit("consumed",(this.NaNif(int24.readInt24LE(decData, 11)&0xFFFFF,0xFFFFF)) / 10) ;
119
119
  this.emit("soc", this.NaNif(((decData.readUInt16LE(13)& 0x3FFF)>>4),0x3FF)/1000)
120
120