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 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
- constructor(device) {
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.eventEmitter = new EventEmitter();
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
- * 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
21
- */
22
- static needsScannerOn(){
23
- return true
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 events() {
27
- throw new Error("events() static function must be implemented by subclass")
87
+ static getMetadata(){
88
+ return this.metadata
28
89
  }
29
90
 
30
- static metadataTags() {
31
- return this.metadata.keys()
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
- static hasMetaData(id) {
35
- return this.metadata.has(id)
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 unitFor(id){
39
- return this.metadata.get(id)?.unit
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
- static instantiable(){
43
- return true;
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
- throw new Error("connect() member function must be implemented by subclass")
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.eventEmitter.removeAllListeners()
361
+ this.removeAllListeners()
362
+ this.device.helper.removeListeners()
363
+ if (this.intervalID){
364
+ clearInterval(this.intervalID)
365
+ }
62
366
  }
63
367
 
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)
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/ping-ac-outlet-plugin-sk
50
- cd ping_ac_outlet_plugin_sk
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 ping-ac-outlet-plugin-sk</pre>
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