bt-sensors-plugin-sk 1.1.1 → 1.2.0-beta.0.0.10

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 (56) hide show
  1. package/BTSensor.js +304 -108
  2. package/Queue.js +48 -0
  3. package/README.md +36 -22
  4. package/classLoader.js +38 -0
  5. package/definitions.json +941 -0
  6. package/index.js +425 -375
  7. package/package.json +52 -6
  8. package/plugin_defaults.json +146 -0
  9. package/public/893.js +1 -1
  10. package/public/main.js +1 -100
  11. package/public/remoteEntry.js +1 -129
  12. package/sensor_classes/ATC.js +34 -22
  13. package/sensor_classes/Aranet/AranetSensor.js +4 -0
  14. package/sensor_classes/Aranet2.js +16 -15
  15. package/sensor_classes/Aranet4.js +23 -17
  16. package/sensor_classes/BlackListedDevice.js +4 -0
  17. package/sensor_classes/DEVELOPMENT.md +1 -6
  18. package/sensor_classes/GoveeH50xx.js +12 -12
  19. package/sensor_classes/GoveeH510x.js +7 -8
  20. package/sensor_classes/IBeacon.js +6 -10
  21. package/sensor_classes/Inkbird.js +8 -6
  22. package/sensor_classes/JBDBMS.js +33 -25
  23. package/sensor_classes/KilovaultHLXPlus.js +33 -16
  24. package/sensor_classes/LancolVoltageMeter.js +4 -3
  25. package/sensor_classes/MopekaTankSensor.js +31 -15
  26. package/sensor_classes/Renogy/RenogySensor.js +15 -6
  27. package/sensor_classes/RenogyBattery.js +15 -13
  28. package/sensor_classes/RenogyRoverClient.js +2 -3
  29. package/sensor_classes/RuuviTag.js +36 -27
  30. package/sensor_classes/ShellySBHT003C.js +19 -22
  31. package/sensor_classes/SwitchBotMeterPlus.js +10 -12
  32. package/sensor_classes/SwitchBotTH.js +13 -16
  33. package/sensor_classes/UNKNOWN.js +8 -4
  34. package/sensor_classes/UltrasonicWindMeter.js +13 -5
  35. package/sensor_classes/Victron/VictronSensor.js +6 -2
  36. package/sensor_classes/VictronACCharger.js +19 -8
  37. package/sensor_classes/VictronBatteryMonitor.js +49 -37
  38. package/sensor_classes/VictronDCDCConverter.js +14 -5
  39. package/sensor_classes/VictronDCEnergyMeter.js +27 -15
  40. package/sensor_classes/VictronGXDevice.js +2 -5
  41. package/sensor_classes/VictronInverter.js +19 -15
  42. package/sensor_classes/VictronInverterRS.js +19 -8
  43. package/sensor_classes/VictronLynxSmartBMS.js +15 -13
  44. package/sensor_classes/VictronOrionXS.js +16 -3
  45. package/sensor_classes/VictronSmartBatteryProtect.js +5 -6
  46. package/sensor_classes/VictronSmartLithium.js +18 -18
  47. package/sensor_classes/VictronSolarCharger.js +31 -18
  48. package/sensor_classes/VictronVEBus.js +2 -5
  49. package/sensor_classes/XiaomiMiBeacon.js +31 -21
  50. package/spec/electrical.json +688 -0
  51. package/spec/environment.json +401 -0
  52. package/spec/sensors.json +39 -0
  53. package/spec/tanks.json +115 -0
  54. package/src/components/PluginConfigurationPanel.js +393 -0
  55. package/src/index.js +0 -0
  56. package/webpack.config.js +71 -0
package/index.js CHANGED
@@ -1,41 +1,57 @@
1
- const fs = require('fs')
2
- const util = require('util')
3
- const path = require('path')
4
- const semver = require('semver')
5
1
  const packageInfo = require("./package.json")
6
2
 
7
3
  const {createBluetooth} = require('node-ble')
8
- const { Variant } = require('dbus-next')
4
+ const {Variant} = require('dbus-next')
9
5
  const {bluetooth, destroy} = createBluetooth()
10
6
 
11
7
  const BTSensor = require('./BTSensor.js')
12
8
  const BLACKLISTED = require('./sensor_classes/BlackListedDevice.js')
9
+ const { createChannel, createSession } = require("better-sse");
10
+ const { clearTimeout } = require('timers')
11
+ const loadClassMap = require('./classLoader.js')
13
12
 
