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