bt-sensors-plugin-sk 1.0.2 → 1.1.0-beta.1
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 +337 -40
- package/README.md +3 -3
- package/bt_co.json +14520 -0
- package/index.js +385 -177
- package/package.json +4 -3
- package/sensor_classes/ATC.js +31 -22
- package/sensor_classes/BlackListedDevice.js +20 -0
- package/sensor_classes/Inkbird.js +51 -0
- package/sensor_classes/RuuviTag.js +121 -0
- package/sensor_classes/UNKNOWN.js +12 -0
- package/sensor_classes/Victron/VictronConstants.js +328 -0
- package/sensor_classes/Victron/VictronSensor.js +106 -0
- package/sensor_classes/VictronACCharger.js +64 -0
- package/sensor_classes/VictronBatteryMonitor.js +160 -0
- package/sensor_classes/VictronDCDCConverter.js +26 -0
- package/sensor_classes/VictronDCEnergyMeter.js +68 -0
- package/sensor_classes/VictronGXDevice.js +43 -0
- package/sensor_classes/VictronInverter.js +44 -0
- package/sensor_classes/VictronInverterRS.js +44 -0
- package/sensor_classes/VictronLynxSmartBMS.js +50 -0
- package/sensor_classes/VictronOrionXS.js +31 -0
- package/sensor_classes/VictronSmartBatteryProtect.js +49 -0
- package/sensor_classes/VictronSmartLithium.js +66 -0
- package/sensor_classes/VictronSolarCharger.js +29 -0
- package/sensor_classes/VictronVEBus.js +47 -0
- package/sensor_classes/XiaomiMiBeacon.js +232 -0
- package/test.js +32 -0
- package/sensor_classes/LYWSD03MMC.js +0 -39
- package/sensor_classes/SmartShunt.js +0 -300
- package/sensor_classes/SmartShunt_GATT.js +0 -64
- package/sensor_classes/TPS.js +0 -49
package/BTSensor.js
CHANGED
|
@@ -1,56 +1,356 @@
|
|
|
1
|
+
const { Variant } = require('dbus-next');
|
|
2
|
+
const { log } = require('node:console');
|
|
1
3
|
const EventEmitter = require('node:events');
|
|
2
|
-
|
|
4
|
+
const BTCompanies = require('./bt_co.json')
|
|
3
5
|
/**
|
|
4
6
|
* @classdesc Abstract class that all sensor classes should inherit from. Sensor subclasses monitor a
|
|
5
7
|
* BT peripheral and emit changes in the sensors's value like "temp" or "humidity"
|
|
6
8
|
* @class BTSensor
|
|
7
9
|
* @see EventEmitter, node-ble/Device
|
|
8
10
|
*/
|
|
9
|
-
class BTSensor {
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
const BTCompanyMap=new Map()
|
|
13
|
+
BTCompanies.company_identifiers.forEach( (v) =>{
|
|
14
|
+
BTCompanyMap.set(v.value, v.name)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
function signalQualityPercentQuad(rssi, perfect_rssi=-20, worst_rssi=-85) {
|
|
18
|
+
const nominal_rssi=(perfect_rssi - worst_rssi);
|
|
19
|
+
var signal_quality =
|
|
20
|
+
(100 *
|
|
21
|
+
(perfect_rssi - worst_rssi) *
|
|
22
|
+
(perfect_rssi - worst_rssi) -
|
|
23
|
+
(perfect_rssi - rssi) *
|
|
24
|
+
(15 * (perfect_rssi - worst_rssi) + 62 * (perfect_rssi - rssi))) /
|
|
25
|
+
((perfect_rssi - worst_rssi) * (perfect_rssi - worst_rssi));
|
|
26
|
+
|
|
27
|
+
if (signal_quality > 100) {
|
|
28
|
+
signal_quality = 100;
|
|
29
|
+
} else if (signal_quality < 1) {
|
|
30
|
+
signal_quality = 0;
|
|
31
|
+
}
|
|
32
|
+
return Math.ceil(signal_quality);
|
|
33
|
+
}
|
|
34
|
+
class BTSensor extends EventEmitter {
|
|
35
|
+
static metadata=new Map()
|
|
36
|
+
constructor(device, config={}, gattConfig={}) {
|
|
37
|
+
super()
|
|
38
|
+
|
|
12
39
|
this.device=device
|
|
13
|
-
this.
|
|
40
|
+
this.name = config?.name
|
|
41
|
+
|
|
42
|
+
this.useGATT = gattConfig?.useGATT
|
|
43
|
+
this.pollFreq = gattConfig?.pollFreq
|
|
44
|
+
|
|
45
|
+
this.Metadatum = this.constructor.Metadatum
|
|
46
|
+
this.metadata = new Map(this.constructor.metadata)
|
|
14
47
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
48
|
+
static _test(data, key){
|
|
49
|
+
var b = Buffer.from(data.replaceAll(" ",""),"hex")
|
|
50
|
+
const d = new this()
|
|
51
|
+
d.getPathMetadata().forEach((datum,tag)=>{
|
|
52
|
+
d.on(tag,(v)=>console.log(`${tag}=${v}`))
|
|
53
|
+
})
|
|
54
|
+
if (key) {
|
|
55
|
+
d.encryptionKey = key
|
|
56
|
+
b = d.decrypt(b)
|
|
57
|
+
console.log(b)
|
|
58
|
+
}
|
|
59
|
+
d.emitValuesFrom(b)
|
|
60
|
+
d.removeAllListeners()
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
static Metadatum =
|
|
64
|
+
class Metadatum{
|
|
65
|
+
|
|
66
|
+
constructor(tag, unit, description,
|
|
67
|
+
read=()=>{
|
|
68
|
+
return null
|
|
69
|
+
}, gatt=null, type){
|
|
70
|
+
this.tag = tag
|
|
71
|
+
this.unit = unit
|
|
72
|
+
this.description = description
|
|
73
|
+
this.read = read
|
|
74
|
+
this.gatt = gatt
|
|
75
|
+
this.type = type //schema type e.g. 'number'
|
|
76
|
+
}
|
|
77
|
+
asJSONSchema(){
|
|
78
|
+
return {
|
|
79
|
+
type:this?.type??'string',
|
|
80
|
+
title: this?.description,
|
|
81
|
+
unit: this?.unit,
|
|
82
|
+
default: this?.default
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
25
86
|
|
|
26
|
-
static
|
|
27
|
-
|
|
87
|
+
static getMetadata(){
|
|
88
|
+
return this.metadata
|
|
28
89
|
}
|
|
29
90
|
|
|
30
|
-
static
|
|
31
|
-
|
|
91
|
+
static {
|
|
92
|
+
var md = this.addMetadatum("name", "string","Name of sensor" )
|
|
93
|
+
md.isParam=true
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
static addMetadatum(tag, ...args){
|
|
99
|
+
var metadatum = new this.Metadatum(tag, ...args)
|
|
100
|
+
this.getMetadata().set(tag,metadatum)
|
|
101
|
+
return metadatum
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
emitData(tag, buffer, ...args){
|
|
105
|
+
this.emit(tag, this.getMetadatum(tag).read(buffer, ...args))
|
|
32
106
|
}
|
|
33
107
|
|
|
34
|
-
|
|
35
|
-
|
|
108
|
+
async init(){
|
|
109
|
+
this.currentProperties = await this.constructor.getDeviceProps(this.device)
|
|
110
|
+
this.addMetadatum("RSSI","db","Signal strength in db")
|
|
111
|
+
this.getMetadatum("RSSI").default=`sensors.${this.getMacAddress().replaceAll(':', '')}.rssi`
|
|
112
|
+
this.getMetadatum("RSSI").read=()=>{return this.getRSSI()}
|
|
113
|
+
this.getMetadatum("RSSI").read.bind(this)
|
|
114
|
+
if (this.canUseGATT()) {
|
|
115
|
+
var md = this.addMetadatum("useGATT", "boolean", "Use GATT connection")
|
|
116
|
+
md.type="boolean"
|
|
117
|
+
md.isParam=true
|
|
118
|
+
md.isGATT=true
|
|
119
|
+
|
|
120
|
+
md = this.addMetadatum("pollFreq", "s", "Polling frequency in seconds")
|
|
121
|
+
md.type="number"
|
|
122
|
+
md.isParam=true
|
|
123
|
+
md.isGATT=true
|
|
124
|
+
}
|
|
36
125
|
}
|
|
37
126
|
|
|
38
|
-
static
|
|
39
|
-
|
|
127
|
+
static async getManufacturerID(device){
|
|
128
|
+
const md = await this.getDeviceProp(device,'ManufacturerData')
|
|
129
|
+
if (!md) return null
|
|
130
|
+
const keys = Object.keys(md)
|
|
131
|
+
if (keys && keys.length>0){
|
|
132
|
+
return parseInt(keys[0])
|
|
133
|
+
}
|
|
134
|
+
return null
|
|
40
135
|
}
|
|
136
|
+
static NaNif(v1,v2) { return (v1==v2)?NaN:v1 }
|
|
137
|
+
|
|
138
|
+
NaNif(v1,v2) { return this.constructor.NaNif(v1,v2) }
|
|
139
|
+
|
|
41
140
|
|
|
42
|
-
|
|
43
|
-
|
|
141
|
+
addMetadatum(tag, ...args){
|
|
142
|
+
var metadatum = new this.Metadatum(tag, ...args)
|
|
143
|
+
this.getMetadata().set(tag, metadatum)
|
|
144
|
+
return metadatum
|
|
44
145
|
}
|
|
45
146
|
|
|
147
|
+
getMetadata(){
|
|
148
|
+
if (this.metadata==undefined)
|
|
149
|
+
this.metadata= new Map(this.constructor.getMetadata())
|
|
150
|
+
return this.metadata
|
|
151
|
+
}
|
|
152
|
+
getMetadatum(tag){
|
|
153
|
+
return this.getMetadata().get(tag)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getPathMetadata(){
|
|
157
|
+
return new Map(
|
|
158
|
+
[...this.getMetadata().entries()].filter(([key,value]) => !(value?.isParam??false))
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
getParamMetadata(){
|
|
162
|
+
return new Map(
|
|
163
|
+
[...this.getMetadata().entries()].filter(([key,value]) => (value?.isParam??false) && !(value?.isGATT??false))
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
getGATTParamMetadata(){
|
|
167
|
+
return new Map(
|
|
168
|
+
[...this.getMetadata().entries()].filter(([key,value]) => (value?.isParam??false) && (value?.isGATT??false))
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
getGATTDescription() {
|
|
172
|
+
return ""
|
|
173
|
+
}
|
|
174
|
+
getSignalStrength(){
|
|
175
|
+
const rssi = this.getRSSI()
|
|
176
|
+
if (!rssi)
|
|
177
|
+
return NaN
|
|
178
|
+
return signalQualityPercentQuad(rssi)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
getBars(){
|
|
182
|
+
const ss = this.getSignalStrength()
|
|
183
|
+
var bars = ""
|
|
184
|
+
|
|
185
|
+
if (ss>0)
|
|
186
|
+
bars+= '\u{2582} ' //;"▂ "
|
|
187
|
+
if (ss>=30)
|
|
188
|
+
bars+= "\u{2584} "
|
|
189
|
+
if (ss>=60)
|
|
190
|
+
bars+= "\u{2586} "
|
|
191
|
+
if (ss > 80)
|
|
192
|
+
bars+= "\u{2588}"
|
|
193
|
+
return bars
|
|
194
|
+
|
|
195
|
+
}
|
|
196
|
+
getDescription(){
|
|
197
|
+
return `${this.getName()} from ${this.getManufacturer()}`
|
|
198
|
+
}
|
|
199
|
+
getName(){
|
|
200
|
+
return this?.name??this.currentProperties.Name
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
getNameAndAddress(){
|
|
204
|
+
return `${this.getName()} at ${this.getMacAddress()}`
|
|
205
|
+
}
|
|
206
|
+
getDisplayName(){
|
|
207
|
+
return `${ this.getName()} (${ this.getMacAddress()} RSSI: ${this.getRSSI()} db / ${this.getSignalStrength().toFixed()}%) ${ this.getBars()}`
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
getMacAddress(){
|
|
211
|
+
return this.currentProperties.Address
|
|
212
|
+
}
|
|
213
|
+
getRSSI(){
|
|
214
|
+
return this.currentProperties?.RSSI??NaN
|
|
215
|
+
}
|
|
216
|
+
canUseGATT(){
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
usingGATT(){
|
|
220
|
+
return this.useGATT
|
|
221
|
+
}
|
|
222
|
+
valueIfVariant(obj){
|
|
223
|
+
if (obj.constructor && obj.constructor.name=='Variant')
|
|
224
|
+
return obj.value
|
|
225
|
+
else
|
|
226
|
+
return obj
|
|
227
|
+
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
static async getPropsProxy(device){
|
|
231
|
+
|
|
232
|
+
const objectProxy = await device.helper.dbus.getProxyObject(device.helper.service, device.helper.object)
|
|
233
|
+
if (!device._propsProxy)
|
|
234
|
+
device._propsProxy = await objectProxy.getInterface('org.freedesktop.DBus.Properties')
|
|
235
|
+
return device._propsProxy
|
|
236
|
+
}
|
|
237
|
+
static async getDeviceProps(device, propNames=[]){
|
|
238
|
+
const _propsProxy = await this.getPropsProxy(device)
|
|
239
|
+
const rawProps = await _propsProxy.GetAll(device.helper.iface)
|
|
240
|
+
const props = {}
|
|
241
|
+
for (const propKey in rawProps) {
|
|
242
|
+
if (propNames.length==0 || propNames.indexOf(propKey)>=0)
|
|
243
|
+
props[propKey] = rawProps[propKey].value
|
|
244
|
+
}
|
|
245
|
+
return props
|
|
246
|
+
}
|
|
247
|
+
static async getDeviceProp(device, prop){
|
|
248
|
+
const _propsProxy = await this.getPropsProxy(device)
|
|
249
|
+
try{
|
|
250
|
+
const rawProps = await _propsProxy.Get(device.helper.iface,prop)
|
|
251
|
+
return rawProps?.value
|
|
252
|
+
}
|
|
253
|
+
catch(e){
|
|
254
|
+
return null //Property $prop (probably) doesn't exist in $device
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* callback function on device properties changing
|
|
260
|
+
*/
|
|
261
|
+
propertiesChanged(props){
|
|
262
|
+
|
|
263
|
+
if (props.RSSI) {
|
|
264
|
+
this.currentProperties.RSSI=this.valueIfVariant(props.RSSI)
|
|
265
|
+
this.emit("RSSI", this.currentProperties.RSSI)
|
|
266
|
+
}
|
|
267
|
+
if (props.ServiceData)
|
|
268
|
+
this.currentProperties.ServiceData=this.valueIfVariant(props.ServiceData)
|
|
269
|
+
|
|
270
|
+
if (props.ManufacturerData)
|
|
271
|
+
this.currentProperties.ManufacturerData=this.valueIfVariant(props.ManufacturerData)
|
|
272
|
+
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
getServiceData(key){
|
|
276
|
+
if (this.currentProperties.ServiceData)
|
|
277
|
+
return this.valueIfVariant (this.currentProperties.ServiceData[key])
|
|
278
|
+
else
|
|
279
|
+
return null
|
|
280
|
+
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
getManufacturerID(){
|
|
284
|
+
const md = this.currentProperties.ManufacturerData
|
|
285
|
+
if (md){
|
|
286
|
+
const keys = Object.keys(this.valueIfVariant(md))
|
|
287
|
+
if (keys.length>0)
|
|
288
|
+
return parseInt(keys[0])
|
|
289
|
+
}
|
|
290
|
+
return null
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
getManufacturer(){
|
|
294
|
+
const id = this.getManufacturerID()
|
|
295
|
+
return (id==null)?"Unknown manufacturer":BTCompanyMap.get(parseInt(id))
|
|
296
|
+
}
|
|
297
|
+
getManufacturerData(key){
|
|
298
|
+
if (this.currentProperties.ManufacturerData)
|
|
299
|
+
return this.valueIfVariant (this.currentProperties.ManufacturerData[key])
|
|
300
|
+
else
|
|
301
|
+
return null
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
initGATTInterval(){
|
|
305
|
+
this.device.disconnect().then(()=>{
|
|
306
|
+
this.initPropertiesChanged()
|
|
307
|
+
this.intervalID = setInterval( () => {
|
|
308
|
+
this.initGATT().then(()=>{
|
|
309
|
+
this.emitGATT()
|
|
310
|
+
this.device.disconnect()
|
|
311
|
+
.then(()=>
|
|
312
|
+
this.initPropertiesChanged()
|
|
313
|
+
)
|
|
314
|
+
.catch((e)=>{
|
|
315
|
+
this.debug(`Error disconnecting from ${this.getName()}: ${e.message}`)
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
.catch((error)=>{
|
|
319
|
+
this.debug(error)
|
|
320
|
+
throw new Error(`unable to emit values for device ${this.getName()}:${error}`)
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
, this.pollFreq*1000)
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
initPropertiesChanged(){
|
|
327
|
+
|
|
328
|
+
this.propertiesChanged.bind(this)
|
|
329
|
+
this.device.helper._prepare()
|
|
330
|
+
this.device.helper.on("PropertiesChanged",
|
|
331
|
+
((props)=> {
|
|
332
|
+
this.propertiesChanged(props)
|
|
333
|
+
}))
|
|
334
|
+
}
|
|
46
335
|
/**
|
|
47
336
|
* Connect to sensor.
|
|
48
337
|
* This is where the logic for connecting to sensor, listening for changes in values and emitting those values go
|
|
49
|
-
* @throws Error if unimplemented by subclass
|
|
50
338
|
*/
|
|
51
|
-
|
|
52
339
|
connect(){
|
|
53
|
-
|
|
340
|
+
this.initPropertiesChanged()
|
|
341
|
+
this.propertiesChanged(this.currentProperties)
|
|
342
|
+
if (this.usingGATT()){
|
|
343
|
+
this.initGATT().then(async ()=>{
|
|
344
|
+
this.emitGATT()
|
|
345
|
+
if (this.pollFreq){
|
|
346
|
+
this.initGATTInterval()
|
|
347
|
+
}
|
|
348
|
+
else
|
|
349
|
+
await this.initGATTNotifications()
|
|
350
|
+
})
|
|
351
|
+
.catch((e)=>this.debug(`GATT services unavailable for ${this.getName()}. Reason: ${e}`))
|
|
352
|
+
}
|
|
353
|
+
return this
|
|
54
354
|
}
|
|
55
355
|
/**
|
|
56
356
|
* Discconnect from sensor.
|
|
@@ -58,22 +358,19 @@ class BTSensor {
|
|
|
58
358
|
*/
|
|
59
359
|
|
|
60
360
|
disconnect(){
|
|
61
|
-
this.
|
|
361
|
+
this.removeAllListeners()
|
|
362
|
+
this.device.helper.removeListeners()
|
|
363
|
+
if (this.intervalID){
|
|
364
|
+
clearInterval(this.intervalID)
|
|
365
|
+
}
|
|
62
366
|
}
|
|
63
367
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
on(eventName, ...args){
|
|
71
|
-
this.eventEmitter.on(eventName, ...args)
|
|
368
|
+
emitValuesFrom(buffer){
|
|
369
|
+
this.getMetadata().forEach((datum, tag)=>{
|
|
370
|
+
if (!(datum.isParam||datum.notify) && datum.read)
|
|
371
|
+
this.emit(tag, datum.read(buffer))
|
|
372
|
+
})
|
|
72
373
|
}
|
|
73
|
-
emit(eventName, value){
|
|
74
|
-
this.eventEmitter.emit(eventName,value);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
374
|
}
|
|
78
375
|
|
|
79
376
|
module.exports = BTSensor
|
package/README.md
CHANGED
|
@@ -46,12 +46,12 @@ This will be the recommended installation when the code is ready for wider shari
|
|
|
46
46
|
From a command prompt:<br>
|
|
47
47
|
|
|
48
48
|
<pre> cd ~/[some_dir]
|
|
49
|
-
git clone https://github.com/naugehyde/
|
|
50
|
-
cd
|
|
49
|
+
git clone https://github.com/naugehyde/bt-sensors-plugin-sk
|
|
50
|
+
cd bt-sensors-plugin-sk
|
|
51
51
|
npm i
|
|
52
52
|
[sudo] npm link
|
|
53
53
|
cd [signalk_home]
|
|
54
|
-
npm link
|
|
54
|
+
npm link bt-sensors-plugin-sk</pre>
|
|
55
55
|
|
|
56
56
|
Finally, restart SK. Plugin should appear in your server plugins list.<br>
|
|
57
57
|
|