bt-sensors-plugin-sk 1.0.0 → 1.0.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 +6 -4
- package/index.js +10 -2
- package/package.json +1 -1
- package/sensor_classes/SmartShunt.js +269 -0
- package/sensor_classes/SmartShunt_GATT.js +3 -0
- package/sensor_classes/TPS.js +24 -2
package/BTSensor.js
CHANGED
|
@@ -22,10 +22,7 @@ class BTSensor {
|
|
|
22
22
|
static needsScannerOn(){
|
|
23
23
|
return true
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
* @returns empty array
|
|
28
|
-
*/
|
|
25
|
+
|
|
29
26
|
static events() {
|
|
30
27
|
throw new Error("events() static function must be implemented by subclass")
|
|
31
28
|
}
|
|
@@ -41,6 +38,11 @@ class BTSensor {
|
|
|
41
38
|
static unitFor(id){
|
|
42
39
|
return this.metadata.get(id)?.unit
|
|
43
40
|
}
|
|
41
|
+
|
|
42
|
+
static instantiable(){
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
44
46
|
/**
|
|
45
47
|
* Connect to sensor.
|
|
46
48
|
* This is where the logic for connecting to sensor, listening for changes in values and emitting those values go
|
package/index.js
CHANGED
|
@@ -5,6 +5,13 @@ const {createBluetooth} = require('node-ble')
|
|
|
5
5
|
const {bluetooth, destroy} = createBluetooth()
|
|
6
6
|
|
|
7
7
|
const BTSensor = require('./BTSensor.js')
|
|
8
|
+
|
|
9
|
+
const Device = require('./node_modules/node-ble/src/Device.js')
|
|
10
|
+
|
|
11
|
+
Device.prototype.getUUIDs=async function() {
|
|
12
|
+
return this.helper.prop('UUIDs')
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
module.exports = function (app) {
|
|
9
16
|
const discoveryTimeout = 30
|
|
10
17
|
const adapterID = 'hci0'
|
|
@@ -23,7 +30,7 @@ module.exports = function (app) {
|
|
|
23
30
|
//But there's a fail safe because I'm a reasonable man.
|
|
24
31
|
|
|
25
32
|
try{
|
|
26
|
-
utilities_sk = require('../
|
|
33
|
+
utilities_sk = require('../utilities-sk/utilities.js')
|
|
27
34
|
}
|
|
28
35
|
catch (error){
|
|
29
36
|
try {
|
|
@@ -67,6 +74,7 @@ module.exports = function (app) {
|
|
|
67
74
|
|
|
68
75
|
function loadClassMap() {
|
|
69
76
|
classMap = utilities_sk.loadClasses(path.join(__dirname, 'sensor_classes'))
|
|
77
|
+
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
app.debug('Loading plugin')
|
|
@@ -133,6 +141,7 @@ module.exports = function (app) {
|
|
|
133
141
|
}
|
|
134
142
|
function updateClassProperties(){
|
|
135
143
|
plugin.schema.properties.peripherals.items.properties.BT_class.enum=[...classMap.keys()]
|
|
144
|
+
plugin.schema.properties.peripherals.items.dependencies.BT_class.oneOf=[]
|
|
136
145
|
classMap.forEach(( cls, className )=>{
|
|
137
146
|
var oneOf = {properties:{BT_class:{enum:[className]}}}
|
|
138
147
|
cls.metadata.forEach((metadatum,tag)=>{
|
|
@@ -205,7 +214,6 @@ module.exports = function (app) {
|
|
|
205
214
|
throw new Error(`File for Class ${peripheral.BT_class} not found.`)
|
|
206
215
|
createPaths(sensorClass, peripheral)
|
|
207
216
|
|
|
208
|
-
|
|
209
217
|
peripheral.sensor = new sensorClass(device);
|
|
210
218
|
await peripheral.sensor.connect();
|
|
211
219
|
for (const tag of sensorClass.metadataTags()){
|
package/package.json
CHANGED
|
@@ -1,5 +1,274 @@
|
|
|
1
1
|
const BTSensor = require("../BTSensor");
|
|
2
|
+
/*
|
|
3
|
+
/ Assuming 'Int8ul' and 'Int16ul' are available from a library like 'construct-js'
|
|
4
|
+
// or you have your own implementation for parsing unsigned integers from bytes
|
|
2
5
|
|
|
6
|
+
// Assuming the following classes are defined elsewhere
|
|
7
|
+
// import { Device, DeviceData } from './base';
|
|
8
|
+
// import { BatteryMonitor, BatteryMonitorData } from './battery_monitor';
|
|
9
|
+
// import { BatterySense, BatterySenseData } from './battery_sense';
|
|
10
|
+
// import { DcEnergyMeter, DcEnergyMeterData } from './dc_energy_meter';
|
|
11
|
+
// import { DcDcConverter, DcDcConverterData } from './dcdc_converter';
|
|
12
|
+
// import { LynxSmartBMS, LynxSmartBMSData } from './lynx_smart_bms';
|
|
13
|
+
// import { SolarCharger, SolarChargerData } from './solar_charger';
|
|
14
|
+
// import { VEBus, VEBusData } from './vebus';
|
|
15
|
+
|
|
16
|
+
// Export the necessary symbols (adjust as needed for your module system)
|
|
17
|
+
export {
|
|
18
|
+
AuxMode,
|
|
19
|
+
Device,
|
|
20
|
+
DeviceData,
|
|
21
|
+
BatteryMonitor,
|
|
22
|
+
BatteryMonitorData,
|
|
23
|
+
BatterySense,
|
|
24
|
+
BatterySenseData,
|
|
25
|
+
DcDcConverter,
|
|
26
|
+
DcDcConverterData,
|
|
27
|
+
DcEnergyMeter,
|
|
28
|
+
DcEnergyMeterData,
|
|
29
|
+
LynxSmartBMS,
|
|
30
|
+
LynxSmartBMSData,
|
|
31
|
+
SolarCharger,
|
|
32
|
+
SolarChargerData,
|
|
33
|
+
VEBus,
|
|
34
|
+
VEBusData,
|
|
35
|
+
detectDeviceType, // Expose the function for use
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Model parser override map
|
|
39
|
+
const MODEL_PARSER_OVERRIDE = {
|
|
40
|
+
0xA3A4: BatterySense,
|
|
41
|
+
0xA3A5: BatterySense,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function detectDeviceType(data) {
|
|
45
|
+
const modelId = Int16ul.parse(data.slice(2, 4));
|
|
46
|
+
const mode = Int8ul.parse(data.slice(4, 5));
|
|
47
|
+
|
|
48
|
+
// Model ID-based preferences
|
|
49
|
+
const match = MODEL_PARSER_OVERRIDE[modelId];
|
|
50
|
+
if (match) {
|
|
51
|
+
return match;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Defaults based on mode
|
|
55
|
+
switch (mode) {
|
|
56
|
+
case 0x2: // BatteryMonitor
|
|
57
|
+
return BatteryMonitor;
|
|
58
|
+
case 0xD: // DcEnergyMeter
|
|
59
|
+
return DcEnergyMeter;
|
|
60
|
+
case 0x4: // DcDcConverter
|
|
61
|
+
return DcDcConverter;
|
|
62
|
+
case 0xA: // LynxSmartBMS
|
|
63
|
+
return LynxSmartBMS;
|
|
64
|
+
case 0x1: // SolarCharger
|
|
65
|
+
return SolarCharger;
|
|
66
|
+
case 0xC: // VE.Bus
|
|
67
|
+
return VEBus;
|
|
68
|
+
// Add cases for other modes as needed
|
|
69
|
+
default:
|
|
70
|
+
return null; // Or handle unrecognized modes appropriately
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
*/
|
|
74
|
+
/*
|
|
75
|
+
import { AES, Counter } from "crypto-js"; // Assuming you're using 'crypto-js' for crypto functions
|
|
76
|
+
|
|
77
|
+
// Enums
|
|
78
|
+
const OperationMode = {
|
|
79
|
+
OFF: 0,
|
|
80
|
+
LOW_POWER: 1,
|
|
81
|
+
FAULT: 2,
|
|
82
|
+
BULK: 3,
|
|
83
|
+
ABSORPTION: 4,
|
|
84
|
+
FLOAT: 5,
|
|
85
|
+
STORAGE: 6,
|
|
86
|
+
EQUALIZE_MANUAL: 7,
|
|
87
|
+
INVERTING: 9,
|
|
88
|
+
POWER_SUPPLY: 11,
|
|
89
|
+
STARTING_UP: 245,
|
|
90
|
+
REPEATED_ABSORPTION:
|
|
91
|
+
246,
|
|
92
|
+
RECONDITION: 247,
|
|
93
|
+
BATTERY_SAFE: 248,
|
|
94
|
+
EXTERNAL_CONTROL: 252,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const ChargerError = {
|
|
98
|
+
// ... (rest of the ChargerError enum)
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const OffReason = {
|
|
102
|
+
// ... (rest of the OffReason enum)
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const AlarmReason = {
|
|
106
|
+
// ... (rest of the AlarmReason enum)
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const ACInState = {
|
|
110
|
+
// ... (rest of the ACInState enum)
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const MODEL_ID_MAPPING = {
|
|
114
|
+
// ... (rest of the MODEL_ID_MAPPING)
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Classes
|
|
118
|
+
|
|
119
|
+
class DeviceData {
|
|
120
|
+
constructor(model_id, data) {
|
|
121
|
+
this._model_id = model_id;
|
|
122
|
+
this._data = data;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
get_model_name() {
|
|
126
|
+
return MODEL_ID_MAPPING[this._model_id] || `<Unknown device: ${this._model_id}>`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class Device {
|
|
131
|
+
static data_type = DeviceData;
|
|
132
|
+
|
|
133
|
+
// Define the packet structure
|
|
134
|
+
static PARSER = {
|
|
135
|
+
prefix: { size: 2, type: "GreedyBytes" }, // Assuming you have a way to handle 'GreedyBytes'
|
|
136
|
+
model_id: "Int16ul", // Assuming you have a way to handle unsigned 16-bit ints
|
|
137
|
+
readout_type: "Int8sl", // Assuming you have a way to handle signed 8-bit ints
|
|
138
|
+
iv: "Int16ul",
|
|
139
|
+
encrypted_data: "GreedyBytes",
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
constructor(advertisement_key) {
|
|
143
|
+
this.advertisement_key = advertisement_key;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get_model_id(data) {
|
|
147
|
+
// Replace with your parsing logic based on the structure
|
|
148
|
+
const parsedData = this.parsePacket(data);
|
|
149
|
+
return parsedData.model_id;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
decrypt(data) {
|
|
153
|
+
const container = this.parsePacket(data);
|
|
154
|
+
|
|
155
|
+
const advertisement_key = CryptoJS.enc.Hex.parse(this.advertisement_key); // Convert hex string to WordArray
|
|
156
|
+
|
|
157
|
+
// The first data byte is a key check byte
|
|
158
|
+
if (container.encrypted_data[0] !== advertisement_key.words[0] & 0xFF) {
|
|
159
|
+
throw new Error("Incorrect advertisement key"); // Or a custom AdvertisementKeyMismatchError
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const ivWords = [container.iv & 0xFFFF, 0]; // Convert IV to WordArray (little-endian)
|
|
163
|
+
const ctr = Counter.create({ words: ivWords });
|
|
164
|
+
|
|
165
|
+
const cipher = AES.createEncryptor(advertisement_key, { iv: ctr, mode: CryptoJS.mode.CTR });
|
|
166
|
+
const paddedData = this.padData(container.encrypted_data.slice(1)); // Pad data (implementation needed)
|
|
167
|
+
const decrypted = cipher.process(paddedData);
|
|
168
|
+
|
|
169
|
+
return decrypted; // You might need to convert this to bytes depending on your usage
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
parse(data) {
|
|
173
|
+
const decrypted = this.decrypt(data);
|
|
174
|
+
const parsed = this.parse_decrypted(decrypted);
|
|
175
|
+
const model = this.get_model_id(data);
|
|
176
|
+
return new this.constructor.data_type(model, parsed);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Abstract method to be implemented by subclasses
|
|
180
|
+
parse_decrypted(decrypted) {
|
|
181
|
+
throw new Error("parse_decrypted method must be implemented by subclass");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Replace this with your actual packet parsing logic based on your 'Struct' implementation
|
|
185
|
+
parsePacket(data) {
|
|
186
|
+
// ... (Your parsing logic here)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Implement padding logic (replace with your actual padding method)
|
|
190
|
+
padData(data) {
|
|
191
|
+
// ... (Your padding logic here)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Helper function
|
|
196
|
+
function kelvin_to_celsius(temp_in_kelvin) {
|
|
197
|
+
return Math.round((temp_in_kelvin - 273.15) * 100) / 100;
|
|
198
|
+
}
|
|
199
|
+
*/
|
|
200
|
+
|
|
201
|
+
/*
|
|
202
|
+
|
|
203
|
+
// Assuming 'Struct' and 'GreedyBytes' are replaced with suitable JavaScript libraries/functions
|
|
204
|
+
// Also assuming 'Device' and 'DeviceData' are classes defined elsewhere, or you can remove the inheritance
|
|
205
|
+
|
|
206
|
+
class BatteryMonitor { // extends Device {
|
|
207
|
+
// Assuming 'BatteryMonitorData' is a class defined elsewhere
|
|
208
|
+
static data_type = BatteryMonitorData;
|
|
209
|
+
|
|
210
|
+
// Define the packet structure
|
|
211
|
+
static PACKET = {
|
|
212
|
+
remaining_mins: 'Int16ul',
|
|
213
|
+
voltage: 'Int16ul',
|
|
214
|
+
alarm: 'Int16ul',
|
|
215
|
+
aux: 'Int16ul',
|
|
216
|
+
current: 'Int24sl',
|
|
217
|
+
consumed_ah: 'Int16ul',
|
|
218
|
+
soc: 'Int16ul',
|
|
219
|
+
// 'GreedyBytes' - Handle extra bytes appropriately in your parsing logic
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
parse_decrypted(decrypted) {
|
|
223
|
+
// 'Struct.parse' - Replace with your parsing logic based on the structure
|
|
224
|
+
const pkt = this.parsePacket(decrypted);
|
|
225
|
+
|
|
226
|
+
const aux_mode = pkt.current & 0b11; // Assuming 'AuxMode' is an enum defined elsewhere
|
|
227
|
+
|
|
228
|
+
const parsed = {
|
|
229
|
+
remaining_mins: pkt.remaining_mins,
|
|
230
|
+
aux_mode: aux_mode,
|
|
231
|
+
current: (pkt.current >> 2) / 1000,
|
|
232
|
+
voltage: pkt.voltage / 100,
|
|
233
|
+
consumed_ah: pkt.consumed_ah / 10,
|
|
234
|
+
soc: ((pkt.soc & 0x3FFF) >> 4) / 10,
|
|
235
|
+
alarm:
|
|
236
|
+
pkt.alarm,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
if (aux_mode === AuxMode.STARTER_VOLTAGE) {
|
|
240
|
+
// 'Int16sl.parse' - Replace with appropriate parsing logic
|
|
241
|
+
// Convert 'pkt.aux' to a 2-byte array (little-endian) and parse as a signed 16-bit integer
|
|
242
|
+
const auxBytes = new Uint8Array([pkt.aux & 0xFF, (pkt.aux >> 8) & 0xFF]);
|
|
243
|
+
const auxValue = new DataView(auxBytes.buffer).getInt16(0, true); // true for little-endian
|
|
244
|
+
parsed.starter_voltage = auxValue / 100;
|
|
245
|
+
} else if (aux_mode === AuxMode.MIDPOINT_VOLTAGE) {
|
|
246
|
+
parsed.midpoint_voltage = pkt.aux / 100;
|
|
247
|
+
} else if (aux_mode === AuxMode.TEMPERATURE) {
|
|
248
|
+
parsed.temperature_kelvin = pkt.aux / 100;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return parsed;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Replace this with your actual packet parsing logic based on your 'Struct' implementation
|
|
255
|
+
parsePacket(decrypted) {
|
|
256
|
+
// ... (Your parsing logic here)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
Use code with caution.
|
|
260
|
+
|
|
261
|
+
Key points and assumptions:
|
|
262
|
+
|
|
263
|
+
Struct and GreedyBytes: You'll need to replace Struct.parse and GreedyBytes with equivalent JavaScript functionality from libraries you're using or custom implementations.
|
|
264
|
+
Int Types: JavaScript doesn't have built-in unsigned integer types. You might need to use libraries like DataView or bitwise operations to handle unsigned values correctly, especially for Int16ul, Int24sl.
|
|
265
|
+
Byte Manipulation: The line Int16sl.parse((pkt.aux).to_bytes(2, "little")) requires converting an integer to a byte array. I've provided a basic implementation using Uint8Array and DataView, but you might need adjustments depending on your data format.
|
|
266
|
+
Enums: I assumed AuxMode is an enum defined elsewhere. Make sure you have that defined in your JavaScript code.
|
|
267
|
+
Device and DeviceData: If you're not using inheritance from Device, you can remove that part. Otherwise, ensure you have those classes defined.
|
|
268
|
+
Remember: This conversion is based on assumptions about your existing JavaScript environment and how you're handling byte structures. You might need to adapt it further based on your specific implementation details.
|
|
269
|
+
|
|
270
|
+
Let me know if you have any questions or need help with specific parts of the conversion!
|
|
271
|
+
*/
|
|
3
272
|
class SmartShunt extends BTSensor{
|
|
4
273
|
constructor(device){
|
|
5
274
|
super(device)
|
|
@@ -36,6 +36,9 @@ class SmartShunt_GATT extends BTSensor{
|
|
|
36
36
|
|
|
37
37
|
async connect() {
|
|
38
38
|
//TBD implement async version with error-checking
|
|
39
|
+
const paired = await this.device.isPaired()
|
|
40
|
+
if (!paired)
|
|
41
|
+
throw new Error( this.device.toString() + " must be paired to use GATT.")
|
|
39
42
|
await this.device.connect()
|
|
40
43
|
const gattServer = await this.device.gatt()
|
|
41
44
|
const gattService = await gattServer.getPrimaryService("65970000-4bda-4c1e-af4b-551c4cf74769")
|
package/sensor_classes/TPS.js
CHANGED
|
@@ -5,15 +5,37 @@ class TPS extends BTSensor{
|
|
|
5
5
|
constructor(device){
|
|
6
6
|
super(device)
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
static async identify(device){
|
|
10
|
+
try{
|
|
11
|
+
const uuids = await device.getUUIDs()
|
|
12
|
+
if (await device.getName() == 'tps' || await device.getAlias() == 'tps' ||
|
|
13
|
+
uuids.length > 0 && uuids[0] == '0000fff0-0000-1000-8000-00805f9b34fb'){
|
|
14
|
+
return this
|
|
15
|
+
}
|
|
16
|
+
} catch (e){
|
|
17
|
+
console.log(e)
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
static metadata = new Map()
|
|
9
24
|
.set('temp',{unit:'K', description: 'temperature'})
|
|
10
25
|
connect() {
|
|
11
26
|
//TBD figure out what the heck this device is actually broadcasting
|
|
12
27
|
//For now it appears the current temp is in the key of the data but there are multiple keys
|
|
28
|
+
//(async ()=>{
|
|
29
|
+
//const c= await this.constructor.identify(this.device)
|
|
30
|
+
//const c = await this.device.getUUIDs()//helper.prop('UUIDs')
|
|
31
|
+
//console.log( c )
|
|
32
|
+
//})()
|
|
13
33
|
const cb = (propertiesChanged) => {
|
|
14
34
|
try {
|
|
15
|
-
this.device.getManufacturerData().then((data)=>{
|
|
16
|
-
|
|
35
|
+
this.device.getManufacturerData().then(async (data)=>{
|
|
36
|
+
const keys = Object.keys(data)
|
|
37
|
+
//console.log(await this.device.helper.props())
|
|
38
|
+
this.emit("temp", (parseInt(keys[keys.length-1])/100) + 273.1);
|
|
17
39
|
})
|
|
18
40
|
}
|
|
19
41
|
catch (error) {
|