bt-sensors-plugin-sk 1.2.5 → 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.
- package/BTSensor.js +77 -38
- package/DistanceManager.js +249 -0
- package/Mixin.js +19 -0
- package/OutOfRangeDevice.js +46 -0
- package/README.md +20 -6
- package/classLoader.js +7 -2
- package/connectUUID.exp +26 -0
- package/index.js +54 -11
- package/package.json +8 -6
- package/public/159.js +1 -1
- package/public/540.js +1 -1
- package/public/681.js +3 -9
- package/public/681.js.LICENSE.txt +2 -0
- package/public/764.js +1 -0
- package/public/images/ATC.jpeg +0 -0
- package/public/images/Aranet4.webp +0 -0
- package/public/images/BP108B.webp +0 -0
- package/public/images/GoveeH5074.jpg +0 -0
- package/public/images/GoveeH5075.webp +0 -0
- package/public/images/GoveeH510x.jpg +0 -0
- package/public/images/InkbirdTH3.webp +0 -0
- package/public/images/JBDBMS.webp +0 -0
- package/public/images/Junctek.webp +0 -0
- package/public/images/KilovaultHLXPlus.jpg +0 -0
- package/public/images/LancolVoltageMeter.webp +0 -0
- package/public/images/LiTimeLiFePo4Battery.avif +0 -0
- package/public/images/MercurySmartcraft.jpg +0 -0
- package/public/images/MopekaTankSensor.jpg +0 -0
- package/public/images/RemoranWave3.jpeg +0 -0
- package/public/images/RenogyInverter.jpg +0 -0
- package/public/images/RenogyRoverClient.jpg +0 -0
- package/public/images/RenogySmartLiFePo4Battery.webp +0 -0
- package/public/images/RuuviTag.jpg +0 -0
- package/public/images/ShellyBLUHT.webp +0 -0
- package/public/images/ShellyBLUMotion.webp +0 -0
- package/public/images/ShellyBluDoorWindow.webp +0 -0
- package/public/images/Skanbatt.jpg +0 -0
- package/public/images/SmartBatteryProtect.webp +0 -0
- package/public/images/SmartBatterySense.webp +0 -0
- package/public/images/SwitchBotMeterPlus.webp +0 -0
- package/public/images/SwitchBotTH.webp +0 -0
- package/public/images/TopbandBattery.webp +0 -0
- package/public/images/Ultrasonic.jpg +0 -0
- package/public/images/VictronBlueSmartACCharger.jpg +0 -0
- package/public/images/VictronBlueSolarMPPT.jpeg +0 -0
- package/public/images/VictronCerboGX.webp +0 -0
- package/public/images/VictronInverterRS.webp +0 -0
- package/public/images/VictronLynxSmartBMS.webp +0 -0
- package/public/images/VictronMultiPlus-II.webp +0 -0
- package/public/images/VictronOrionTrIsolated.webp +0 -0
- package/public/images/VictronOrionTrNonIsolated.webp +0 -0
- package/public/images/VictronPhoenixInverter.webp +0 -0
- package/public/images/VictronPhoenixSmart1600.webp +0 -0
- package/public/images/VictronSmartBatteryProtect.jpg +0 -0
- package/public/images/VictronSmartIP43.webp +0 -0
- package/public/images/VictronSmartLithiumBattery.jpg +0 -0
- package/public/images/VictronSmartSolarMPPT.webp +0 -0
- package/public/images/VictronVEBus.webp +0 -0
- package/public/images/iBeacon.jpg +0 -0
- package/public/main.js +1 -1
- package/public/remoteEntry.js +1 -1
- package/readUUID.exp +23 -0
- package/sensor_classes/ATC.js +3 -2
- package/sensor_classes/Aranet2.js +3 -1
- package/sensor_classes/Aranet4.js +1 -2
- package/sensor_classes/BankManager.js +1 -1
- package/sensor_classes/Beacon/AbstractBeaconMixin.js +85 -0
- package/sensor_classes/Beacon/Eddystone.js +77 -0
- package/sensor_classes/Beacon/iBeacon.js +58 -0
- package/sensor_classes/EctiveBMS.js +270 -0
- package/sensor_classes/FeasyComBeacon.js +68 -0
- package/sensor_classes/GobiusCTankMeter.js +4 -3
- package/sensor_classes/GoveeH5074.js +2 -0
- package/sensor_classes/GoveeH5075.js +2 -0
- package/sensor_classes/GoveeH510x.js +1 -0
- package/sensor_classes/Inkbird.js +1 -0
- package/sensor_classes/JBDBMS.js +1 -0
- package/sensor_classes/Junctek.js +14 -6
- package/sensor_classes/KilovaultHLXPlus.js +1 -0
- package/sensor_classes/LancolVoltageMeter.js +2 -0
- package/sensor_classes/MercurySmartcraft.js +1 -0
- package/sensor_classes/MopekaTankSensor.js +7 -204
- package/sensor_classes/RemoranWave3.js +2 -0
- package/sensor_classes/Renogy/RenogySensor.js +1 -0
- package/sensor_classes/RenogyBattery.js +3 -4
- package/sensor_classes/RenogyInverter.js +3 -6
- package/sensor_classes/RenogyRoverClient.js +3 -0
- package/sensor_classes/RuuviTag.js +11 -8
- package/sensor_classes/ShellySBDW002C.js +3 -1
- package/sensor_classes/ShellySBHT003C.js +7 -0
- package/sensor_classes/ShellySBMO003Z.js +3 -2
- package/sensor_classes/ShenzhenLiOnBMS.js +4 -0
- package/sensor_classes/SwitchBotMeterPlus.js +1 -1
- package/sensor_classes/SwitchBotTH.js +2 -1
- package/sensor_classes/UNKNOWN.js +2 -1
- package/sensor_classes/UltrasonicWindMeter.js +3 -0
- package/sensor_classes/Victron/VictronConstants.js +2 -0
- package/sensor_classes/Victron/VictronIdentifier.js +24 -0
- package/sensor_classes/Victron/VictronSensor.js +59 -49
- package/sensor_classes/VictronACCharger.js +1 -6
- package/sensor_classes/VictronBatteryMonitor.js +37 -26
- package/sensor_classes/VictronDCDCConverter.js +1 -4
- package/sensor_classes/VictronDCEnergyMeter.js +1 -4
- package/sensor_classes/VictronGXDevice.js +1 -4
- package/sensor_classes/VictronInverter.js +2 -3
- package/sensor_classes/VictronInverterRS.js +2 -4
- package/sensor_classes/VictronLynxSmartBMS.js +1 -4
- package/sensor_classes/VictronOrionXS.js +1 -3
- package/sensor_classes/VictronSmartBatteryProtect.js +1 -4
- package/sensor_classes/VictronSmartLithium.js +1 -4
- package/sensor_classes/VictronSolarCharger.js +1 -3
- package/sensor_classes/VictronVEBus.js +1 -4
- package/sensor_classes/XiaomiMiBeacon.js +5 -2
- package/sensor_classes/iBeaconSensor.js +40 -0
- package/src/components/PluginConfigurationPanel.js +134 -173
- package/Screenshot 2025-06-12 at 9.33.57/342/200/257AM.png +0 -0
- package/bt-sensors-plugin-sk copy.json +0 -170
- package/bt-sensors-plugin-sk.json.bak +0 -121
- package/public/847.js +0 -1
- package/sensor_classes/IBeacon.js +0 -45
- package/vsl_patch_17_06_25.patch +0 -13
- /package/public/images/{Aranet2_HOME_F_900x900_90OVA5J.original.webp → Aranet2.webp} +0 -0
- /package/public/images/{Bank Manager All-in-onewc.webp → BankManager.webp} +0 -0
- /package/public/images/{Gobius_C.png → GobiusCTankMeter.png} +0 -0
- /package/public/images/{Victron-SmartShunt.jpg → VictronSmartShunt.jpg} +0 -0
- /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
|
-
|
|
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
|
-
|
|
638
|
-
|
|
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
|
|
626
|
+
}
|
|
627
|
+
static getImageHTML() {
|
|
628
|
+
return `<img src="${this.ImageSrc}${this.ImageFile}" style="float: left; margin-right: 10px;" height="150" object-fit="cover" ></img>`
|
|
639
629
|
}
|
|
630
|
+
|
|
640
631
|
getImageHTML(){
|
|
641
|
-
return `<img src="
|
|
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,19 +893,19 @@ class BTSensor extends EventEmitter {
|
|
|
892
893
|
this.app.handleMessage(id,
|
|
893
894
|
{
|
|
894
895
|
updates:
|
|
895
|
-
[{ meta: [{path: preparePath(
|
|
896
|
+
[{ meta: [{path: this.preparePath(path), value: { units: pathMeta?.unit }}]}]
|
|
896
897
|
})
|
|
897
898
|
}
|
|
898
899
|
})
|
|
899
900
|
}
|
|
900
901
|
|
|
901
902
|
initPaths(deviceConfig, id){
|
|
902
|
-
const source =
|
|
903
|
+
const source = this.getName()
|
|
903
904
|
Object.keys(this.getPaths()).forEach((tag)=>{
|
|
904
905
|
const pathMeta=this.getPath(tag)
|
|
905
906
|
const path = deviceConfig.paths[tag];
|
|
906
907
|
if (!(path === undefined)) {
|
|
907
|
-
let preparedPath = preparePath(
|
|
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 )
|
|
@@ -922,6 +923,44 @@ class BTSensor extends EventEmitter {
|
|
|
922
923
|
return (Date.now()-this?._lastContact??Date.now())/1000
|
|
923
924
|
}
|
|
924
925
|
|
|
926
|
+
prepareConfig(config){
|
|
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
|
+
}
|
|
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
|
+
|
|
925
964
|
|
|
926
965
|
}
|
|
927
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,18 +2,26 @@
|
|
|
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
|
+
|
|
13
|
+
# Version 1.2.5-1
|
|
14
|
+
|
|
15
|
+
- Reverted change from 1.2.5 to path's source field
|
|
16
|
+
- Victron Sensor model ID and name improvements to constistency for VE Smart Networking enabled devices
|
|
17
|
+
- Improved initial startup responsiveness
|
|
18
|
+
|
|
5
19
|
# Version 1.2.5
|
|
6
20
|
|
|
7
21
|
- On initial startup, plugin saves default configuration. Fixing the "missing" configured devices after restart.
|
|
8
22
|
- Mopeka Tank Sensor configuration fix
|
|
9
23
|
- Added number of found devices in a domain in the configuration screen's domain tab
|
|
10
24
|
|
|
11
|
-
## IMPORTANT NOTE TO NEW USERS OF VERSIONS OLDER THAN 1.2.5
|
|
12
|
-
|
|
13
|
-
There's a known issue with saving the configuration after initial installation owing to current ServerAPI limitations.
|
|
14
|
-
|
|
15
|
-
Your device config after you've saved it will appear to be "missing" after restarting. It's in fact saved in the plugin config directory. All you have to do is Submit the main configuration then enable and optionally disable Debug. This, believe it or not, ensures that the config is marked as enabled. You should see your data now and upon restart.
|
|
16
|
-
|
|
17
25
|
# Version 1.2.4-4
|
|
18
26
|
|
|
19
27
|
Junctek support (tested)
|
|
@@ -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
|
-
|
|
31
|
-
|
|
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
|
package/connectUUID.exp
ADDED
|
@@ -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"
|