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 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
- constructor(device) {
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.eventEmitter = new EventEmitter();
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
- * tells plugin if the class needs to keep the scanner running.
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
- static needsScannerOn(){
23
- return true
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
- static metadataTags() {
31
- return this.metadata.keys()
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
- static hasMetaData(id) {
35
- return this.metadata.has(id)
287
+ getManufacturer(){
288
+ const id = this.getManufacturerID()
289
+ return (id==null)?"Unknown manufacturer":BTCompanyMap.get(parseInt(id))
36
290
  }
37
291
 
38
- static unitFor(id){
39
- return this.metadata.get(id)?.unit
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
- static instantiable(){
43
- return true;
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
- throw new Error("connect() member function must be implemented by subclass")
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.eventEmitter.removeAllListeners()
359
+ this.removeAllListeners()
360
+ this.device.helper.removeListeners()
361
+ if (this.intervalID){
362
+ clearInterval(this.intervalID)
363
+ }
62
364
  }
63
365
 
64
- /**
65
- * Convenience method for emitting value changes.
66
- * Just passes on(eventName, ...args) through to EventEmitter instance
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