bt-sensors-plugin-sk 1.3.8-beta2 → 1.3.8-beta4
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 +4 -3
- package/README.md +9 -2
- package/package.json +1 -1
- package/public/images/shelly-ws90.jpeg +0 -0
- package/sensor_classes/JikongBMS.js +136 -43
- package/sensor_classes/SensorPush.js +24 -7
- package/sensor_classes/ShellySBWS90CM.js +231 -0
- package/sensor_classes/Victron/VictronSensor.js +16 -0
- package/sensor_classes/VictronACCharger.js +5 -3
- package/sensor_classes/VictronDCDCConverter.js +6 -6
- package/sensor_classes/VictronInverter.js +5 -5
- package/sensor_classes/VictronInverterRS.js +10 -8
- package/sensor_classes/VictronOrionXS.js +4 -4
- package/sensor_classes/VictronSmartBatteryProtect.js +5 -3
- package/sensor_classes/VictronSolarCharger.js +8 -2
- package/sensor_classes/VictronVEBus.js +1 -1
package/BTSensor.js
CHANGED
|
@@ -613,6 +613,7 @@ class BTSensor extends EventEmitter {
|
|
|
613
613
|
)
|
|
614
614
|
try {
|
|
615
615
|
await this.device.helper.callMethod('Connect')
|
|
616
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
616
617
|
} catch (e) {
|
|
617
618
|
this.debug(e)
|
|
618
619
|
throw new Error(e.message)
|
|
@@ -1184,7 +1185,7 @@ class BTSensor extends EventEmitter {
|
|
|
1184
1185
|
path: `notifications.sensors.${this.macAndName()}`,
|
|
1185
1186
|
value: {
|
|
1186
1187
|
state: "warn",
|
|
1187
|
-
message:
|
|
1188
|
+
message: `Unable to communicate with BT sensor: ${this.macAndName()}` ,
|
|
1188
1189
|
method: ["visual", "sound"]
|
|
1189
1190
|
}
|
|
1190
1191
|
}]
|
|
@@ -1219,7 +1220,7 @@ class BTSensor extends EventEmitter {
|
|
|
1219
1220
|
path: `notifications.sensors.${this.macAndName()}`,
|
|
1220
1221
|
value: {
|
|
1221
1222
|
state: "warn",
|
|
1222
|
-
message: `No contact with sensor for ${this.elapsedTimeSinceLastContact()} seconds`,
|
|
1223
|
+
message: `No contact with BT sensor ${this.macAndName()} for ${this.elapsedTimeSinceLastContact()} seconds`,
|
|
1223
1224
|
method: ["visual", "sound"]
|
|
1224
1225
|
}
|
|
1225
1226
|
}]
|
|
@@ -1238,7 +1239,7 @@ class BTSensor extends EventEmitter {
|
|
|
1238
1239
|
path: `notifications.sensors.${this.macAndName()}`,
|
|
1239
1240
|
value: {
|
|
1240
1241
|
state: "normal",
|
|
1241
|
-
message: `
|
|
1242
|
+
message: `Normal communication with BT sensor ${this.macAndName()}`,
|
|
1242
1243
|
method: []
|
|
1243
1244
|
}
|
|
1244
1245
|
}]
|
package/README.md
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# Bluetooth Sensors for [Signal K](http://www.signalk.org)
|
|
2
2
|
|
|
3
3
|
## WHAT'S NEW
|
|
4
|
-
# Version 1.3.8-beta2
|
|
4
|
+
# Version 1.3.8-beta2/3/4
|
|
5
|
+
- SensorPush fixes
|
|
6
|
+
- Error/state code and description for Victron devices per issue #124
|
|
7
|
+
- More descriptive notifications per issue #126
|
|
8
|
+
- V19 Jikong BMS support
|
|
9
|
+
|
|
10
|
+
## New sensors
|
|
11
|
+
|
|
12
|
+
- Shelly/Ecowitt WS90 weather station
|
|
5
13
|
|
|
6
|
-
- SensorPush fixes
|
|
7
14
|
|
|
8
15
|
# Version 1.3.8-beta1
|
|
9
16
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bt-sensors-plugin-sk",
|
|
3
|
-
"version": "1.3.8-
|
|
3
|
+
"version": "1.3.8-beta4",
|
|
4
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": {
|
|
Binary file
|
|
@@ -31,8 +31,8 @@ class JikongBMS extends BTSensor {
|
|
|
31
31
|
static validAcknowledgeHeader = 0xaa5590eb;
|
|
32
32
|
|
|
33
33
|
static commandResponse = {
|
|
34
|
-
0x96: 0x02,
|
|
35
|
-
0x97: 0x03,
|
|
34
|
+
0x96: [0x02],
|
|
35
|
+
0x97: [0x01, 0x03],
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
static async test(datafile) {
|
|
@@ -350,57 +350,150 @@ class JikongBMS extends BTSensor {
|
|
|
350
350
|
|
|
351
351
|
getBuffer(command) {
|
|
352
352
|
return new Promise(async (resolve, reject) => {
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
353
|
+
const datasize = 300;
|
|
354
|
+
const expectedResponses = []
|
|
355
|
+
.concat(this.constructor.commandResponse[command] ?? [])
|
|
356
|
+
.filter((frameType) => frameType !== undefined);
|
|
357
|
+
let stream = Buffer.alloc(0);
|
|
358
|
+
let timer;
|
|
359
|
+
let settled = false;
|
|
360
|
+
|
|
361
|
+
const removeValueChangedListener = () => {
|
|
362
|
+
if (typeof this.rxChar?.off === "function") {
|
|
363
|
+
this.rxChar.off("valuechanged", valChanged);
|
|
364
|
+
} else if (typeof this.rxChar?.removeListener === "function") {
|
|
365
|
+
this.rxChar.removeListener("valuechanged", valChanged);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
357
368
|
|
|
358
|
-
const
|
|
359
|
-
|
|
369
|
+
const cleanup = () => {
|
|
370
|
+
removeValueChangedListener();
|
|
360
371
|
clearTimeout(timer);
|
|
361
|
-
|
|
362
|
-
new Error(
|
|
363
|
-
`Response timed out (+30s) getting results for command ${command} from JikongBMS device ${this.getName()}.`
|
|
364
|
-
)
|
|
365
|
-
);
|
|
366
|
-
}, 30000);
|
|
372
|
+
};
|
|
367
373
|
|
|
368
|
-
const
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
)
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
374
|
+
const resolveBuffer = (buffer) => {
|
|
375
|
+
if (settled) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
settled = true;
|
|
379
|
+
cleanup();
|
|
380
|
+
resolve(buffer);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const rejectBuffer = (error) => {
|
|
384
|
+
if (settled) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
settled = true;
|
|
388
|
+
cleanup();
|
|
389
|
+
reject(error);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const findHeader = (buffer, start = 0) => {
|
|
393
|
+
for (let i = start; i <= buffer.length - 4; i++) {
|
|
394
|
+
const header = buffer.readUInt32BE(i);
|
|
395
|
+
if (header === this.constructor.validResponseHeader) {
|
|
396
|
+
return { index: i, header };
|
|
397
|
+
}
|
|
398
|
+
if (header === this.constructor.validAcknowledgeHeader) {
|
|
399
|
+
return { index: i, header };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const parseStream = () => {
|
|
406
|
+
while (stream.length >= 4) {
|
|
407
|
+
const headerMatch = findHeader(stream);
|
|
408
|
+
|
|
409
|
+
if (!headerMatch) {
|
|
410
|
+
// Preserve a possible partial 4-byte header between notifications.
|
|
411
|
+
stream = stream.slice(-3);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (headerMatch.index > 0) {
|
|
377
416
|
this.debug(
|
|
378
|
-
`
|
|
379
|
-
buffer
|
|
380
|
-
)}from ${this.getName()}, not processing.`
|
|
417
|
+
`Discarding ${headerMatch.index} leading byte(s) before JK header for ${this.getName()}`
|
|
381
418
|
);
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
);
|
|
393
|
-
resolve(result);
|
|
394
|
-
} else {
|
|
395
|
-
offset = 0;
|
|
396
|
-
result = Buffer.alloc(datasize);
|
|
419
|
+
stream = stream.slice(headerMatch.index);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (
|
|
423
|
+
stream.length >= 4 &&
|
|
424
|
+
stream.readUInt32BE(0) === this.constructor.validAcknowledgeHeader
|
|
425
|
+
) {
|
|
426
|
+
// Old/new JK modules often append a short ack after the data frame.
|
|
427
|
+
if (stream.length <= 4) {
|
|
428
|
+
return;
|
|
397
429
|
}
|
|
398
|
-
|
|
399
|
-
|
|
430
|
+
stream = stream.slice(4);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (stream.length < datasize) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const candidate = stream.slice(0, datasize);
|
|
439
|
+
const computedCRC =
|
|
440
|
+
sumByteArray(candidate.subarray(0, datasize - 1)) & 0xff;
|
|
441
|
+
const remoteCRC = candidate[datasize - 1];
|
|
442
|
+
|
|
443
|
+
if (computedCRC !== remoteCRC) {
|
|
444
|
+
const nextHeader = findHeader(stream, 1);
|
|
445
|
+
const bytesToDrop = nextHeader ? nextHeader.index : 1;
|
|
446
|
+
this.debug(
|
|
447
|
+
`CRC mismatch for ${this.getName()} (${computedCRC} != ${remoteCRC}), dropping ${bytesToDrop} byte(s)`
|
|
448
|
+
);
|
|
449
|
+
stream = stream.slice(bytesToDrop);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (
|
|
454
|
+
expectedResponses.length === 0 ||
|
|
455
|
+
expectedResponses.includes(candidate[4])
|
|
456
|
+
) {
|
|
457
|
+
this.debug(
|
|
458
|
+
`Rec'd command in buffer ${JSON.stringify(
|
|
459
|
+
candidate
|
|
460
|
+
)} for ${this.getName()}`
|
|
461
|
+
);
|
|
462
|
+
resolveBuffer(candidate);
|
|
463
|
+
return;
|
|
400
464
|
}
|
|
465
|
+
|
|
466
|
+
this.debug(
|
|
467
|
+
`Ignoring JK frame type ${candidate[4]} while waiting for ${expectedResponses.join(
|
|
468
|
+
", "
|
|
469
|
+
)} from ${this.getName()}`
|
|
470
|
+
);
|
|
471
|
+
stream = stream.slice(datasize);
|
|
401
472
|
}
|
|
402
473
|
};
|
|
474
|
+
|
|
475
|
+
const valChanged = async (buffer) => {
|
|
476
|
+
if (settled) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (!Buffer.isBuffer(buffer)) {
|
|
480
|
+
buffer = Buffer.from(buffer);
|
|
481
|
+
}
|
|
482
|
+
stream = Buffer.concat([stream, buffer]);
|
|
483
|
+
parseStream();
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
timer = setTimeout(() => {
|
|
487
|
+
rejectBuffer(
|
|
488
|
+
new Error(
|
|
489
|
+
`Response timed out (+30s) getting results for command ${command} from JikongBMS device ${this.getName()}.`
|
|
490
|
+
)
|
|
491
|
+
);
|
|
492
|
+
}, 30000);
|
|
493
|
+
|
|
494
|
+
// Listen before writing so the first JK notification chunk is not lost.
|
|
403
495
|
this.rxChar.on("valuechanged", valChanged);
|
|
496
|
+
await this.sendReadFunctionRequest(command);
|
|
404
497
|
});
|
|
405
498
|
}
|
|
406
499
|
|
|
@@ -16,6 +16,7 @@ class SensorPush extends BTSensor{
|
|
|
16
16
|
static ServiceUUID = "EF090000-11D6-42BA-93B8-9DD7EC090AB0"
|
|
17
17
|
static Characteristics =
|
|
18
18
|
{
|
|
19
|
+
|
|
19
20
|
tx: "EF090003-11D6-42BA-93B8-9DD7EC090AA9",
|
|
20
21
|
adv: "EF090005-11D6-42BA-93B8-9DD7EC090AA9",
|
|
21
22
|
batt: "EF090007-11D6-42BA-93B8-9DD7EC090AA9",
|
|
@@ -32,16 +33,25 @@ class SensorPush extends BTSensor{
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
async emitGATT(){
|
|
36
|
+
await this.characteristics.temp.writeValue(Buffer.from([1,0,0,0]))
|
|
37
|
+
|
|
35
38
|
this.emitData("temp", await this.characteristics.temp.readValue())
|
|
39
|
+
|
|
40
|
+
await this.characteristics.hum.writeValue(Buffer.from([1,0,0,0]))
|
|
36
41
|
this.emitData("humidity", await this.characteristics.hum.readValue())
|
|
42
|
+
|
|
37
43
|
this.emitData("batt", await this.characteristics.batt.readValue())
|
|
38
|
-
|
|
44
|
+
|
|
45
|
+
if (this.characteristics.bar){
|
|
46
|
+
await this.characteristics.bar.writeValue(Buffer.from([1,0,0,0]))
|
|
47
|
+
this.emitData("pressure", await this.characteristics.bar.readValue())
|
|
48
|
+
}
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
initSchema(){
|
|
42
52
|
super.initSchema()
|
|
43
53
|
this.getGATTParams()["useGATT"].default=true
|
|
44
|
-
this.getGATTParams()["pollFreq"].default=
|
|
54
|
+
this.getGATTParams()["pollFreq"].default=60
|
|
45
55
|
this._schema.properties.gattParams.required.push("pollFreq")
|
|
46
56
|
|
|
47
57
|
this.addDefaultParam("zone")
|
|
@@ -84,7 +94,7 @@ class SensorPush extends BTSensor{
|
|
|
84
94
|
.read=(buffer)=>{ return buffer.readUInt16LE()/1000}
|
|
85
95
|
|
|
86
96
|
this.addDefaultPath("temp","environment.temperature")
|
|
87
|
-
.read=(buffer)=>{ return buffer.readInt32LE()/100}
|
|
97
|
+
.read=(buffer)=>{ return 273.15+(buffer.readInt32LE()/100)}
|
|
88
98
|
|
|
89
99
|
this.addDefaultPath("humidity","environment.humidity")
|
|
90
100
|
.read=(buffer)=>{ return buffer.readInt32LE()/10000}
|
|
@@ -118,13 +128,20 @@ class SensorPush extends BTSensor{
|
|
|
118
128
|
const service = await gattServer.getPrimaryService(this.constructor.ServiceUUID.toLowerCase())
|
|
119
129
|
this.characteristics={}
|
|
120
130
|
for (const c in this.constructor.Characteristics) {
|
|
121
|
-
|
|
131
|
+
const uuid = this.constructor.Characteristics[c].toLowerCase()
|
|
132
|
+
try{
|
|
133
|
+
this.characteristics[c] = await service.getCharacteristic(uuid)
|
|
134
|
+
} catch (e) {
|
|
135
|
+
this.debug(`characteristic ${c} with uuid ${uuid} not available.`)
|
|
136
|
+
}
|
|
122
137
|
}
|
|
123
|
-
if (this.tx)
|
|
138
|
+
if (this.tx && this.characteristics.tx)
|
|
124
139
|
await writeInt8(this.characteristics.tx,this.tx)
|
|
125
|
-
|
|
140
|
+
|
|
141
|
+
if (this.LED && this.characteristics.LED)
|
|
126
142
|
await writeUInt8(this.characteristics.LED,this.LED)
|
|
127
|
-
|
|
143
|
+
|
|
144
|
+
if (this.adv && this.characteristics.adv)
|
|
128
145
|
await writeUInt16LE(this.characteristics.adv,Math.round((this.adv/625)*1000))
|
|
129
146
|
}
|
|
130
147
|
async initGATTNotifications() {
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
const AbstractBTHomeSensor = require("./BTHome/AbstractBTHomeSensor");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sensor class representing the Shelly / Ecowitt WS90 weather station.
|
|
5
|
+
* The device publishes BTHome v2 service data on UUID 0xFCD2.
|
|
6
|
+
*/
|
|
7
|
+
class ShellySBWS90CM extends AbstractBTHomeSensor {
|
|
8
|
+
static Domain = this.SensorDomains.environmental;
|
|
9
|
+
static SHORTENED_LOCAL_NAME = "SBWS-90CM";
|
|
10
|
+
static LOCAL_NAME = "SBWS-90CM";
|
|
11
|
+
static ImageFile = "shelly-ws90.jpeg";
|
|
12
|
+
|
|
13
|
+
getTextDescription() {
|
|
14
|
+
return `Shelly/Ecowitt WS90 weather station (BTHome v2 over BLE service UUID 0xFCD2).`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
getId() {
|
|
18
|
+
// Stable, persistent ID so SignalK can store configuration
|
|
19
|
+
return `ShellySBWS90CM:${this.getAddress()}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
initSchema() {
|
|
23
|
+
super.initSchema();
|
|
24
|
+
this.addDefaultParam("zone", true).default = "outside";
|
|
25
|
+
|
|
26
|
+
this.addMetadatum("temperatureK", "K", "outside temperature", this.parseTemperatureK.bind(this)).default =
|
|
27
|
+
"environment.{zone}.temperature";
|
|
28
|
+
|
|
29
|
+
this.addMetadatum("humidityRatio", "ratio", "relative humidity", this.parseHumidityRatio.bind(this)).default =
|
|
30
|
+
"environment.{zone}.humidity";
|
|
31
|
+
|
|
32
|
+
this.addMetadatum("pressurePa", "Pa", "ambient pressure", this.parsePressurePa.bind(this)).default =
|
|
33
|
+
"environment.outside.pressure";
|
|
34
|
+
|
|
35
|
+
this.addMetadatum("illuminanceLux", "lux", "illuminance", this.parseIlluminanceLux.bind(this)).default =
|
|
36
|
+
"environment.{zone}.illuminance";
|
|
37
|
+
|
|
38
|
+
this.addMetadatum("uvIndex", "", "UV index", this.parseUVIndex.bind(this)).default = "environment.{zone}.uvIndex";
|
|
39
|
+
|
|
40
|
+
this.addMetadatum("isRaining", "boolean", "rain status", this.parseIsRaining.bind(this)).default =
|
|
41
|
+
"environment.{zone}.raining";
|
|
42
|
+
|
|
43
|
+
this.addMetadatum("precipitationMm", "mm", "precipitation", this.parsePrecipitationMm.bind(this)).default =
|
|
44
|
+
"environment.{zone}.precipitation";
|
|
45
|
+
|
|
46
|
+
this.addMetadatum("windSpeed", "m/s", "true wind speed", this.parseWindSpeed.bind(this)).default = "environment.wind.speedTrue";
|
|
47
|
+
|
|
48
|
+
this.addMetadatum("windGust", "m/s", "true wind gust", this.parseWindGust.bind(this)).default = "environment.wind.gust";
|
|
49
|
+
|
|
50
|
+
this.addMetadatum("windDirectionRad", "rad", "true wind direction", this.parseWindDirectionRad.bind(this)).default =
|
|
51
|
+
"environment.wind.directionTrue";
|
|
52
|
+
|
|
53
|
+
this.addMetadatum("batteryRatio", "ratio", "battery ratio", this.parseBatteryRatio.bind(this)).default =
|
|
54
|
+
"sensors.{macAndName}.battery";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
parseBatteryRatio(decoded) {
|
|
58
|
+
if (typeof decoded.batteryRatio === "number") return decoded.batteryRatio;
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
parseTemperatureK(decoded) {
|
|
63
|
+
if (typeof decoded.temperatureK === "number") return decoded.temperatureK;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
parseHumidityRatio(decoded) {
|
|
68
|
+
if (typeof decoded.humidityRatio === "number") return decoded.humidityRatio;
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
parsePressurePa(decoded) {
|
|
73
|
+
if (typeof decoded.pressurePa === "number") return decoded.pressurePa;
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
parseIlluminanceLux(decoded) {
|
|
78
|
+
if (typeof decoded.illuminanceLux === "number") return decoded.illuminanceLux;
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
parseUVIndex(decoded) {
|
|
83
|
+
if (typeof decoded.uvIndex === "number") return decoded.uvIndex;
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
parseIsRaining(decoded) {
|
|
88
|
+
if (typeof decoded.isRaining === "boolean") return decoded.isRaining;
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
parsePrecipitationMm(decoded) {
|
|
93
|
+
if (typeof decoded.precipitationMm === "number") return decoded.precipitationMm;
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
parseWindDirectionRad(decoded) {
|
|
98
|
+
if (typeof decoded.windDirectionRad === "number") return decoded.windDirectionRad;
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
parseWindSpeed(decoded) {
|
|
103
|
+
if (typeof decoded.windSpeed === "number") return decoded.windSpeed;
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
parseWindGust(decoded) {
|
|
108
|
+
if (typeof decoded.windGust === "number") return decoded.windGust;
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
readU16LE(buffer, index) {
|
|
113
|
+
return buffer.readUInt16LE(index);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
readI16LE(buffer, index) {
|
|
117
|
+
return buffer.readInt16LE(index);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
readU24LE(buffer, index) {
|
|
121
|
+
return buffer[index] | (buffer[index + 1] << 8) | (buffer[index + 2] << 16);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
decodeWS90BTHomeData(buffer) {
|
|
125
|
+
if (!buffer || buffer.length < 1) return null;
|
|
126
|
+
|
|
127
|
+
const decoded = {};
|
|
128
|
+
const deviceInfo = buffer.readUInt8(0);
|
|
129
|
+
// BTHome v2 encryption flag: bit0 = 1 means encrypted payload
|
|
130
|
+
if ((deviceInfo & 0x01) !== 0) {
|
|
131
|
+
this.debug(`${this.getDisplayName()} BTHome payload is encrypted; skipping decode`);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
let index = 1;
|
|
135
|
+
|
|
136
|
+
let windSpeedCount = 0;
|
|
137
|
+
|
|
138
|
+
while (index < buffer.length) {
|
|
139
|
+
const objectId = buffer[index++];
|
|
140
|
+
switch (objectId) {
|
|
141
|
+
case 0x00: // packet id uint8 (ignore)
|
|
142
|
+
if (index + 1 > buffer.length) return decoded;
|
|
143
|
+
index += 1;
|
|
144
|
+
break;
|
|
145
|
+
case 0x01: // battery % uint8 -> ratio
|
|
146
|
+
if (index + 1 > buffer.length) return decoded;
|
|
147
|
+
decoded.batteryRatio = Number.parseFloat((buffer.readUInt8(index) / 100).toFixed(2));
|
|
148
|
+
index += 1;
|
|
149
|
+
break;
|
|
150
|
+
case 0x45: // temperature C*0.1 int16 -> Kelvin
|
|
151
|
+
if (index + 2 > buffer.length) return decoded;
|
|
152
|
+
decoded.temperatureK = Number.parseFloat((273.15 + this.readI16LE(buffer, index) * 0.1).toFixed(2));
|
|
153
|
+
index += 2;
|
|
154
|
+
break;
|
|
155
|
+
case 0x2e: // humidity % uint8 -> ratio
|
|
156
|
+
if (index + 1 > buffer.length) return decoded;
|
|
157
|
+
decoded.humidityRatio = Number.parseFloat((buffer.readUInt8(index) / 100).toFixed(2));
|
|
158
|
+
index += 1;
|
|
159
|
+
break;
|
|
160
|
+
case 0x04: // pressure hPa*0.01 uint24 -> Pa
|
|
161
|
+
if (index + 3 > buffer.length) return decoded;
|
|
162
|
+
decoded.pressurePa = Number.parseFloat((this.readU24LE(buffer, index) * 0.01 * 100).toFixed(2));
|
|
163
|
+
index += 3;
|
|
164
|
+
break;
|
|
165
|
+
case 0x05: // illuminance lux*0.01 uint24 -> lux
|
|
166
|
+
if (index + 3 > buffer.length) return decoded;
|
|
167
|
+
decoded.illuminanceLux = Number.parseFloat((this.readU24LE(buffer, index) * 0.01).toFixed(2));
|
|
168
|
+
index += 3;
|
|
169
|
+
break;
|
|
170
|
+
case 0x46: // UV index *0.1 uint8
|
|
171
|
+
if (index + 1 > buffer.length) return decoded;
|
|
172
|
+
decoded.uvIndex = Number.parseFloat((buffer.readUInt8(index) * 0.1).toFixed(1));
|
|
173
|
+
index += 1;
|
|
174
|
+
break;
|
|
175
|
+
case 0x20: // rain status bool uint8
|
|
176
|
+
if (index + 1 > buffer.length) return decoded;
|
|
177
|
+
decoded.isRaining = buffer.readUInt8(index) !== 0;
|
|
178
|
+
index += 1;
|
|
179
|
+
break;
|
|
180
|
+
case 0x5f: // precipitation mm*0.1 uint16
|
|
181
|
+
if (index + 2 > buffer.length) return decoded;
|
|
182
|
+
decoded.precipitationMm = Number.parseFloat((this.readU16LE(buffer, index) * 0.1).toFixed(1));
|
|
183
|
+
index += 2;
|
|
184
|
+
break;
|
|
185
|
+
case 0x5e: // wind direction deg*0.01 uint16 -> radians
|
|
186
|
+
if (index + 2 > buffer.length) return decoded;
|
|
187
|
+
decoded.windDirectionRad = Number.parseFloat(((this.readU16LE(buffer, index) * 0.01 * Math.PI) / 180).toFixed(4));
|
|
188
|
+
index += 2;
|
|
189
|
+
break;
|
|
190
|
+
case 0x44: { // wind/gust speed m/s*0.01 uint16; appears twice
|
|
191
|
+
if (index + 2 > buffer.length) return decoded;
|
|
192
|
+
const value = Number.parseFloat((this.readU16LE(buffer, index) * 0.01).toFixed(2));
|
|
193
|
+
if (windSpeedCount === 0) decoded.windSpeed = value;
|
|
194
|
+
else if (windSpeedCount === 1) decoded.windGust = value;
|
|
195
|
+
windSpeedCount += 1;
|
|
196
|
+
index += 2;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case 0x08: // optional dew point C*0.01 int16 (ignored)
|
|
200
|
+
if (index + 2 > buffer.length) return decoded;
|
|
201
|
+
index += 2;
|
|
202
|
+
break;
|
|
203
|
+
case 0x0c: // optional capacitor voltage V*0.001 uint16 (ignored)
|
|
204
|
+
if (index + 2 > buffer.length) return decoded;
|
|
205
|
+
index += 2;
|
|
206
|
+
break;
|
|
207
|
+
default:
|
|
208
|
+
this.debug(`${this.getDisplayName()} unsupported BTHome object 0x${objectId.toString(16)}`);
|
|
209
|
+
return decoded;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return decoded;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
propertiesChanged(props) {
|
|
217
|
+
const raw = this.getServiceData(this.constructor.BTHOME_SERVICE_ID);
|
|
218
|
+
if (!raw) return;
|
|
219
|
+
const buffer = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
220
|
+
if (!this._ws90LoggedOnce) {
|
|
221
|
+
this._ws90LoggedOnce = true;
|
|
222
|
+
this.debug(`WS90 raw serviceData len=${buffer.length} hex=${buffer.toString("hex")}`);
|
|
223
|
+
}
|
|
224
|
+
const decoded = this.decodeWS90BTHomeData(buffer);
|
|
225
|
+
if (!decoded) return;
|
|
226
|
+
|
|
227
|
+
this.emitValuesFrom(decoded);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = ShellySBWS90CM;
|
|
@@ -83,6 +83,22 @@ const VictronIdentifier = require('./VictronIdentifier.js');
|
|
|
83
83
|
alarm: `0x${alarm.toString(16).padStart(8,"0")}`,
|
|
84
84
|
alarmstate: 'alert'})
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
_getOperationMode(buff, offset=0){
|
|
88
|
+
const code = buff.readUInt8(offset)
|
|
89
|
+
return {
|
|
90
|
+
code: code,
|
|
91
|
+
message: VC.OperationMode.get(code)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_getChargerError(buff, offset=1){
|
|
96
|
+
const code = buff.readUInt8(offset)
|
|
97
|
+
return {
|
|
98
|
+
code: code,
|
|
99
|
+
message: VC.ChargerError.get(code)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
86
102
|
async init(){
|
|
87
103
|
await super.init()
|
|
88
104
|
this.addParameter(
|
|
@@ -29,10 +29,12 @@ class VictronACCharger extends VictronSensor{
|
|
|
29
29
|
this.addDefaultParam("id")
|
|
30
30
|
|
|
31
31
|
this.addMetadatum('state','', 'device state',
|
|
32
|
-
(buff)=>{return
|
|
33
|
-
|
|
32
|
+
(buff)=>{return this._getOperationMode(buff)})
|
|
33
|
+
.default= "electrical.chargers.{id}.state"
|
|
34
|
+
|
|
34
35
|
this.addMetadatum('chargerError','', 'charger error code',
|
|
35
|
-
(buff)=>{return
|
|
36
|
+
(buff)=>{return this._getChargerError(buff)}
|
|
37
|
+
)
|
|
36
38
|
.default= "electrical.chargers.{id}.error"
|
|
37
39
|
|
|
38
40
|
this.addMetadatum('batt1','V', 'battery 1 voltage')
|
|
@@ -8,13 +8,13 @@ class VictronDCDCConverter extends VictronSensor{
|
|
|
8
8
|
initSchema(){
|
|
9
9
|
super.initSchema()
|
|
10
10
|
this.addDefaultParam("id")
|
|
11
|
+
this.addMetadatum('state','', 'device state',
|
|
12
|
+
(buff)=>{return this._getOperationMode(buff)})
|
|
13
|
+
.default= "electrical.chargers.{id}.state"
|
|
11
14
|
|
|
12
|
-
this.addMetadatum('
|
|
13
|
-
(buff)=>{return
|
|
14
|
-
.default="electrical.chargers.{id}.
|
|
15
|
-
this.addMetadatum('chargerError','', 'charger error',
|
|
16
|
-
(buff)=>{return VC.ChargerError.get(buff.readUInt8(1))})
|
|
17
|
-
.default="electrical.chargers.{id}.error"
|
|
15
|
+
this.addMetadatum('chargerError','', 'charger error code',
|
|
16
|
+
(buff)=>{return this._getChargerError(buff)})
|
|
17
|
+
.default= "electrical.chargers.{id}.error"
|
|
18
18
|
|
|
19
19
|
this.addMetadatum('inputVoltage','V', 'input voltage',
|
|
20
20
|
(buff)=>{return this.NaNif(buff.readUInt16LE(2),0xFFFF)/100})
|
|
@@ -9,13 +9,13 @@ class VictronInverter extends VictronSensor{
|
|
|
9
9
|
initSchema(){
|
|
10
10
|
super.initSchema()
|
|
11
11
|
this.addDefaultParam("id")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
this.addMetadatum('state','', 'inverter device state',
|
|
13
|
+
(buff)=>{return this._getOperationMode(buff)}
|
|
14
|
+
)
|
|
15
|
+
.default="electrical.inverters.{id}.state"
|
|
16
16
|
|
|
17
17
|
const md = this.addMetadatum('alarmReason','', 'reason for alarm',
|
|
18
|
-
(buff)=>{return buff.
|
|
18
|
+
(buff)=>{return buff.readUInt16LE(1)})
|
|
19
19
|
.default="electrical.inverters.{id}.alarm"
|
|
20
20
|
|
|
21
21
|
this.addDefaultPath('dcVoltage','electrical.inverters.dc.voltage')
|
|
@@ -10,14 +10,16 @@ class VictronInverterRS extends VictronSensor{
|
|
|
10
10
|
|
|
11
11
|
initSchema() {
|
|
12
12
|
super.initSchema()
|
|
13
|
-
this.addDefaultParam("id")
|
|
14
|
-
this.addMetadatum('
|
|
15
|
-
(buff)=>{return
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
this.addDefaultParam("id")
|
|
14
|
+
this.addMetadatum('state','', 'device state',
|
|
15
|
+
(buff)=>{return this._getOperationMode(buff)}
|
|
16
|
+
)
|
|
17
|
+
.default='electrical.inverters.{id}.state'
|
|
18
|
+
|
|
19
|
+
this.addMetadatum('chargerError','', 'charger error code',
|
|
20
|
+
(buff)=>{return this._getChargerError(buff)}
|
|
21
|
+
)
|
|
22
|
+
.default='electrical.inverters.{id}.error'
|
|
21
23
|
|
|
22
24
|
this.addMetadatum('batteryVoltage','V', 'battery voltage',
|
|
23
25
|
(buff)=>{return this.NaNif(buff.readInt16LE(2),0x7FFF)/100})
|
|
@@ -10,11 +10,11 @@ class VictronOrionXS extends VictronSensor{
|
|
|
10
10
|
super.initSchema()
|
|
11
11
|
this.addDefaultParam("id")
|
|
12
12
|
this.addMetadatum('deviceState','', 'device state',
|
|
13
|
-
(buff)=>{return
|
|
13
|
+
(buff)=>{return this._getOperationMode(buff)})
|
|
14
14
|
.default="electrical.chargers.{id}.state"
|
|
15
|
-
|
|
16
|
-
this.addMetadatum('chargerError','', 'charger error',
|
|
17
|
-
(buff)=>{return
|
|
15
|
+
|
|
16
|
+
this.addMetadatum('chargerError','', 'charger error code',
|
|
17
|
+
(buff)=>{return this._getChargerError(buff)})
|
|
18
18
|
.default="electrical.chargers.{id}.error"
|
|
19
19
|
|
|
20
20
|
this.addMetadatum('outputVoltage','V', 'output voltage',
|
|
@@ -24,13 +24,15 @@ class VictronSmartBatteryProtect extends VictronSensor{
|
|
|
24
24
|
initSchema(){
|
|
25
25
|
super.initSchema()
|
|
26
26
|
this.addDefaultParam("id")
|
|
27
|
+
|
|
27
28
|
this.addMetadatum('deviceState','', 'device state',
|
|
28
|
-
(buff)=>{return
|
|
29
|
+
(buff)=>{return this._getOperationMode(buff)})
|
|
30
|
+
|
|
29
31
|
this.addMetadatum('outputStatus','', 'output status', //TODO
|
|
30
32
|
(buff)=>{return (buff.readUInt8(2))})
|
|
31
33
|
|
|
32
|
-
this.addMetadatum('chargerError','', 'charger error',
|
|
33
|
-
(buff)=>{return
|
|
34
|
+
this.addMetadatum('chargerError','', 'charger error',
|
|
35
|
+
(buff)=>{return this._getChargerError(buff,3)})
|
|
34
36
|
this.addMetadatum('alarmReason','', 'alarm reason',
|
|
35
37
|
(buff)=>{return buff.readUInt16LE(4)})
|
|
36
38
|
this.addMetadatum('warningReason','', 'warning reason', //TODO
|
|
@@ -9,11 +9,17 @@ class VictronSolarCharger extends VictronSensor{
|
|
|
9
9
|
this.addDefaultParam("id")
|
|
10
10
|
|
|
11
11
|
this.addMetadatum('chargeState','', 'charge state',
|
|
12
|
-
|
|
12
|
+
(buff)=>{
|
|
13
|
+
const code = buff.readUInt8(0)
|
|
14
|
+
return {
|
|
15
|
+
code: code,
|
|
16
|
+
message: VC.OperationMode.get(code)
|
|
17
|
+
}
|
|
18
|
+
})
|
|
13
19
|
.default="electrical.solar.{id}.state"
|
|
14
20
|
|
|
15
21
|
this.addMetadatum('chargerError','', 'charger error',
|
|
16
|
-
(buff)=>{return
|
|
22
|
+
(buff)=>{return this._getChargerError(buff)})
|
|
17
23
|
.default="electrical.solar.{id}.error"
|
|
18
24
|
|
|
19
25
|
this.addMetadatum('voltage','V', 'charger battery voltage',
|
|
@@ -9,7 +9,7 @@ class VictronVEBus extends VictronSensor{
|
|
|
9
9
|
initSchema(){
|
|
10
10
|
super.initSchema()
|
|
11
11
|
this.addMetadatum('chargeState','', 'charge state',
|
|
12
|
-
(buff)=>{return
|
|
12
|
+
(buff)=>{ return this._getOperationMode(buff)})
|
|
13
13
|
|
|
14
14
|
this.addMetadatum('veBusError','', 'VE bus error',
|
|
15
15
|
(buff)=>{return buff.readUInt8(1)}) //TODO
|