bt-sensors-plugin-sk 1.2.5-1 → 1.2.6-beta

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.
Files changed (127) hide show
  1. package/BTSensor.js +74 -38
  2. package/DistanceManager.js +249 -0
  3. package/Mixin.js +19 -0
  4. package/OutOfRangeDevice.js +46 -0
  5. package/README.md +14 -0
  6. package/classLoader.js +7 -2
  7. package/connectUUID.exp +26 -0
  8. package/index.js +49 -3
  9. package/package.json +8 -6
  10. package/public/159.js +1 -1
  11. package/public/540.js +1 -1
  12. package/public/681.js +3 -9
  13. package/public/681.js.LICENSE.txt +2 -0
  14. package/public/764.js +1 -0
  15. package/public/images/ATC.jpeg +0 -0
  16. package/public/images/Aranet4.webp +0 -0
  17. package/public/images/BP108B.webp +0 -0
  18. package/public/images/GoveeH5074.jpg +0 -0
  19. package/public/images/GoveeH5075.webp +0 -0
  20. package/public/images/GoveeH510x.jpg +0 -0
  21. package/public/images/InkbirdTH3.webp +0 -0
  22. package/public/images/JBDBMS.webp +0 -0
  23. package/public/images/Junctek.webp +0 -0
  24. package/public/images/KilovaultHLXPlus.jpg +0 -0
  25. package/public/images/LancolVoltageMeter.webp +0 -0
  26. package/public/images/LiTimeLiFePo4Battery.avif +0 -0
  27. package/public/images/MercurySmartcraft.jpg +0 -0
  28. package/public/images/MopekaTankSensor.jpg +0 -0
  29. package/public/images/RemoranWave3.jpeg +0 -0
  30. package/public/images/RenogyInverter.jpg +0 -0
  31. package/public/images/RenogyRoverClient.jpg +0 -0
  32. package/public/images/RenogySmartLiFePo4Battery.webp +0 -0
  33. package/public/images/RuuviTag.jpg +0 -0
  34. package/public/images/ShellyBLUHT.webp +0 -0
  35. package/public/images/ShellyBLUMotion.webp +0 -0
  36. package/public/images/ShellyBluDoorWindow.webp +0 -0
  37. package/public/images/Skanbatt.jpg +0 -0
  38. package/public/images/SmartBatteryProtect.webp +0 -0
  39. package/public/images/SmartBatterySense.webp +0 -0
  40. package/public/images/SwitchBotMeterPlus.webp +0 -0
  41. package/public/images/SwitchBotTH.webp +0 -0
  42. package/public/images/TopbandBattery.webp +0 -0
  43. package/public/images/Ultrasonic.jpg +0 -0
  44. package/public/images/VictronBlueSmartACCharger.jpg +0 -0
  45. package/public/images/VictronBlueSolarMPPT.jpeg +0 -0
  46. package/public/images/VictronCerboGX.webp +0 -0
  47. package/public/images/VictronInverterRS.webp +0 -0
  48. package/public/images/VictronLynxSmartBMS.webp +0 -0
  49. package/public/images/VictronMultiPlus-II.webp +0 -0
  50. package/public/images/VictronOrionTrIsolated.webp +0 -0
  51. package/public/images/VictronOrionTrNonIsolated.webp +0 -0
  52. package/public/images/VictronPhoenixInverter.webp +0 -0
  53. package/public/images/VictronPhoenixSmart1600.webp +0 -0
  54. package/public/images/VictronSmartBatteryProtect.jpg +0 -0
  55. package/public/images/VictronSmartIP43.webp +0 -0
  56. package/public/images/VictronSmartLithiumBattery.jpg +0 -0
  57. package/public/images/VictronSmartSolarMPPT.webp +0 -0
  58. package/public/images/VictronVEBus.webp +0 -0
  59. package/public/images/iBeacon.jpg +0 -0
  60. package/public/main.js +1 -1
  61. package/public/remoteEntry.js +1 -1
  62. package/readUUID.exp +23 -0
  63. package/sensor_classes/ATC.js +3 -2
  64. package/sensor_classes/Aranet2.js +3 -1
  65. package/sensor_classes/Aranet4.js +1 -2
  66. package/sensor_classes/BankManager.js +1 -1
  67. package/sensor_classes/Beacon/AbstractBeaconMixin.js +85 -0
  68. package/sensor_classes/Beacon/Eddystone.js +77 -0
  69. package/sensor_classes/Beacon/iBeacon.js +58 -0
  70. package/sensor_classes/EctiveBMS.js +270 -0
  71. package/sensor_classes/FeasyComBeacon.js +68 -0
  72. package/sensor_classes/GobiusCTankMeter.js +4 -3
  73. package/sensor_classes/GoveeH5074.js +2 -0
  74. package/sensor_classes/GoveeH5075.js +2 -0
  75. package/sensor_classes/GoveeH510x.js +1 -0
  76. package/sensor_classes/Inkbird.js +1 -0
  77. package/sensor_classes/JBDBMS.js +1 -0
  78. package/sensor_classes/Junctek.js +14 -6
  79. package/sensor_classes/KilovaultHLXPlus.js +1 -0
  80. package/sensor_classes/LancolVoltageMeter.js +2 -0
  81. package/sensor_classes/MercurySmartcraft.js +1 -0
  82. package/sensor_classes/MopekaTankSensor.js +3 -200
  83. package/sensor_classes/RemoranWave3.js +2 -0
  84. package/sensor_classes/Renogy/RenogySensor.js +1 -0
  85. package/sensor_classes/RenogyBattery.js +3 -4
  86. package/sensor_classes/RenogyInverter.js +3 -6
  87. package/sensor_classes/RenogyRoverClient.js +3 -0
  88. package/sensor_classes/RuuviTag.js +6 -3
  89. package/sensor_classes/ShellySBDW002C.js +3 -1
  90. package/sensor_classes/ShellySBHT003C.js +7 -0
  91. package/sensor_classes/ShellySBMO003Z.js +3 -2
  92. package/sensor_classes/ShenzhenLiOnBMS.js +4 -0
  93. package/sensor_classes/SwitchBotMeterPlus.js +1 -1
  94. package/sensor_classes/SwitchBotTH.js +2 -1
  95. package/sensor_classes/UNKNOWN.js +2 -1
  96. package/sensor_classes/UltrasonicWindMeter.js +3 -0
  97. package/sensor_classes/Victron/VictronConstants.js +2 -0
  98. package/sensor_classes/Victron/VictronIdentifier.js +24 -0
  99. package/sensor_classes/Victron/VictronSensor.js +43 -54
  100. package/sensor_classes/VictronACCharger.js +1 -6
  101. package/sensor_classes/VictronBatteryMonitor.js +37 -26
  102. package/sensor_classes/VictronDCDCConverter.js +1 -4
  103. package/sensor_classes/VictronDCEnergyMeter.js +1 -4
  104. package/sensor_classes/VictronGXDevice.js +1 -4
  105. package/sensor_classes/VictronInverter.js +2 -3
  106. package/sensor_classes/VictronInverterRS.js +2 -4
  107. package/sensor_classes/VictronLynxSmartBMS.js +1 -4
  108. package/sensor_classes/VictronOrionXS.js +1 -3
  109. package/sensor_classes/VictronSmartBatteryProtect.js +1 -4
  110. package/sensor_classes/VictronSmartLithium.js +1 -4
  111. package/sensor_classes/VictronSolarCharger.js +1 -3
  112. package/sensor_classes/VictronVEBus.js +1 -4
  113. package/sensor_classes/XiaomiMiBeacon.js +5 -2
  114. package/sensor_classes/iBeaconSensor.js +40 -0
  115. package/src/components/PluginConfigurationPanel.js +53 -17
  116. package/Screenshot 2025-06-12 at 9.33.57/342/200/257AM.png +0 -0
  117. package/bt-sensors-plugin-sk copy.json +0 -170
  118. package/bt-sensors-plugin-sk.json.bak +0 -121
  119. package/diff.txt +0 -2860
  120. package/public/847.js +0 -1
  121. package/sensor_classes/IBeacon.js +0 -45
  122. package/vsl_patch_17_06_25.patch +0 -13
  123. /package/public/images/{Aranet2_HOME_F_900x900_90OVA5J.original.webp → Aranet2.webp} +0 -0
  124. /package/public/images/{Bank Manager All-in-onewc.webp → BankManager.webp} +0 -0
  125. /package/public/images/{Gobius_C.png → GobiusCTankMeter.png} +0 -0
  126. /package/public/images/{Victron-SmartShunt.jpg → VictronSmartShunt.jpg} +0 -0
  127. /package/public/images/{smartsolarMPPT7515.png → VictronSmartSolarMPPT7515.png} +0 -0
