bt-sensors-plugin-sk 1.1.0-beta.2.2.6 → 1.2.0-beta.0.0.1.test

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
@@ -10,7 +10,8 @@ const EventEmitter = require('node:events');
10
10
  * {@link module:node-ble}
11
11
  */
12
12
 
13
- const BTCompanies = require('./bt_co.json')
13
+ const BTCompanies = require('./bt_co.json');
14
+
14
15
  /**
15
16
  * @global A map of company names keyed by their Bluetooth ID
16
17
  * {@link ./sensor_classes/bt_co.json} file derived from bluetooth-sig source:
@@ -51,6 +52,38 @@ function signalQualityPercentQuad(rssi, perfect_rssi=-20, worst_rssi=-85) {
51
52
  }
52
53
  return Math.ceil(signal_quality);
53
54
  }
55
+ function preparePath(obj, str) {
56
+ const regex = /\{([^}]+)\}/g;
57
+ let match;
58
+ let resultString = "";
59
+ let lastIndex = 0;
60
+
61
+ while ((match = regex.exec(str)) !== null) {
62
+ const fullMatch = match[0];
63
+ const keyToAccess = match[1].trim();
64
+
65
+ // Append the text before the current curly braces
66
+ resultString += str.substring(lastIndex, match.index);
67
+ lastIndex = regex.lastIndex;
68
+
69
+ try {
70
+ let evalResult = obj[keyToAccess];
71
+ if (typeof evalResult === 'function'){
72
+ evalResult= evalResult.call(obj)
73
+ }
74
+
75
+ resultString += evalResult !== undefined ? evalResult : `${keyToAccess}_value_undefined`;
76
+ } catch (error) {
77
+ console.error(`Error accessing key '${keyToAccess}':`, error);
78
+ resultString += fullMatch; // Keep the original curly braces on error
79
+ }
80
+ }
81
+
82
+ // Append any remaining text after the last curly braces
83
+ resultString += str.substring(lastIndex);
84
+
85
+ return resultString || str; // Return original string if no replacements were made
86
+ }
54
87
 
55
88
  /**
56
89
  * @classdesc Class that all sensor classes should inherit from. Sensor subclasses
@@ -65,8 +98,9 @@ function signalQualityPercentQuad(rssi, perfect_rssi=-20, worst_rssi=-85) {
65
98
  */
66
99
 