14
13
  class MissingSensor {
15
14
 
16
15
 
17
16
  constructor(config){
18
- this.Metadatum = BTSensor.Metadatum
19
- this.addMetadatum=BTSensor.prototype.addMetadatum
20
- this.getPathMetadata = BTSensor.prototype.getPathMetadata
21
- this.getParamMetadata = BTSensor.prototype.getParamMetadata
17
+ this.config=config
18
+ this.addPath=BTSensor.prototype.addPath.bind(this)
19
+ this.addParameter=BTSensor.prototype.addParameter.bind(this)
20
+ this.addDefaultPath=BTSensor.prototype.addDefaultPath.bind(this)
21
+ this.addDefaultParam=BTSensor.prototype.addDefaultParam.bind(this)
22
+ this.getPath=BTSensor.prototype.getPath.bind(this)
22
23
 
23
- this.metadata= new Map()
24
+ this.getJSONSchema = BTSensor.prototype.getJSONSchema.bind(this)
25
+ this.initSchema = BTSensor.prototype.initSchema.bind(this)
26
+
27
+ this.initSchema()
24
28
  var keys = Object.keys(config?.paths??{})
25
- this.addMetadatum.bind(this)
29
+
26
30
  keys.forEach((key)=>{
27
- this.addMetadatum(key, config.paths[key]?.type??'string', config.paths[key].description )
31
+ this.addPath(key,
32
+ { type: config.paths[key]?.type??'string',
33
+ title: config.paths[key].title} )
28
34
  } )
29
35
  keys = Object.keys(config?.params??{})
30
36
  keys.forEach((key)=>{
31
- this.addMetadatum(key, config.params[key]?.type??'string', config.params[key].description ).isParam=true
37
+ this.addParameter(key,
38
+ { type: config.params[key]?.type??'string',
39
+ title: config.params[key].title
40
+ })
32
41
  this[key]=config.params[key]
33
42
  })
34
43
  this.mac_address = config.mac_address
35
44
 
36
45
  }
37
46
  hasGATT(){
38
- return false
47
+ return this.config.gattParams
48
+ }
49
+ initGATTConnection(){
50
+
51
+ }
52
+
53
+ getGATTDescription(){
54
+ return ""
39
55
  }
40
56
  getMetadata(){
41
57
  return this.metadata
@@ -47,409 +63,404 @@ class MissingSensor {
47
63
  return ""
48
64
  }
49
65
  getName(){
50
- return this?.name??"Unknown device"
66
+ return `${this?.name??"Unknown device"} (OUT OF RANGE)`
51
67
  }
52
68
  getDisplayName(){
53
- return `OUT OF RANGE DEVICE (${this.getName()} ${this.getMacAddress()})`
69
+ return `(${this.getName()} ${this.getMacAddress()})`
70
+ }
71
+ getRSSI(){
72
+ return NaN
54
73
  }
55
74
  stopListening(){}
56
75
  listen(){}
76
+ isActive(){
77
+ return false
78
+ }
79
+ elapsedTimeSinceLastContact(){
80
+ return NaN
81
+ }
82
+ getSignalStrength(){
83
+ return NaN
84
+ }
85
+
57
86
  }
58
87
  module.exports = function (app) {
59
- var adapterID = 'hci0'
60
-
61
-
62
88
  var deviceConfigs
63
89
  var starts=0
64
- var classMap
65
-
66
- var utilities_sk
67
90
 
68
91
  var plugin = {};
69
92
  plugin.id = 'bt-sensors-plugin-sk';
70
93
  plugin.name = 'BT Sensors plugin';
71
94
  plugin.description = 'Plugin to communicate with and update paths to BLE Sensors in Signalk';
72
-
73
-
74
95
 
75
- //Try and load utilities-sk NOTE: should be installed from App Store--
76
- //But there's a fail safe because I'm a reasonable man.
77
-
78
- utilities_sk = {
79
- loadClasses: function(dir, ext='.js')
80
- {
81
- const classMap = new Map()
82
- const classFiles = fs.readdirSync(dir)
83
- .filter(file => file.endsWith(ext));
84
-
85
- classFiles.forEach(file => {
86
- const filePath = path.join(dir, file);
87
- const cls = require(filePath);
88
- classMap.set(cls.name, cls);
89
- })
90
- return classMap
91
- }
92
- }
93
-
94
- function sleep(x) {
95
- return new Promise((resolve) => {
96
- setTimeout(() => {
97
- resolve(x);
98
- }, x);
99
- });
100
- }
101
- async function instantiateSensor(device,config){
102
- try{
103
- for (var [clsName, cls] of classMap) {
104
- if (clsName.startsWith("_")) continue
105
- const c = await cls.identify(device)
106
- if (c) {
107
- c.debug=app.debug
108
- const sensor = new c(device,config?.params, config?.gattParams)
109
- sensor.debug=app.debug
110
- await sensor.init()
111
- app.debug(`instantiated ${await BTSensor.getDeviceProp(device,"Address")}`)
112
-
113
- return sensor
114
- }
115
- }} catch(error){
116
- const msg = `Unable to instantiate ${await BTSensor.getDeviceProp(device,"Address")}: ${error.message} `
117
- app.debug(msg)
118
- app.debug(error)
119
- app.setPluginError(msg)
120
- }
121
- //if we're here ain't got no class for the device
122
- var sensor
123
- if (config.params?.sensorClass){
124
- const c = classMap.get(config.params.sensorClass)
125
- c.debug=app.debug
126
- sensor = new c(device,config?.params, config?.gattParams)
127
- sensor.debug=app.debug
128
- await sensor.init()
129
- } else{
130
- sensor = new (classMap.get('UNKNOWN'))(device)
131
- await sensor.init()
132
- }
133
- return sensor
134
- }
135
-
136
- function createPaths(config){
137
- config.sensor.getMetadata().forEach((metadatum, tag)=>{
138
- if ((!(metadatum?.isParam)??false)){ //param metadata is passed to the sensor at
139
- //create time through the constructor, and isn't a
140
- //a value you want to see in a path
141
-
142
- const path = config.paths[tag]
143
- if (!(path===undefined))
144
- app.handleMessage(plugin.id,
145
- {
146
- updates:
147
- [{ meta: [{path: path, value: { units: metadatum?.unit }}]}]
148
- })
149
- }
150
- })
151
- }
152
-
153
- function initPaths(deviceConfig){
154
- deviceConfig.sensor.getMetadata().forEach((metadatum, tag)=>{
155
- const path = deviceConfig.paths[tag];
156
- if (!(path === undefined))
157
- deviceConfig.sensor.on(tag, (val)=>{
158
- if (metadatum.notify){
159
- app.notify( tag, val, plugin.id )
160
- } else {
161
- updatePath(path,val)
162
- }
163
- })
164
- })
165
- }
166
- function updatePath(path, val){
167
- app.handleMessage(plugin.id, {updates: [ { values: [ {path: path, value: val }] } ] })
168
- }
169
-
170
- function loadClassMap() {
171
- const _classMap = utilities_sk.loadClasses(path.join(__dirname, 'sensor_classes'))
172
- classMap = new Map([..._classMap].filter(([k, v]) => !k.startsWith("_") ))
173
- const libPath = app.config.appPath +(
174
- semver.gt(app.config.version,"2.13.5")?"dist":"lib"
175
- )
176
- //+ app.config.version
177
- import(libPath+"/modules.js").then( (modulesjs)=>{
178
- const { default:defaultExport} = modulesjs
179
- const modules = defaultExport.modulesWithKeyword(app.config, "signalk-bt-sensor-class")
180
- modules.forEach((module)=>{
181
- module.metadata.classFiles.forEach((classFile)=>{
182
- const cls = require(module.location+module.module+"/"+classFile);
183
- classMap.set(cls.name, cls);
184
- })
185
- })
186
- classMap.get('UNKNOWN').classMap=new Map([...classMap].sort().filter(([k, v]) => !v.isSystem )) // share the classMap with Unknown for configuration purposes
187
- })
188
- }
189
-
190
96
  app.debug(`Loading plugin ${packageInfo.version}`)
191
97
 
192
98
  plugin.schema = {
193
99
  type: "object",
194
- description: "NOTE: \n 1) Plugin must be enabled to configure your sensors. \n"+
195
- "2) You will have to wait until the scanner has found your device before seeing your device's config fields and saving the configuration. \n"+
196
- "3) To refresh the list of available devices and their configurations, just open and close the config screen by clicking on the arrow symbol in the config's top bar. \n"+
197
- "4) If you submit and get errors it may be because the configured devices have not yet all been discovered.",
198
100
  required:["adapter","discoveryTimeout", "discoveryInterval"],
199
101
  properties: {
200
102
  adapter: {title: "Bluetooth adapter",
201
103
  type: "string", default: "hci0"},
202
104
  transport: {title: "Transport ",
203
- type: "string", enum: ["auto","le","bredr"], default: "le", enumNames:["Auto", "LE-Bluetooth Low Energy", "BR/EDR Bluetooth basic rate/enhanced data rate"]},
204
-
105
+ type: "string", enum: ["auto","le","bredr"], default: "le", enumNames:["Auto", "LE-Bluetooth Low Energy", "BR/EDR Bluetooth basic rate/enhanced data rate"]},
205
106
  discoveryTimeout: {title: "Default device discovery timeout (in seconds)",
206
107
  type: "integer", default: 30,
207
108
  minimum: 10,
208
109
  maximum: 3600
209
110
  },
210
-
211
111
  discoveryInterval: {title: "Scan for new devices interval (in seconds-- 0 for no new device scanning)",
212
112
  type: "integer",
213
113
  default: 10,
214
- minimum: 0,
215
- multipleOf: 10
114
+ minimum: 0
216
115
  },
217
- peripherals:
218
- { type: "array", title: "Sensors", items:{
219
- title: "", type: "object",
220
- required:["mac_address", "discoveryTimeout"],
221
- properties:{
222
- active: {title: "Active", type: "boolean", default: true },
223
- mac_address: {title: "Bluetooth Sensor", type: "string" },
224
- discoveryTimeout: {title: "Device discovery timeout (in seconds)",
225
- type: "integer", default:30,
226
- minimum: 10,
227
- maximum: 600
228
- }
229
- }
230
-
231
- }
232
- }
233
116
  }
234
117
  }
235
- const UI_SCHEMA =
236
- { "peripherals": {
237
- 'ui:disabled': !plugin.started
238
- }
239
- }
240
- plugin.uiSchema=UI_SCHEMA
241
-
242
- function updateSensorDisplayName(sensor){
243
- const mac_address = sensor.getMacAddress()
244
- const displayName = sensor.getDisplayName()
245
-
246
- const mac_addresses_list = plugin.schema.properties.peripherals.items.properties.
247
- mac_address.enum
248
- const mac_addresses_names = plugin.schema.properties.peripherals.items.properties.
249
- mac_address.enumNames
250
-
251
- var index = mac_addresses_list.indexOf(mac_address)
252
- if (index!=-1)
253
- mac_addresses_names[index]= displayName
254
- }
255
118
 
256
- function removeSensorFromList(sensor){
257
- const mac_addresses_list = plugin.schema.properties.peripherals.items.properties.mac_address.enum
258
- const mac_addresses_names = plugin.schema.properties.peripherals.items.properties.mac_address.enumNames
259
- const oneOf = plugin.schema.properties.peripherals.items.dependencies.mac_address.oneOf
260
- const mac_address = sensor.getMacAddress()
119
+
120
+ plugin.started=false
121
+
122
+ var discoveryIntervalID, progressID, progressTimeoutID, deviceHealthID
123
+ var adapter
124
+ const channel = createChannel()
125
+ const classMap = loadClassMap(app)
126
+ const sensorMap=new Map()
127
+
128
+
129
+ /* plugin.registerWithRouter = function(router) {
130
+ router.get('/sendPluginState', async (req, res) => {
261
131
 
262
- const i = mac_addresses_list.indexOf(mac_address)
263
- if (i<0) return // n'existe pas
132
+ res.status(200).json({
133
+ "state":(plugin.started?"started":"stopped")
134
+ })
135
+ });
136
+ router.get("/sse", async (req, res) => {
137
+ const session = await createSession(req, res);
138
+ channel.register(session)
139
+ });
140
+
141
+ }*/
142
+
143
+ plugin.start = async function (options, restartPlugin) {
144
+ plugin.started=true
145
+ var adapterID=options.adapter
146
+ var foundConfiguredDevices=0
264
147
 
265
- mac_addresses_list.splice(i,1)
266
- mac_addresses_names.splice(i,1)
267
- oneOf.splice(oneOf.findIndex((p)=>p.properties.mac_address.enum[0]==mac_address),1)
148
+ plugin.registerWithRouter = function(router) {
268
149
 
269
- }
270
- function addSensorToList(sensor){
271
- if (!plugin.schema.properties.peripherals.items.dependencies)
272
- plugin.schema.properties.peripherals.items.dependencies={mac_address:{oneOf:[]}}
150
+ router.post('/updateSensorData', async (req, res) => {
151
+ app.debug(req.body)
152
+ const i = deviceConfigs.findIndex((p)=>p.mac_address==req.body.mac_address)
153
+ if (i<0){
154
+ if (!options.peripherals){
155
+ if (!options.hasOwnProperty("peripherals"))
156
+ options.peripherals=[]
157
+
158
+ options.peripherals=[]
159
+ }
160
+ options.peripherals.push(req.body)
161
+ } else {
162
+ options.peripherals[i] = req.body
163
+ }
164
+ app.savePluginOptions(
165
+ options, async () => {
166
+ app.debug('Plugin options saved')
167
+ res.status(200).json({message: "Sensor updated"})
168
+ const sensor = sensorMap.get(req.body.mac_address)
169
+ if (sensor) {
170
+ removeSensorFromList(sensor)
171
+ if (sensor.isActive())
172
+ await sensor.stopListening()
173
+ initConfiguredDevice(req.body)
174
+ }
175
+
176
+ }
177
+ )
178
+
179
+ });
180
+ router.post('/removeSensorData', async (req, res) => {
181
+ app.debug(req.body)
182
+ const i = deviceConfigs.findIndex((p)=>p.mac_address==req.body.mac_address)
183
+ if (i>=0){
184
+ deviceConfigs.splice(i,1)
185
+ }
186
+ if (sensorMap.has(req.body.mac_address))
187
+ sensorMap.delete(req.body.mac_address)
188
+ app.savePluginOptions(
189
+ options, () => {
190
+ app.debug('Plugin options saved')
191
+ res.status(200).json({message: "Sensor updated"})
192
+ channel.broadcast({},"resetSensors")
193
+ }
194
+ )
195
+
196
+ });
197
+
198
+ router.post('/updateBaseData', async (req, res) => {
199
+
200
+ app.debug(req.body)
201
+ Object.assign(options,req.body)
202
+ app.savePluginOptions(
203
+ options, () => {
204
+ app.debug('Plugin options saved')
205
+ res.status(200).json({message: "Plugin updated"})
206
+ channel.broadcast({},"pluginRestarted")
207
+ restartPlugin(options)
208
+ }
209
+ )
210
+ });
273
211
 
274
- if(plugin.schema.properties.peripherals.items.properties.mac_address.enum==undefined) {
275
- plugin.schema.properties.peripherals.items.properties.mac_address.enum=[]
276
- plugin.schema.properties.peripherals.items.properties.mac_address.enumNames=[]
277
- }
278
- const mac_addresses_names = plugin.schema.properties.peripherals.items.properties.mac_address.enumNames
279
- const mac_addresses_list = plugin.schema.properties.peripherals.items.properties.mac_address.enum
280
- const mac_address = sensor.getMacAddress()
281
- const displayName = sensor.getDisplayName()
282
-
283
- var index = mac_addresses_list.indexOf(mac_address)
284
- if (index==-1)
285
- index = mac_addresses_list.push(mac_address)-1
286
- mac_addresses_names[index]= displayName
287
- var oneOf = {properties:{mac_address:{enum:[mac_address]}}}
288
212
 
289
- oneOf.properties.params={
290
- title:`Device parameters`,
291
- description: sensor.getDescription(),
292
- type:"object",
293
- properties:{}
294
- }
295
- sensor.getParamMetadata().forEach((metadatum,tag)=>{
296
- oneOf.properties.params.properties[tag]=metadatum.asJSONSchema()
297
- })
213
+ router.get('/getBaseData', (req, res) => {
214
+
215
+ res.status(200).json(
216
+ {
217
+ schema: plugin.schema,
218
+ data: {
219
+ adapter: options.adapter,
220
+ transport: options.transport,
221
+ discoveryTimeout: options.discoveryTimeout,
222
+ discoveryInterval: options.discoveryInterval
223
+ }
224
+ }
225
+ );
226
+ })
227
+ router.get('/getSensors', (req, res) => {
228
+ app.debug("Sending sensors")
229
+ const t = sensorsToJSON()
230
+ res.status(200).json(t)
231
+ });
232
+
233
+ router.get('/getProgress', (req, res) => {
234
+ app.debug("Sending progress")
235
+ const json = {"progress":foundConfiguredDevices/deviceConfigs.length, "maxTimeout": 1,
236
+ "deviceCount":foundConfiguredDevices,
237
+ "totalDevices": deviceConfigs.length}
238
+ res.status(200).json(json)
239
+
240
+ });
241
+
242
+ router.get('/getPluginState', async (req, res) => {
243
+
244
+ res.status(200).json({
245
+ "state":(plugin.started?"started":"stopped")
246
+ })
247
+ });
248
+ router.get("/sse", async (req, res) => {
249
+ const session = await createSession(req, res);
250
+ channel.register(session)
251
+ });
252
+
298
253
 
299
- if (sensor.hasGATT()){
254
+ };
300
255
 
301
- oneOf.properties.gattParams={
302
- title:`GATT Specific device parameters`,
303
- description: sensor.getGATTDescription(),
304
- type:"object",
305
- properties:{}
306
- }
307
- sensor.getGATTParamMetadata().forEach((metadatum,tag)=>{
308
- oneOf.properties.gattParams.properties[tag]=metadatum.asJSONSchema()
309
- })
256
+ function sensorsToJSON(){
257
+ return Array.from(
258
+ Array.from(sensorMap.values()).filter((s)=>!(s instanceof BLACKLISTED) ).map(
259
+ sensorToJSON
260
+ ))
310
261
  }
311
262
 
312
- oneOf.properties.paths={
313
- title:"Signalk Paths",
314
- description: `Signalk paths to be updated when ${sensor.getName()}'s values change`,
315
- type:"object",
316
- properties:{}
263
+ function getSensorInfo(sensor){
264
+
265
+ const etslc = sensor.elapsedTimeSinceLastContact()
266
+ return { mac: sensor.getMacAddress(),
267
+ name: sensor.getName(),
268
+ RSSI: sensor.getRSSI(),
269
+ signalStrength: sensor.getSignalStrength(),
270
+ lastContactDelta: etslc
271
+ }
317
272
  }
318
- sensor.getPathMetadata().forEach((metadatum,tag)=>{
319
- oneOf.properties.paths.properties[tag]=metadatum.asJSONSchema()
320
- })
321
- plugin.schema.properties.peripherals.items.dependencies.mac_address.oneOf.push(oneOf)
322
- //plugin.schema.properties.peripherals.items.title=sensor.getName()
323
273
 
324
- }
325
- function deviceNameAndAddress(config){
326
- return `${config?.name??""}${config.name?" at ":""}${config.mac_address}`
327
- }
328
-
329
- async function createSensor(adapter, config) {
330
- return new Promise( ( resolve, reject )=>{
331
- var s
274
+ function sensorToJSON(sensor){
275
+ const config = getDeviceConfig(sensor.getMacAddress())
276
+ return {
277
+ info: getSensorInfo(sensor),
278
+ schema: sensor.getJSONSchema(),
279
+ config: config?config:{}
280
+
281
+ }
282
+ }
283
+ async function startScanner(transport) {
332
284
 
333
- app.debug(`Waiting on ${deviceNameAndAddress(config)}`)
334
- adapter.waitDevice(config.mac_address,config.discoveryTimeout*1000)
335
- .then(async (device)=> {
336
- app.debug(`Found ${config.mac_address}`)
337
- s = await instantiateSensor(device,config)
338
- sensorMap.set(config.mac_address,s)
339
-
340
- if (s instanceof BLACKLISTED)
341
- reject ( `Device is blacklisted (${s.reasonForBlacklisting()}).`)
342
- else{
343
- addSensorToList(s)
344
- s.on("RSSI",(()=>{
345
- updateSensorDisplayName(s)
346
- }))
347
- resolve(s)
285
+ app.debug("Starting scan...");
286
+ //Use adapter.helper directly to get around Adapter::startDiscovery()
287
+ //filter options which can cause issues with Device::Connect()
288
+ //turning off Discovery
289
+ //try {await adapter.startDiscovery()}
290
+ try{
291
+ if (transport) {
292
+ app.debug(`Setting Bluetooth transport option to ${transport}`)
293
+ await adapter.helper.callMethod('SetDiscoveryFilter', {
294
+ Transport: new Variant('s', transport)
295
+ })
296
+ }
297
+ await adapter.helper.callMethod('StartDiscovery')
298
+ }
299
+ catch (error){
300
+ app.debug(error)
348
301
  }
349
- })
350
- .catch((e)=>{
351
- if (s)
352
- s.stopListening()
353
-
354
- app.debug(`Unable to communicate with device ${deviceNameAndAddress(config)} Reason: ${e?.message??e}`)
355
- app.debug(e)
356
- reject( e?.message??e )
357
- })})
358
- }
359
-
360
- async function startScanner(transport) {
361
-
362
- app.debug("Starting scan...");
363
- //Use adapter.helper directly to get around Adapter::startDiscovery()
364
- //filter options which can cause issues with Device::Connect()
365
- //turning off Discovery
366
- //try {await adapter.startDiscovery()}
367
- try{
368
- if (transport) {
369
- app.debug(`Setting Bluetooth transport option to ${transport}`)
370
- await adapter.helper.callMethod('SetDiscoveryFilter', {
371
- Transport: new Variant('s', transport)
372
- })
373
- }
374
- await adapter.helper.callMethod('StartDiscovery')
375
- }
376
- catch (error){
377
- app.debug(error)
302
+
378
303
  }
379
304
 
380
- }
381
-
382
- const sensorMap=new Map()
383
-
384
- plugin.started=false
305
+ function updateSensor(sensor){
306
+ channel.broadcast(getSensorInfo(sensor), "sensorchanged")
307
+ }
385
308
 
386
- loadClassMap()
387
- var discoveryIntervalID
388
- var adapter
389
- var adapterPower
390
- plugin.start = async function (options, restartPlugin) {
391
- plugin.started=true
309
+ function removeSensorFromList(sensor){
310
+ sensorMap.delete(sensor.getMacAddress())
311
+ channel.broadcast({mac:sensor.getMacAddress()},"removesensor")
312
+ }
392
313
 
393
- var adapterID=options.adapter
314
+ function addSensorToList(sensor){
315
+ app.debug(`adding sensor to list ${sensor.getMacAddress()}`)
316
+ if (sensorMap.has(sensor.getMacAddress()) )
317
+ debugger
318
+ sensorMap.set(sensor.getMacAddress(),sensor)
319
+ channel.broadcast(sensorToJSON(sensor),"newsensor");
320
+ }
321
+ function deviceNameAndAddress(config){
322
+ return `${config?.name??""}${config.name?" at ":""}${config.mac_address}`
323
+ }
394
324
 
325
+ function createSensor(adapter, config) {
326
+ return new Promise( ( resolve, reject )=>{
327
+ var s
328
+ const startNumber=starts
329
+ //app.debug(`Waiting on ${deviceNameAndAddress(config)}`)
330
+ adapter.waitDevice(config.mac_address,(config?.discoveryTimeout??30)*1000)
331
+ .then(async (device)=> {
332
+ if (startNumber != starts ) {
333
+ return
334
+ }
335
+ //app.debug(`Found ${config.mac_address}`)
336
+ s = await instantiateSensor(device,config)
337
+
338
+ if (s instanceof BLACKLISTED)
339
+ reject ( `Device is blacklisted (${s.reasonForBlacklisting()}).`)
340
+ else{
341
+ addSensorToList(s)
342
+ s._lastRSSI=-1*Infinity
343
+ s.on("RSSI",(()=>{
344
+ if (Date.now()-s._lastRSSI > 20000) { //only update RSSI on client every five seconds
345
+ //app.debug(`${s.getMacAddress()} ${Date.now()-s._lastRSSI}`)
346
+ s._lastRSSI=Date.now()
347
+ updateSensor(s)
348
+ }
349
+
350
+ }))
351
+ resolve(s)
352
+ }
353
+ })
354
+ .catch((e)=>{
355
+ if (s)
356
+ s.stopListening()
357
+ if (startNumber == starts ) {
358
+ app.debug(`Unable to communicate with device ${deviceNameAndAddress(config)} Reason: ${e?.message??e}`)
359
+ app.debug(e)
360
+ reject( e?.message??e )
361
+ }
362
+ })})
363
+ }
395
364
  function getDeviceConfig(mac){
396
365
  return deviceConfigs.find((p)=>p.mac_address==mac)
397
366
  }
367
+ async function instantiateSensor(device,config){
368
+ try{
369
+ for (var [clsName, cls] of classMap) {
370
+ if (clsName.startsWith("_")) continue
371
+ const c = await cls.identify(device)
372
+ if (c) {
373
+ c.debug=app.debug
374
+ const sensor = new c(device,config?.params, config?.gattParams)
375
+ sensor.debug=app.debug
376
+ sensor.app=app
377
+ sensor._adapter=adapter //HACK!
378
+
379
+ await sensor.init()
380
+ //app.debug(`instantiated ${await BTSensor.getDeviceProp(device,"Address")}`)
381
+
382
+ return sensor
383
+ }
384
+ }} catch(error){
385
+ const msg = `Unable to instantiate ${await BTSensor.getDeviceProp(device,"Address")}: ${error.message} `
386
+ app.debug(msg)
387
+ app.debug(error)
388
+ app.setPluginError(msg)
389
+ }
390
+ //if we're here ain't got no class for the device
391
+ var sensor
392
+ if (config.params?.sensorClass){
393
+ var c = classMap.get(config.params.sensorClass)
394
+ } else{
395
+ c = classMap.get('UNKNOWN')
396
+ }
397
+ c.debug=app.debug
398
+ sensor = new c(device,config?.params, config?.gattParams)
399
+ sensor.debug=app.debug
400
+ sensor.app=app
398
401
 
402
+ await sensor.init()
403
+ return sensor
404
+ }
405
+
399
406
  function initConfiguredDevice(deviceConfig){
407
+ const startNumber=starts
400
408
  app.setPluginStatus(`Initializing ${deviceNameAndAddress(deviceConfig)}`);
401
409
  if (!deviceConfig.discoveryTimeout)
402
410
  deviceConfig.discoveryTimeout = options.discoveryTimeout
403
411
  createSensor(adapter, deviceConfig).then((sensor)=>{
404
- deviceConfig.sensor=sensor
412
+ if (startNumber != starts ) {
413
+ return
414
+ }
405
415
  if (deviceConfig.active) {
406
- if (deviceConfig.paths){
407
- createPaths(deviceConfig)
408
- initPaths(deviceConfig)
409
- }
410
- const result = Promise.resolve(deviceConfig.sensor.listen())
411
- result.then(() => {
412
- app.debug(`Listening for changes from ${deviceConfig.sensor.getDisplayName()}`);
413
- app.setPluginStatus(`Initial scan complete. Listening to ${++found} sensors.`);
414
- })
415
-
416
+ app.setPluginStatus(`Listening to ${++foundConfiguredDevices} sensors.`);
417
+ sensor.activate(deviceConfig, plugin)
416
418
  }
417
-
419
+
418
420
  })
419
421
  .catch((error)=>
420
422
  {
423
+ if (deviceConfig.unconfigured) return
424
+ if (startNumber != starts ) {
425
+ return
426
+ }
421
427
  const msg =`Sensor at ${deviceConfig.mac_address} unavailable. Reason: ${error}`
422
428
  app.debug(msg)
423
429
  app.debug(error)
424
430
  if (deviceConfig.active)
425
431
  app.setPluginError(msg)
426
- deviceConfig.sensor=new MissingSensor(deviceConfig)
427
- addSensorToList(deviceConfig.sensor) //add sensor to list with known options
432
+ const sensor=new MissingSensor(deviceConfig)
433
+ ++foundConfiguredDevices
434
+
435
+ addSensorToList(sensor) //add sensor to list with known options
428
436
 
429
437
  })
430
438
  }
431
439
  function findDevices (discoveryTimeout) {
440
+ const startNumber = starts
432
441
  app.setPluginStatus("Scanning for new Bluetooth devices...");
433
442
 
434
443
  adapter.devices().then( (macs)=>{
435
- for (var mac of macs) {
436
- const deviceConfig = getDeviceConfig(mac)
437
- const _mac = mac
438
-
439
- if (deviceConfig && deviceConfig.sensor instanceof MissingSensor){
440
- removeSensorFromList(deviceConfig.sensor)
441
- initConfiguredDevice(deviceConfig)
442
- } else
443
- {
444
- if (!sensorMap.has(_mac)) {
445
- if (deviceConfig) continue;
446
- createSensor(adapter,
447
- {mac_address:_mac, discoveryTimeout: discoveryTimeout*1000})
448
- .then((s)=>
449
- app.setPluginStatus(`Found ${s.getDisplayName()}.`))
450
- .catch((e)=>
451
- app.debug(`Device at ${_mac} unavailable. Reason: ${e}`))
444
+ if (startNumber != starts ) {
445
+ return
446
+ }
447
+ for (const mac of macs) {
448
+ var deviceConfig = getDeviceConfig(mac)
449
+ const sensor = sensorMap.get(mac)
450
+
451
+ if (sensor) {
452
+ if (sensor instanceof MissingSensor){
453
+ removeSensorFromList(sensor)
454
+ initConfiguredDevice(deviceConfig)
452
455
  }
456
+ } else {
457
+
458
+ if (!deviceConfig) {
459
+ deviceConfig = {mac_address: mac,
460
+ discoveryTimeout: discoveryTimeout*1000,
461
+ active: false, unconfigured: true}
462
+ initConfiguredDevice(deviceConfig)
463
+ }
453
464
  }
454
465
  }
455
466
  })
@@ -458,10 +469,13 @@ module.exports = function (app) {
458
469
  function findDeviceLoop(discoveryTimeout, discoveryInterval, immediate=true ){
459
470
  if (immediate)
460
471
  findDevices(discoveryTimeout)
461
- discoveryIntervalID = setInterval( findDevices, discoveryInterval*1000, discoveryTimeout)
472
+ discoveryIntervalID =
473
+ setInterval( findDevices, discoveryInterval*1000, discoveryTimeout)
462
474
  }
463
-
464
-
475
+
476
+ channel.broadcast({state:"started"},"pluginstate")
477
+
478
+
465
479
  if (!adapterID || adapterID=="")
466
480
  adapterID = "hci0"
467
481
  //Check if Adapter has changed since last start()
@@ -483,39 +497,32 @@ module.exports = function (app) {
483
497
 
484
498
  await adapter.helper._prepare()
485
499
  adapter.helper._propsProxy.on('PropertiesChanged', async (iface,changedProps,invalidated) => {
500
+ app.debug(changedProps)
486
501
  if (Object.hasOwn(changedProps,"Powered")){
487
502
  if (changedProps.Powered.value==false) {
488
503
  if (plugin.started){ //only call stop() if plugin is started
489
504
  app.setPluginStatus(`Bluetooth Adapter ${adapterID} turned off. Plugin disabled.`)
490
505
  await plugin.stop()
491
- adapterPower=false
492
506
  }
493
507
  } else {
494
- if (!adapterPower) { //only call start() once
495
- adapterPower=true
496
- await plugin.start(options,restartPlugin)
497
- }
508
+ await restartPlugin(options)
498
509
  }
499
510
  }
500
511
  })
501
512
  if (!await adapter.isPowered()) {
502
513
  app.debug(`Bluetooth Adapter ${adapterID} not powered on.`)
503
514
  app.setPluginError(`Bluetooth Adapter ${adapterID} not powered on.`)
504
- adapterPower=false
505
515
  await plugin.stop()
506
516
  return
507
517
  }
508
518
  }
509
- adapterPower=true
510
519
 
511
- plugin.uiSchema.peripherals['ui:disabled']=false
512
520
  sensorMap.clear()
521
+ if (channel)
522
+ channel.broadcast({state:"started"},"pluginstate")
513
523
  deviceConfigs=options?.peripherals??[]
514
524
 
515
525
  if (plugin.stopped) {
516
- await sleep(5000) //Make sure plugin.stop() completes first
517
- //plugin.start is called asynchronously for some reason
518
- //and does not wait for plugin.stop to complete
519
526
  plugin.stopped=false
520
527
  }
521
528
 
@@ -526,15 +533,10 @@ module.exports = function (app) {
526
533
  plugin.schema.properties.adapter.enum.push(a.adapter)
527
534
  plugin.schema.properties.adapter.enumNames.push(`${a.adapter} @ ${ await a.getAddress()} (${await a.getName()})`)
528
535
  }
529
-
530
- plugin.uiSchema.adapter={'ui:disabled': (activeAdapters.length==1)}
531
-
532
536
 
533
537
  await startScanner(options.transport)
534
538
  if (starts>0){
535
539
  app.debug(`Plugin ${packageInfo.version} restarting...`);
536
- if (plugin.schema.properties.peripherals.items.dependencies)
537
- plugin.schema.properties.peripherals.items.dependencies.mac_address.oneOf=[]
538
540
  } else {
539
541
  app.debug(`Plugin ${packageInfo.version} started` )
540
542
 
@@ -542,43 +544,90 @@ module.exports = function (app) {
542
544
  starts++
543
545
  if (!await adapter.isDiscovering())
544
546
  try{
545
- await adapter.startDiscovery()
547
+ await startScanner()
546
548
  } catch (e){
547
549
  app.debug(`Error starting scan: ${e.message}`)
548
550
  }
549
551
  if (!(deviceConfigs===undefined)){
550
- var found = 0
551
- for (const deviceConfig of deviceConfigs) {
552
- initConfiguredDevice(deviceConfig)
552
+ const maxTimeout=Math.max(...deviceConfigs.map((dc)=>dc?.discoveryTimeout??options.discoveryTimeout))
553
+ var progress = 0
554
+ if (progressID==null)
555
+ progressID = setInterval(()=>{
556
+ channel.broadcast({"progress":++progress, "maxTimeout": maxTimeout, "deviceCount":foundConfiguredDevices, "totalDevices": deviceConfigs.length},"progress")
557
+ if ( foundConfiguredDevices==deviceConfigs.length){
558
+ app.debug("progress complete")
559
+ progressID,progressTimeoutID = null
560
+ clearTimeout(progressTimeoutID)
561
+ clearInterval(progressID)
562
+ progressID = null
563
+ }
564
+ },1000);
565
+ if (progressTimeoutID==null)
566
+ progressTimeoutID = setTimeout(()=> {
567
+ app.debug("progress timed out ")
568
+ if (progressID) {
569
+
570
+ clearInterval(progressID);
571
+ progressID=null
572
+ channel.broadcast({"progress":maxTimeout, "maxTimeout": maxTimeout, "deviceCount":foundConfiguredDevices, "totalDevices": deviceConfigs.length},"progress")
573
+ }
574
+ }, (maxTimeout+1)*1000);
575
+
576
+ for (const config of deviceConfigs) {
577
+ initConfiguredDevice(config)
553
578
  }
554
579
  }
580
+ const minTimeout=Math.min(...deviceConfigs.map((dc)=>dc?.discoveryTimeout??options.discoveryTimeout))
581
+ const intervalTimeout = ((minTimeout==Infinity)?(options?.discoveryTimeout??plugin.schema.properties.discoveryTimeout.default):minTimeout)*1000
582
+ deviceHealthID = setInterval( ()=> {
583
+ sensorMap.forEach((sensor)=>{
584
+ const config = getDeviceConfig(sensor.getMacAddress())
585
+ const dt = config?.discoveryTimeout??options.discoveryTimeout
586
+ const lc=sensor.elapsedTimeSinceLastContact()
587
+ if (lc > dt) {
588
+ app.debug(`${sensor.getMacAddress()} not heard from in ${lc} seconds`)
589
+ channel.broadcast(getSensorInfo(sensor), "sensorchanged")
590
+ }
591
+ })
592
+ }, intervalTimeout)
593
+
594
+ if (!options.hasOwnProperty("discoveryInterval" )) //no config -- first run
595
+ options.discoveryInterval = plugin.schema.properties.discoveryInterval.default
596
+
555
597
  if (options.discoveryInterval && !discoveryIntervalID)
556
- findDeviceLoop(options.discoveryTimeout, options.discoveryInterval)
598
+ findDeviceLoop(options?.discoveryTimeout??plugin.schema.properties.discoveryTimeout.default,
599
+ options.discoveryInterval)
557
600
  }
558
601
  plugin.stop = async function () {
559
602
  app.debug("Stopping plugin")
560
603
  plugin.stopped=true
561
604
  plugin.started=false
562
- plugin.uiSchema.peripherals['ui:disabled']=true
605
+ channel.broadcast({state:"stopped"},"pluginstate")
606
+ if (discoveryIntervalID) {
607
+ clearInterval(discoveryIntervalID)
608
+ discoveryIntervalID=null
609
+ }
610
+ if (progressID) {
611
+ clearInterval(progressID)
612
+ progressID=null
613
+ }
614
+ if (progressTimeoutID) {
615
+ clearTimeout(progressTimeoutID)
616
+ progressTimeoutID=null
617
+ }
618
+
563
619
  if ((sensorMap)){
564
- plugin.schema.properties.peripherals.items.properties.mac_address.enum=[]
565
- plugin.schema.properties.peripherals.items.properties.mac_address.enumNames=[]
566
- sensorMap.forEach(async (sensor, mac)=> {
620
+ for await (const sensorEntry of sensorMap.entries()) {
567
621
  try{
568
- await sensor.stopListening()
569
- app.debug(`No longer listening to ${mac}`)
622
+ await sensorEntry[1].stopListening()
623
+ app.debug(`No longer listening to ${sensorEntry[0]}`)
570
624
  }
571
625
  catch (e){
572
- app.debug(`Error stopping listening to ${mac}: ${e.message}`)
626
+ app.debug(`Error stopping listening to ${sensorEntry[0]}: ${e.message}`)
573
627
  }
574
- })
575
- }
576
-
577
- if (discoveryIntervalID) {
578
- clearInterval(discoveryIntervalID)
579
- discoveryIntervalID=null
628
+ }
580
629
  }
581
-
630
+ sensorMap.clear()
582
631
  if (adapter && await adapter.isDiscovering())
583
632
  try{
584
633
  await adapter.stopDiscovery()
@@ -586,9 +635,10 @@ module.exports = function (app) {
586
635
  } catch (e){
587
636
  app.debug(`Error stopping scan: ${e.message}`)
588
637
  }
638
+
589
639
  app.debug('BT Sensors plugin stopped')
590
640
 
591
641
  }
592
-
642
+
593
643
  return plugin;
594
644
  }