package/BTSensor.js CHANGED
@@ -2,16 +2,19 @@ const { Variant } = require('dbus-next');
2
2
  const { log } = require('node:console');
3
3
  const EventEmitter = require('node:events');
4
4
  const AutoQueue = require("./Queue.js")
5
+ const DistanceManager = require("./DistanceManager")
5
6
 
6
7
  /**
7
8
  * @author Andrew Gerngross <oh.that.andy@gmail.com>
8
9
  */
9
10
 
11
+
10
12
  /**
11
13
  * {@link module:node-ble}
12
14
  */
13
15
 
14
16
  const BTCompanies = require('./bt_co.json');
17
+
15
18
  const connectQueue = new AutoQueue()
16
19
 
17
20
  /**
@@ -54,38 +57,8 @@ function signalQualityPercentQuad(rssi, perfect_rssi=-20, worst_rssi=-85) {
54
57
  }
55
58
  return Math.ceil(signal_quality);
56
59
  }
57
- function preparePath(obj, str) {
58
- const regex = /\{([^}]+)\}/g;
59
- let match;
60
- let resultString = "";
61
- let lastIndex = 0;
62
-
63
- while ((match = regex.exec(str)) !== null) {
64
- const fullMatch = match[0];
65
- const keyToAccess = match[1].trim();
66
-
67
- // Append the text before the current curly braces
68
- resultString += str.substring(lastIndex, match.index);
69
- lastIndex = regex.lastIndex;
70
-
71
- try {
72
- let evalResult = obj[keyToAccess];
73
- if (typeof evalResult === 'function'){
74
- evalResult= evalResult.call(obj)
75
- }
76
60
 
77
- resultString += evalResult !== undefined ? evalResult.replace(/\s+/g,'_') : `${keyToAccess}_value_undefined`;
78
- } catch (error) {
79
- console.error(`Error accessing key '${keyToAccess}':`, error);
80
- resultString += fullMatch; // Keep the original curly braces on error
81
- }
82
- }
83
-
84
- // Append any remaining text after the last curly braces
85
- resultString += str.substring(lastIndex);
86
-
87
- return resultString || str; // Return original string if no replacements were made
88
- }
61
+
89
62
 
90
63
  /**
91
64
  * @classdesc Class that all sensor classes should inherit from. Sensor subclasses
@@ -102,13 +75,17 @@ function preparePath(obj, str) {
102
75
  class BTSensor extends EventEmitter {
103
76
 
104
77
  static DEFAULTS = require('./plugin_defaults.json');
78
+ static DistanceManagerSingleton= new DistanceManager({DISTANCE_FIND_LAST_FEW_SAMPLE_TIME_FRAME_MILLIS:60000}) //should be a singleton
79
+
80
+ static IsRoaming = false;
105
81
 
106
82
  static SensorDomains={
107
83
  unknown: { name: "unknown", description: "Unknown sensor domain "},
108
84
  environmental: { name: "environmental", description: "Sensors that measure environmental conditions - air temperature, humidity etc."},
109
85
  electrical: { name: "electrical", description: "Electrical sensor - chargers, batteries, inverters etc."},
110
86
  propulsion: { name: "propulsion", description: "Sensors that measure engine state"},
111
- tanks: { name: "tanks", description: "Sensors that measure level in tanks (gas, propane, water etc.) "}
87
+ tanks: { name: "tanks", description: "Sensors that measure level in tanks (gas, propane, water etc.) "},
88
+ beacons: { name: "beacons", description: "iBeacon/Eddystone sensor tags"}
112
89
  }
113
90
  static Domain = this.SensorDomains.unknown
114
91
  /**
@@ -254,6 +231,8 @@ class BTSensor extends EventEmitter {
254
231
  throw new Error("BTSensor is an abstract class. ::identify must be implemented by the subclass")
255
232
  }
256
233
 
234
+ static DisplayName() { return `${this.name} (${this.Domain.name}) `}
235
+
257
236
  /**
258
237
  * getManufacturerID is used to help ID the manufacturer of a device
259
238
  *
@@ -634,27 +613,49 @@ class BTSensor extends EventEmitter {
634
613
  return this.currentProperties.Address
635
614
  }
636
615
 
637
- getImage(){
638
- return "bluetooth-logo.png"
616
+ static ImageFile = "bluetooth-logo.png"
617
+ static Description = "Bluetooth device"
618
+ static Manufacturer = "Unknown"
619
+ static ImageSrc = "../bt-sensors-plugin-sk/images/"
620
+
621
+ getImageFile(){
622
+ return this.constructor.ImageFile
623
+ }
624
+ getImageSrc(){
625
+ return this.constructor.ImageSrc
639
626
  }
627
+ static getImageHTML() {
628
+ return `<img src="${this.ImageSrc}${this.ImageFile}" style="float: left; margin-right: 10px;" height="150" object-fit="cover" ></img>`
629
+ }
630
+
640
631
  getImageHTML(){
641
- return `<img src="../bt-sensors-plugin-sk/images/${this.getImage()}" height="150" object-fit="cover" ></img>`
632
+ return `<img src="${this.getImageSrc()}${this.getImageFile()}" style="float: left; margin-right: 10px;" height="150" object-fit="cover" ></img>`
642
633
  }
643
634
 
644
635
  getTextDescription(){
645
636
  return `${this.getName()} from ${this.getManufacturer()}`
646
637
  }
647
638
 
639
+ static getTextDescription(){
640
+ return `${this.name} from ${this.Manufacturer}`
641
+ }
642
+
648
643
  getDescription(){
649
644
  return `<div>${this.getImageHTML()} ${this.getTextDescription()} </div>`
645
+ }
650
646
 
647
+ static getDescription(){
648
+ return `<div>${this.getImageHTML()} ${this.getTextDescription()} </div>`
651
649
  }
650
+
652
651
  getName(){
653
652
  const name = this?.name??this.currentProperties.Name
654
653
  return name?name:"Unknown"
655
654
 
656
655
  }
657
656
  macAndName(){
657
+ if (this.getMacAddress()==null)
658
+ debugger
658
659
  return `${this.getName().replaceAll(':', '-').replaceAll(" ","_")}-${this.getMacAddress().replaceAll(':', '-')}`
659
660
  }
660
661
  getNameAndAddress(){
@@ -892,7 +893,7 @@ class BTSensor extends EventEmitter {
892
893
  this.app.handleMessage(id,
893
894
  {
894
895
  updates:
895
- [{ meta: [{path: preparePath(this, path), value: { units: pathMeta?.unit }}]}]
896
+ [{ meta: [{path: this.preparePath(path), value: { units: pathMeta?.unit }}]}]
896
897
  })
897
898
  }
898
899
  })
@@ -904,7 +905,7 @@ class BTSensor extends EventEmitter {
904
905
  const pathMeta=this.getPath(tag)
905
906
  const path = deviceConfig.paths[tag];
906
907
  if (!(path === undefined)) {
907
- let preparedPath = preparePath(this, path)
908
+ let preparedPath = this.preparePath(path)
908
909
  this.on(tag, (val)=>{
909
910
  if (pathMeta.notify){
910
911
  this.app.notify(tag, val, id )
@@ -923,8 +924,43 @@ class BTSensor extends EventEmitter {
923
924
  }
924
925
 
925
926
  prepareConfig(config){
926
- config.params.sensorClass=this.constructor.name
927
+ if (!config.params.sensorClass)
928
+ config.params.sensorClass=this.constructor.name
929
+ }
930
+
931
+ preparePath(str) {
932
+ const regex = /\{([^}]+)\}/g;
933
+ let match;
934
+ let resultString = "";
935
+ let lastIndex = 0;
936
+
937
+ while ((match = regex.exec(str)) !== null) {
938
+ const fullMatch = match[0];
939
+ const keyToAccess = match[1].trim();
940
+
941
+ // Append the text before the current curly braces
942
+ resultString += str.substring(lastIndex, match.index);
943
+ lastIndex = regex.lastIndex;
944
+
945
+ try {
946
+ let evalResult = this[keyToAccess];
947
+ if (typeof evalResult === 'function'){
948
+ evalResult= evalResult.call(this)
949
+ }
950
+
951
+ resultString += evalResult !== undefined ? evalResult.replace(/\s+/g,'_') : `${keyToAccess}_value_undefined`;
952
+ } catch (error) {
953
+ console.error(`Error accessing key '${keyToAccess}':`, error);
954
+ resultString += fullMatch; // Keep the original curly braces on error
955
+ }
927
956
  }
957
+
958
+ // Append any remaining text after the last curly braces
959
+ resultString += str.substring(lastIndex);
960
+
961
+ return resultString || str; // Return original string if no replacements were made
962
+ }
963
+
928
964
 
929
965
  }
930
966
 
@@ -0,0 +1,249 @@
1
+ const { LRUCache } = require('lru-cache')
2
+
3
+
4
+ class DistanceManager {
5
+ static METHOD_AVG = 1;
6
+ static METHOD_WEIGHTED_AVG = 2;
7
+ static METHOD_LAST_FEW_SAMPLES = 3;
8
+
9
+ Constant = {
10
+ DISTANCE_FIND_LAST_FEW_SAMPLE_TIME_FRAME_MILLIS: 5000, // Example value, adjust as needed
11
+ DISTANCE_FIND_TIME_FRAME_MILLIS: 10000, // Example value, adjust as needed
12
+ LAST_FEW_SAMPLE_COUNT: 5 // Example value, adjust as needed
13
+ };
14
+
15
+
16
+ #beaconRssiSampleMap = new LRUCache({ttl:1000*60*5, ttlAutopurge: true}); // Using LRUCache with a ttl of 5m for beaconRssiSampleMap
17
+
18
+ #timeFormatter = new Intl.DateTimeFormat('en-US', {
19
+ hour: '2-digit',
20
+ minute: '2-digit',
21
+ second: '2-digit',
22
+ hour12: false // Use 24-hour format
23
+ });
24
+
25
+ constructor(constants) {
26
+ Object.assign(this.Constant,constants)
27
+ // Constructor is empty as initialization is done with property declarations
28
+ }
29
+
30
+ addSample(macAddress, rssi) {
31
+ // In Node.js (JavaScript), maps and objects are not inherently synchronized like
32
+ // Java's synchronized blocks. If this were a multi-threaded Node.js environment
33
+ // (e.g., Worker Threads), you'd need explicit locking mechanisms or message passing.
34
+ // For typical single-threaded Node.js, direct access is usually fine,
35
+ // but if concurrency is a concern, consider a mutex implementation.
36
+ let samples = this.#beaconRssiSampleMap.get(macAddress);
37
+ if (!samples) {
38
+ samples = new Map(); // Using Map instead of LinkedHashMap
39
+ this.#beaconRssiSampleMap.set(macAddress, samples);
40
+ }
41
+ samples.set(Date.now(), rssi);
42
+ }
43
+
44
+ getDistance(macAddress, txPower, method, debugLog) {
45
+ const samples = this.#beaconRssiSampleMap.get(macAddress);
46
+ if (!samples) {
47
+ return 0;
48
+ }
49
+
50
+ // Create a new Map to avoid modifying the original during filtering/processing
51
+ const currentSamples = new Map(samples);
52
+
53
+ const fromTimestamp = Date.now() - (method === DistanceManager.METHOD_LAST_FEW_SAMPLES ?
54
+ this.Constant.DISTANCE_FIND_LAST_FEW_SAMPLE_TIME_FRAME_MILLIS : this.Constant.DISTANCE_FIND_TIME_FRAME_MILLIS);
55
+ const toTimestamp = Date.now();
56
+
57
+ const filteredRssi = this.#filterRssiSamplesWithinTimeFrame(currentSamples, fromTimestamp, toTimestamp);
58
+
59
+ const smoothRssi = this.#reduceNoiseFromRSSI(filteredRssi);
60
+
61
+ let rssi = 0;
62
+ let distance;
63
+
64
+ switch (method) {
65
+ case DistanceManager.METHOD_AVG:
66
+ rssi = this.#calculateAverage(smoothRssi);
67
+ break;
68
+ case DistanceManager.METHOD_WEIGHTED_AVG:
69
+ rssi = this.#calculateWeightedAverageOfRssiSamples(smoothRssi);
70
+ break;
71
+ case DistanceManager.METHOD_LAST_FEW_SAMPLES:
72
+ if (samples.size >= this.Constant.LAST_FEW_SAMPLE_COUNT) {
73
+ rssi = this.#calculateAverage(smoothRssi);
74
+ } else {
75
+ return -1;
76
+ }
77
+ break;
78
+ default:
79
+ // Handle unknown method or provide a default
80
+ console.warn(`Unknown method: ${method}`);
81
+ return 0;
82
+ }
83
+
84
+ distance = this.#calculateAccuracy(txPower, rssi);
85
+
86
+ if (debugLog) {
87
+ this.#logSamples(smoothRssi, fromTimestamp, toTimestamp, rssi, distance);
88
+ }
89
+ return distance;
90
+ }
91
+
92
+ #removeOutliers(filteredRssi, avgOutlierRssi, outlierConstant) {
93
+ const outlierRemoveRssi = new Map();
94
+
95
+ const minRssi = Math.floor(avgOutlierRssi) - outlierConstant;
96
+ const maxRssi = Math.floor(avgOutlierRssi) + outlierConstant;
97
+
98
+ for (const [timestamp, value] of filteredRssi.entries()) {
99
+ if (value >= minRssi && value <= maxRssi) {
100
+ outlierRemoveRssi.set(timestamp, value);
101
+ }
102
+ }
103
+ return outlierRemoveRssi;
104
+ }
105
+
106
+ #reduceNoiseFromRSSI(filteredRssi) {
107
+ const smoothRssi = new Map();
108
+
109
+ const rssiEntries = Array.from(filteredRssi.entries());
110
+ const totalRssi = rssiEntries.length;
111
+
112
+ for (let i = 0; i < totalRssi; i++) {
113
+ const [currentTimestamp, currentRssi] = rssiEntries[i];
114
+ const nextRssi = rssiEntries[i + 1] ? rssiEntries[i + 1][1] : currentRssi; // If no next, use current
115
+
116
+ const avgRssi = Math.floor((currentRssi + nextRssi) / 2);
117
+ smoothRssi.set(currentTimestamp, avgRssi);
118
+ }
119
+ return smoothRssi;
120
+ }
121
+
122
+ #filterRssiSamplesWithinTimeFrame(rssiSamples, fromTimestamp, toTimestamp) {
123
+ const filteredSamples = new Map();
124
+ for (const [timestamp, rssi] of rssiSamples.entries()) {
125
+ if (fromTimestamp === 0) {
126
+ if (timestamp <= toTimestamp) {
127
+ filteredSamples.set(timestamp, rssi);
128
+ }
129
+ } else if (timestamp > fromTimestamp && timestamp <= toTimestamp) {
130
+ filteredSamples.set(timestamp, rssi);
131
+ }
132
+ }
133
+ return filteredSamples;
134
+ }
135
+
136
+ #calculateAverage(filteredRssi) {
137
+ let sum = 0;
138
+ if (filteredRssi.size === 0) {
139
+ return 0; // Avoid division by zero
140
+ }
141
+ for (const rssi of filteredRssi.values()) {
142
+ sum += rssi;
143
+ }
144
+ return sum / filteredRssi.size;
145
+ }
146
+
147
+ #calculateWeightedAverageOfRssiSamples(filteredRssi) {
148
+ // 1. Find count
149
+ const uniqueRssiCountMap = new Map();
150
+ for (const rssi of filteredRssi.values()) {
151
+ uniqueRssiCountMap.set(rssi, (uniqueRssiCountMap.get(rssi) || 0) + 1);
152
+ }
153
+
154
+ // 2. Find weight of each rssi
155
+ const uniqueRssiWeightMap = new Map();
156
+ const totalSamples = filteredRssi.size;
157
+ if (totalSamples === 0) {
158
+ return 0; // Avoid division by zero
159
+ }
160
+ for (const [rssi, count] of uniqueRssiCountMap.entries()) {
161
+ const weight = count / totalSamples;
162
+ uniqueRssiWeightMap.set(rssi, weight);
163
+ }
164
+
165
+ // 3. Calculate weighted average
166
+ let sum = 0;
167
+ for (const [rssi, weight] of uniqueRssiWeightMap.entries()) {
168
+ sum += rssi * weight;
169
+ }
170
+ return sum;
171
+ }
172
+
173
+ #calculateAccuracy(txPower, rssi) {
174
+ if (rssi === 0) {
175
+ return -1.0;
176
+ }
177
+
178
+ const ratio = rssi * 1.0 / txPower;
179
+ if (ratio < 1.0) {
180
+ return Math.pow(ratio, 10);
181
+ } else {
182
+ return ((0.42093) * Math.pow(ratio, 6.9476)) + 0.54992; // Nexus 5 formula
183
+ }
184
+ }
185
+
186
+ #logSamples(samples, fromTimestamp, toTimestamp, rssiWeightedAvg, distance) {
187
+ const object = {};
188
+ const array = [];
189
+ try {
190
+ for (const [timestamp, rssi] of samples.entries()) {
191
+ const jsonSample = {
192
+ rssi: rssi,
193
+ timestamp: this.#getTimeString(timestamp)
194
+ };
195
+ array.push(jsonSample);
196
+ }
197
+ object.calc_rssi = rssiWeightedAvg;
198
+ object.distance = distance;
199
+ object.start_time = this.#getTimeString(fromTimestamp);
200
+ object.end_time = this.#getTimeString(toTimestamp);
201
+ object.samples = array;
202
+ // In Node.js, 'console.log' is used instead of 'Log.d'
203
+ //console.log("SampleData", JSON.stringify(object, null, 2)); // Prettify output
204
+ } catch (error) {
205
+ console.error("Error logging samples:", error); // Use console.error for errors
206
+ }
207
+ }
208
+
209
+ #getTimeString(millis) {
210
+ const d = new Date(millis);
211
+ return this.#timeFormatter.format(d);
212
+ }
213
+ }
214
+
215
+ // Example Usage (for demonstration purposes)
216
+ /*
217
+ // You might need to define or import Constant with your specific values
218
+ const Constant = {
219
+ DISTANCE_FIND_LAST_FEW_SAMPLE_TIME_FRAME_MILLIS: 5000,
220
+ DISTANCE_FIND_TIME_FRAME_MILLIS: 10000,
221
+ LAST_FEW_SAMPLE_COUNT: 5
222
+ };
223
+
224
+ const distanceManager = new DistanceManager();
225
+
226
+ // Simulate adding some samples
227
+ distanceManager.addSample("AA:BB:CC:DD:EE:FF", -70);
228
+ setTimeout(() => distanceManager.addSample("AA:BB:CC:DD:EE:FF", -72), 500);
229
+ setTimeout(() => distanceManager.addSample("AA:BB:CC:DD:EE:FF", -68), 1000);
230
+ setTimeout(() => distanceManager.addSample("AA:BB:CC:DD:EE:FF", -75), 1500);
231
+ setTimeout(() => distanceManager.addSample("AA:BB:CC:DD:EE:FF", -71), 2000);
232
+ setTimeout(() => distanceManager.addSample("AA:BB:CC:DD:EE:FF", -69), 2500);
233
+ setTimeout(() => distanceManager.addSample("AA:BB:CC:DD:EE:FF", -73), 3000);
234
+
235
+ // Get distance after some time
236
+ setTimeout(() => {
237
+ const txPower = -59; // Example TxPower
238
+ let distance = distanceManager.getDistance("AA:BB:CC:DD:EE:FF", txPower, DistanceManager.METHOD_AVG, true);
239
+ console.log(`Calculated Distance (AVG): ${distance.toFixed(2)} meters`);
240
+
241
+ distance = distanceManager.getDistance("AA:BB:CC:DD:EE:FF", txPower, DistanceManager.METHOD_WEIGHTED_AVG, true);
242
+ console.log(`Calculated Distance (WEIGHTED_AVG): ${distance.toFixed(2)} meters`);
243
+
244
+ distance = distanceManager.getDistance("AA:BB:CC:DD:EE:FF", txPower, DistanceManager.METHOD_LAST_FEW_SAMPLES, true);
245
+ console.log(`Calculated Distance (LAST_FEW_SAMPLES): ${distance.toFixed(2)} meters`);
246
+
247
+ }, 4000);
248
+ */
249
+ module.exports=DistanceManager
package/Mixin.js ADDED
@@ -0,0 +1,19 @@
1
+ function bindThisToThat(obj1, obj2){
2
+ for (let method of getInstanceMethodNames(obj1)) {
3
+ obj1[method]=obj1[method].bind(obj2)
4
+ }
5
+ }
6
+
7
+ function getInstanceMethodNames (obj) {
8
+ return Object
9
+ .getOwnPropertyNames (Object.getPrototypeOf (obj))
10
+ .filter(name => (name !== 'constructor' && typeof obj[name] === 'function'));
11
+ }
12
+
13
+ class Mixin{
14
+ constructor(obj){
15
+ bindThisToThat(this, obj)
16
+ }
17
+ }
18
+
19
+ module.exports=Mixin
@@ -0,0 +1,46 @@
1
+
2
+ const { toPathSchema } = require('@rjsf/utils');
3
+ const EventEmitter = require('node:events');
4
+
5
+ class OutOfRangeDevice extends EventEmitter{
6
+ constructor(adapter, config){
7
+ super()
8
+ this.helper=new EventEmitter()
9
+ this.helper._prepare=()=>{}
10
+ this.helper.callMethod=()=>{}
11
+ this.helper.removeListeners=(()=>{this.removeAllListeners()}).bind(this.helper)
12
+ let props={Address: {value:config.mac_address}}
13
+ this._propsProxy={}
14
+ this._propsProxy.GetAll=()=>{
15
+ return props
16
+ }
17
+ this._propsProxy.Get=(key)=>{
18
+ if(key==="Address")
19
+ return props.Address
20
+ else return null
21
+ }
22
+
23
+ this.intervalID=setInterval(
24
+ ()=>{
25
+ adapter.waitDevice(config.mac_address,(config?.discoveryTimeout??30)*1000)
26
+ .then(async (device)=> {
27
+ this.emit("deviceFound", device)
28
+ clearInterval(this.intervalID)
29
+ this.intervalID=undefined
30
+ }).catch((e)=>{
31
+
32
+ })
33
+ },
34
+ (config?.discoveryTimeout??30)*1000
35
+ )
36
+
37
+ }
38
+ connect(){}
39
+ disconnect(){}
40
+ stopListening(){
41
+ this.removeAllListeners()
42
+ if (this.intervalID)
43
+ clearInterval(this.intervalID)
44
+ }
45
+ }
46
+ module.exports=OutOfRangeDevice
package/README.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## WHAT'S NEW
4
4
 