67
100
  class BTSensor extends EventEmitter {
68
- static metadata=new Map()
69
-
101
+ //static metadata=new Map()
102
+ static DEFAULTS = require('./plugin_defaults.json');
103
+
70
104
  /**
71
105
  *
72
106
  * @param {module:node-ble/Device} device
@@ -77,13 +111,11 @@ class BTSensor extends EventEmitter {
77
111
  super()
78
112
 
79
113
  this.device=device
80
- this.name = config?.name
81
-
82
- this.useGATT = gattConfig?.useGATT
83
- this.pollFreq = gattConfig?.pollFreq
84
-
85
- this.Metadatum = this.constructor.Metadatum
86
- this.metadata = new Map()
114
+
115
+ Object.assign(this,config)
116
+ Object.assign(this,gattConfig)
117
+
118
+ this._state = null
87
119
  }
88
120
  /**
89
121
  * @function _test Test sensor parsing
@@ -133,7 +165,7 @@ class BTSensor extends EventEmitter {
133
165
  var b = Buffer.from(data.replaceAll(" ",""),"hex")
134
166
  const d = new this(null,config)
135
167
  d.initMetadata()
136
- d.getPathMetadata().forEach((datum,tag)=>{
168
+ Object.keys(d.getPaths()).forEach((tag)=>{
137
169
  d.on(tag,(v)=>console.log(`${tag}=${v}`))
138
170
  })
139
171
  if (key) {
@@ -145,38 +177,7 @@ class BTSensor extends EventEmitter {
145
177
  d.removeAllListeners()
146
178
 
147
179
  }
148
- static Metadatum =
149
- /**
150
- * @class encapsulates a sensor's metadata
151
- * @todo refactor and/or just plain rethink constructor parameters
152
- */
153
- class Metadatum{
154
-
155
- constructor(tag, unit, description, read, gatt=null, type){
156
- this.tag = tag
157
- this.unit = unit
158
- this.description = description
159
- this.read = read
160
- this.gatt = gatt
161
- this.type = type //schema type e.g. 'number'
162
- }
163
- /**
164
- *
165
- * @returns A JSON object passed by plugin to the plugin's schema
166
- * dynamically updated at runtime upon discovery and interrogation
167
- * of the device
168
- */
169
- asJSONSchema(){
170
- return {
171
- type:this?.type??'string',
172
- title: this?.description,
173
- unit: this?.unit,
174
- enum: this?.enum,
175
- default: this?.default
176
- }
177
- }
178
- }
179
-
180
+
180
181
  //static utility Functions
181
182
  /**
182
183
  *
@@ -274,28 +275,93 @@ class BTSensor extends EventEmitter {
274
275
  *
275
276
  */
276
277
 
278
+ initSchema(){
279
+ this._schema = {
280
+ properties:{
281
+ active: {title: "Active", type: "boolean", default: true },
282
+ discoveryTimeout: {title: "Device discovery timeout (in seconds)",
283
+ type: "integer", default:30,
284
+ minimum: 10,
285
+ maximum: 600 },
286
+
287
+ params:{
288
+ title:`Device parameters`,
289
+ description: this.getDescription(),
290
+ type:"object",
291
+ properties:{}
292
+ },
293
+ paths:{
294
+ title:"Signalk Paths",
295
+ description: `Signalk paths to be updated when ${this.getName()}'s values change`,
296
+ type:"object",
297
+ properties:{}
298
+ }
299
+ }
300
+ }
301
+
302
+ if (this.hasGATT()){
303
+
304
+ this._schema.properties.gattParams={
305
+ title:`GATT Specific device parameters`,
306
+ description: this.getGATTDescription(),
307
+ type:"object",
308
+ properties:{
309
+ useGATT: {title: "Use GATT connection", type: "boolean", default: false },
310
+ pollFreq: { type: "number", title: "Polling frequency in seconds"}
311
+ }
312
+ }
313
+ }
314
+
315
+ }
277
316
  async init(){
317
+ this.currentProperties = await this.constructor.getDeviceProps(this.device)
318
+ this.initSchema()
319
+
320
+
278
321
  //create the 'name' parameter
279
- var md = this.addMetadatum("name", "string","Name of sensor" )
280
- md.isParam=true
322
+ this.addDefaultParam("name")
323
+
324
+ //create the 'location' parameter
325
+
326
+ this.addDefaultParam("location")
327
+
281
328
  //create the 'RSSI' parameter
282
- this.currentProperties = await this.constructor.getDeviceProps(this.device)
283
- this.addMetadatum("RSSI","db","Signal strength in db")
284
- this.getMetadatum("RSSI").default=`sensors.${this.getMacAddress().replaceAll(':', '')}.rssi`
285
- this.getMetadatum("RSSI").read=()=>{return this.getRSSI()}
286
- this.getMetadatum("RSSI").read.bind(this)
287
- //create GATT params (iff sensor is GATT-ish)
288
- if (this.hasGATT()) {
289
- md = this.addMetadatum("useGATT", "boolean", "Use GATT connection")
290
- md.type="boolean"
291
- md.isParam=true
292
- md.isGATT=true
293
-
294
- md = this.addMetadatum("pollFreq", "s", "Polling frequency in seconds")
295
- md.type="number"
296
- md.isParam=true
297
- md.isGATT=true
329
+ this.addDefaultPath("RSSI","sensors.RSSI")
330
+ this.getPath("RSSI").read=()=>{return this.getRSSI()}
331
+ this.getPath("RSSI").read.bind(this)
332
+ this.initListen()
333
+ }
334
+
335
+ initListen(){
336
+ Promise.resolve(this.listen())
337
+ }
338
+ activate(config, plugin){
339
+ if (config.paths){
340
+ this.createPaths(config,plugin.id)
341
+ this.initPaths(config,plugin.id)
342
+ this.debug(`Paths activated for ${this.getDisplayName()}`);
298
343
  }
344
+ if (this.usingGATT()){
345
+ try {
346
+ this.activateGATT()
347
+ } catch (e) {
348
+ this.debug(`GATT services unavailable for ${this.getName()}. Reason: ${e}`)
349
+ this._state="ERROR"
350
+ return
351
+ }
352
+ }
353
+ this._state="ACTIVE"
354
+ }
355
+
356
+ activateGATT(){
357
+ this.initGATTConnection().then(async ()=>{
358
+ this.emitGATT()
359
+ if (this.pollFreq){
360
+ this.initGATTInterval()
361
+ }
362
+ else
363
+ await this.initGATTNotifications()
364
+ })
299
365
  }
300
366
 
301
367
  /**
@@ -307,9 +373,55 @@ class BTSensor extends EventEmitter {
307
373
  */
308
374
 
309
375
  addMetadatum(tag, ...args){
310
- var metadatum = new this.Metadatum(tag, ...args)
311
- this.getMetadata().set(tag, metadatum)
312
- return metadatum
376
+
377
+ const md = {}
378
+ if (args[0]) md.unit = args[0]
379
+ if (args[1]) md.title = args[1]
380
+ if (args[2]) md.read = args[2]
381
+ if (args[3]) md.gatt = args[3]
382
+ if (args[4]) md.type = args[4]
383
+
384
+ return this.addPath(tag,md)
385
+ }
386
+
387
+ addParameter(tag, param){
388
+
389
+ if (!param.type)
390
+ param.type="string"
391
+
392
+ this._schema.properties.params.properties[tag]=param
393
+ return this._schema.properties.params.properties[tag]
394
+ }
395
+
396
+ addPath(tag, path){
397
+ if (!path.type)
398
+ path.type="string"
399
+
400
+ if (!path.pattern)
401
+ path.pattern="^(?:[^{}\\s]*\\{[a-zA-Z0-9]+\\}[^{}\\s]*|[^{}\\s]*)$"
402
+ this._schema.properties.paths.properties[tag]=path
403
+ return this._schema.properties.paths.properties[tag]
404
+ }
405
+
406
+ addGATTParameter(tag, param){
407
+
408
+ if (!param.type)
409
+ param.type="string"
410
+
411
+ return this._schema.properties.gattParams.properties[tag]=param
412
+ }
413
+
414
+ addDefaultPath(tag,defaultPath){
415
+ const path = eval(`BTSensor.DEFAULTS.${defaultPath}`)
416
+ return this.addPath(tag,Object.assign({}, path))
417
+ }
418
+
419
+ addDefaultParam(tag){
420
+ return this.addParameter(tag,Object.assign({}, BTSensor.DEFAULTS.params[tag]))
421
+ }
422
+
423
+ getJSONSchema(){
424
+ return this._schema
313
425
  }
314
426
 
315
427
  //GATT Initialization functions
@@ -376,12 +488,12 @@ class BTSensor extends EventEmitter {
376
488
  */
377
489
  initPropertiesChanged(){
378
490
 
379
- this.propertiesChanged.bind(this)
491
+ this._propertiesChanged.bind(this)
380
492
  this.device.helper._prepare()
381
493
  this.device.helper.on("PropertiesChanged",
382
494
  ((props)=> {
383
495
  try{
384
- this.propertiesChanged(props)
496
+ this._propertiesChanged(props)
385
497
  }
386
498
  catch(error){
387
499
  this.debug(`Error occured on ${this.getNameAndAddress()}: ${error?.message??error}`)
@@ -392,30 +504,20 @@ class BTSensor extends EventEmitter {
392
504
  //END instance initialization functions
393
505
 
394
506
  //Metadata functions
395
- getMetadata(){
396
- if (this.metadata==undefined)
397
- this.metadata= new Map(this.constructor.getMetadata())
398
- return this.metadata
399
- }
400
507
 
401
- getMetadatum(tag){
402
- return this.getMetadata().get(tag)
508
+ getPath(tag){
509
+ return this._schema.properties.paths.properties[tag]
403
510
  }
404
511
 
405
- getPathMetadata(){
406
- return new Map(
407
- [...this.getMetadata().entries()].filter(([key,value]) => !(value?.isParam??false))
408
- )
512
+ getPaths(){
513
+ return this._schema.properties.paths.properties
409
514
  }
410
- getParamMetadata(){
411
- return new Map(
412
- [...this.getMetadata().entries()].filter(([key,value]) => (value?.isParam??false) && !(value?.isGATT??false))
413
- )
515
+ getParams(){
516
+ return this._schema.properties.params.properties
414
517
  }
415
- getGATTParamMetadata(){
416
- return new Map(
417
- [...this.getMetadata().entries()].filter(([key,value]) => (value?.isParam??false) && (value?.isGATT??false))
418
- )
518
+ getGATTParams(){
519
+ return this._schema.properties.gattParams.properties
520
+
419
521
  }
420
522
  //End metadata functions
421
523
 
@@ -429,9 +531,13 @@ class BTSensor extends EventEmitter {
429
531
  return `${this.getName()} from ${this.getManufacturer()}`
430
532
  }
431
533
  getName(){
432
- return this?.name??this.currentProperties.Name
433
- }
534
+ const name = this?.name??this.currentProperties.Name
535
+ return name?name:"Unknown"
434
536
 
537
+ }
538
+ macAndName(){
539
+ return `${this.getName().replaceAll(':', '-').replaceAll(" ","_")}-${this.getMacAddress().replaceAll(':', '-')}`
540
+ }
435
541
  getNameAndAddress(){
436
542
  return `${this.getName()} at ${this.getMacAddress()}`
437
543
  }
@@ -475,6 +581,8 @@ class BTSensor extends EventEmitter {
475
581
  else
476
582
  return null
477
583
  }
584
+
585
+
478
586
  //END Device property functions
479
587
 
480
588
  //Sensor RSSI state functions
@@ -490,6 +598,13 @@ class BTSensor extends EventEmitter {
490
598
  return signalQualityPercentQuad(rssi)
491
599
  }
492
600
 
601
+ getState(){
602
+ return this._state
603
+ }
604
+
605
+ isActive(){
606
+ return this._state=="ACTIVE"
607
+ }
493
608
  getBars(){
494
609
  const ss = this.getSignalStrength()
495
610
  var bars = ""
@@ -532,7 +647,8 @@ class BTSensor extends EventEmitter {
532
647
  * @param {*} props which contains ManufacturerData and ServiceData (where the sensor's data resides)
533
648
  * set up by BTSensor::initPropertiesChanged()
534
649
  */
535
- propertiesChanged(props){
650
+ _propertiesChanged(props){
651
+ this._lastContact=Date.now()
536
652
 
537
653
  if (props.RSSI) {
538
654
  this.currentProperties.RSSI=this.valueIfVariant(props.RSSI)
@@ -543,8 +659,13 @@ class BTSensor extends EventEmitter {
543
659
 
544
660
  if (props.ManufacturerData)
545
661
  this.currentProperties.ManufacturerData=this.valueIfVariant(props.ManufacturerData)
662
+ if (this.isActive())
663
+ this.propertiesChanged(props)
546
664
 
547
665
  }
666
+ propertiesChanged(props){
667
+ //implemented by subclass
668
+ }
548
669
 
549
670
  /**
550
671
  *
@@ -555,14 +676,18 @@ class BTSensor extends EventEmitter {
555
676
  }
556
677
 
557
678
  emitData(tag, buffer, ...args){
558
- this.emit(tag, this.getMetadatum(tag).read(buffer, ...args))
679
+ const md = this.getPath(tag)
680
+ if (md && md.read)
681
+ this.emit(tag, md.read(buffer, ...args))
682
+
683
+
559
684
  }
560
685
 
561
686
  emitValuesFrom(buffer){
562
- this.getMetadata().forEach((datum, tag)=>{
563
- if (!(datum.isParam||datum.notify) && datum.read)
564
- this.emit(tag, datum.read(buffer))
565
- })
687
+ Object.keys(this.getPaths())
688
+ .forEach(
689
+ (tag)=>this.emitData(tag,buffer)
690
+ )
566
691
  }
567
692
 
568
693
  /**
@@ -576,21 +701,10 @@ class BTSensor extends EventEmitter {
576
701
  listen(){
577
702
  try{
578
703
  this.initPropertiesChanged()
579
- this.propertiesChanged(this.currentProperties)
704
+ this._propertiesChanged(this.currentProperties)
580
705
  } catch(e){
581
706
  this.debug(e)
582
707
  }
583
- if (this.usingGATT()){
584
- this.initGATTConnection().then(async ()=>{
585
- this.emitGATT()
586
- if (this.pollFreq){
587
- this.initGATTInterval()
588
- }
589
- else
590
- await this.initGATTNotifications()
591
- })
592
- .catch((e)=>this.debug(`GATT services unavailable for ${this.getName()}. Reason: ${e}`))
593
- }
594
708
  return this
595
709
  }
596
710
 
@@ -601,12 +715,13 @@ class BTSensor extends EventEmitter {
601
715
  * Called automatically by Plugin::plugin.stop()
602
716
  */
603
717
 
604
- stopListening(){
718
+ async stopListening(){
605
719
  this.removeAllListeners()
606
720
  this.device.helper.removeListeners()
607
721
  if (this.intervalID){
608
722
  clearInterval(this.intervalID)
609
- }
723
+ }
724
+ this._state="ASLEEP"
610
725
  }
611
726
  //END Sensor listen-to-changes functions
612
727
 
@@ -622,6 +737,41 @@ class BTSensor extends EventEmitter {
622
737
 
623
738
  }
624
739
  //End instance utility functions
740
+
741
+ createPaths(config, id){
742
+ Object.keys(this.getPaths()).forEach((tag)=>{
743
+ const pathMeta=this.getPath(tag)
744
+ const path = config.paths[tag]
745
+ if (!(path===undefined))
746
+ this.app.handleMessage(id,
747
+ {
748
+ updates:
749
+ [{ meta: [{path: preparePath(this, path), value: { units: pathMeta?.unit }}]}]
750
+ })
751
+ })
752
+ }
753
+
754
+ initPaths(deviceConfig, id){
755
+ Object.keys(this.getPaths()).forEach((tag)=>{
756
+ const pathMeta=this.getPath(tag)
757
+ const path = deviceConfig.paths[tag];
758
+ if (!(path === undefined)) {
759
+ this.on(tag, (val)=>{
760
+ if (pathMeta.notify){
761
+ this.app.notify(tag, val, id )
762
+ } else {
763
+ this.updatePath(preparePath(this,path),val,id)
764
+ }
765
+ })
766
+ }
767
+ })
768
+ }
769
+ updatePath(path, val,id){
770
+ this.app.handleMessage(id, {updates: [ { values: [ {path: path, value: val }] } ] })
771
+ }
772
+ elapsedTimeSinceLastContact(){
773
+ return (Date.now()-this?._lastContact??Date.now())/1000
774
+ }
625
775
  }
626
776
 
627
777
  module.exports = BTSensor
package/README.md CHANGED
@@ -1,40 +1,8 @@
1
1
  # Bluetooth Sensors for [Signal K](http://www.signalk.org)
2
2
 
3
- ## BETA 2.2.4
4
- ### What's New
5
- Fix to 2.2.3 scan and connect issues. Added Transport option to Config page. Default is Bluetooth LE (was Auto in 2.2.3).
6
-
7
- ## BETA 2.2.3
8
- ### What's New
9
- Support for [JBD/Jiabaida/Xiaoxiang Battery management systems](https://jiabaida-bms.com/), IBeacon and clone devices courtesy of [Arjen R](https://github.com/ArjenR).
10
-
11
- ### What's New for Developers
12
- Runtime loading of external device modules. See (https://github.com/naugehyde/bt-sensors-plugin-sk/discussions/26)
13
- ## BETA 2.2.2
14
- ### What's New
15
- Support for Lancol Battery Meters, Kilovault HLX+ smart batteries courtesy of [sdlee1963](https://github.com/sdlee1963) and baseline support for BTHome devices as well as support for the ShellySBHT003C enviromental sensor courtesy of [Sebastian Haas](https://github.com/sebastianhaas)
16
-
17
- Fixed incorrect reporting of Victron Battery Monitor -> aux temperature values.
18
-
19
- ## BETA 2.2.1
20
-
21
- ### What's New
22
-
23
- GoveeH50xx sensor support. Selectable bluetooth adapter on config screen (in case you have more than bluetooth adapter on your server).
24
-
25
-
26
- ## BETA 2.2.0
27
-
28
- ### What's New
29
-
30
- Support for Aranet4 environment sensor and Renogy Rover/Wanderer Controllers. Untested support for Renogy Battery and Inverter clients. If you have a Renogy Battery or Inverter with bluetooth support, please give it a try and let me know how it goes.
31
-
32
- ### RENOGY NOTES
33
-
34
- The class of Renogy Devices cannot be reliably identified from their Bluetooth advertisements. <br>
35
-
36
- On the plugin config page, You will need to select the device from the Device dropdown, then select the appropriate class from the Class dropdown. After that, you will need to hit the Submit button. On restart of the plugin, the plugin should recognize the device. If you've selected the appropriate class (RenogyRoverClient for example), you should see configs for paths. <br>
3
+ ## Beta 1.0.0
37
4
 
5
+ Dynamic configuration added (no more screen refreshing necessary).
38
6
 
39
7
  ## WHAT IT IS
40
8