5
+ # Version 1.2.6-beta
6
+
7
+ - Mopeka examples not defaults in config
8
+ - Fix so no plugin restart required after selecting sensor class for an unidentified device
9
+ - FeasyCom BP108B support -- iBeacon/Eddystone protocols
10
+ - Ective, Topband, Skanbatt etc LiFePo4 BMS support (ective.de)
11
+ - Fixed Junctek chargeDirection at start (amps will not reported until chargeDirection is known)
12
+
5
13
  # Version 1.2.5-1
6
14
 
7
15
  - Reverted change from 1.2.5 to path's source field
@@ -95,6 +103,8 @@ It's pretty easy to write and deploy your own sensor class for any currently uns
95
103
  |[Junctek](https://www.junteks.com)|[Junctek BMS](https://www.junteks.com/pages/product/index) |
96
104
  |[Remoran](https://remoran.eu)| [Remoran Wave.3](https://remoran.eu/wave.html)|
97
105
  |[AC DC Systems](https://marinedcac.com) | [Bank Manager] hybrid (Pb and Li) charger(https://marinedcac.com/pages/bankmanager)|
106
+ |[Ective](https://ective.de/)| Also Topband(?), Skanbatt and others |
107
+
98
108
 
99
109
  ### Environmental
100
110
  | Manufacturer | Devices |
@@ -122,7 +132,11 @@ It's pretty easy to write and deploy your own sensor class for any currently uns
122
132
  |--------------|----------|
123
133
  | [Mercury](https://www.mercurymarine.com)| [Mercury Smartcraft](https://www.mercurymarine.com/us/en/gauges-and-controls/displays/smartcraft-connect) connect engine sensor|
124
134
 
135
+ ### Beacons
125
136
 
137
+ | Manufacturer | Devices |
138
+ |--------------|----------|
139
+ |[FeasyCom](https://www.feasycom.com/)| [BP108B](https://www.feasycom.com/product/fsc-bp108b/) |
126
140
 
127
141
 
128
142
  ## WHO IT'S FOR
package/classLoader.js CHANGED
@@ -27,8 +27,13 @@ const semver = require('semver')
27
27
  const modules = defaultExport.modulesWithKeyword(app.config, "signalk-bt-sensor-class")
28
28
  modules.forEach((module)=>{
29
29
  module.metadata.classFiles.forEach((classFile)=>{
30
- const cls = require(module.location+module.module+"/"+classFile);
31
- classMap.set(cls.name, cls);
30
+ try{
31
+ const cls = require(module.location+module.module+"/"+classFile);
32
+ classMap.set(cls.name, cls);
33
+ } catch (e) {
34
+ console.log(`Unable to load class (${cls.name}): ${e.message}`)
35
+ console.log(e)
36
+ }
32
37
  })
33
38
  })
34
39
  classMap.get('UNKNOWN').classMap=new Map([...classMap].sort().filter(([k, v]) => !v.isSystem )) // share the classMap with Unknown for configuration purposes
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/expect -f
2
+
3
+ set device [lindex $argv 0];
4
+ set uuid [lindex $argv 1];
5
+ set notifies [lindex $argv 2];
6
+ set timeout 60
7
+ spawn bluetoothctl
8
+ send -- "scan on\r"
9
+ expect $device
10
+ send -- "connect $device\r"
11
+ expect "ServicesResolved: yes"
12
+ send -- "scan off\r"
13
+ expect "Discovering: no"
14
+ send -- "menu gatt\r"
15
+ expect "Menu gatt:"
16
+ send -- "select-attribute $uuid\r"
17
+ expect "service"
18
+ send -- "acquire-notify\r"
19
+ expect "NotifyAcquired: yes"
20
+ for {set i 0} {$i < $notifies} {incr i} {
21
+ expect "Notification:"
22
+ }
23
+ send -- "back\r"
24
+ expect "Menu main:"
25
+ send -- "disconnect\r"
26
+ expect "disconnected"