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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -5,37 +5,47 @@ const semver = require('semver')
5
5
  const packageInfo = require("./package.json")
6
6
 
7
7
  const {createBluetooth} = require('node-ble')
8
- const { Variant } = require('dbus-next')
8
+ const {Variant} = require('dbus-next')
9
9
  const {bluetooth, destroy} = createBluetooth()
10
10
 
11
11
  const BTSensor = require('./BTSensor.js')
12
12
  const BLACKLISTED = require('./sensor_classes/BlackListedDevice.js')
13
+ const { createChannel, createSession } = require("better-sse");
14
+ const { clearTimeout } = require('timers')
13
15
 
14
16
  class MissingSensor {
15
17
 
16
18
 
17
19
  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
20
+ this.config=config
21
+ this.addPath=BTSensor.prototype.addPath.bind(this)
22
+ this.addParameter=BTSensor.prototype.addParameter.bind(this)
23
+
24
+ this.getJSONSchema = BTSensor.prototype.getJSONSchema.bind(this)
25
+ this.initSchema = BTSensor.prototype.initSchema.bind(this)
26
+
27
+ this.initSchema()
22
28
 
23
- this.metadata= new Map()
24
29
  var keys = Object.keys(config?.paths??{})
25
- this.addMetadatum.bind(this)
30
+
26
31
  keys.forEach((key)=>{
27
- this.addMetadatum(key, config.paths[key]?.type??'string', config.paths[key].description )
32
+ this.addPath(key,
33
+ { type: config.paths[key]?.type??'string',
34
+ title: config.paths[key].title} )
28
35
  } )
29
36
  keys = Object.keys(config?.params??{})
30
37
  keys.forEach((key)=>{
31
- this.addMetadatum(key, config.params[key]?.type??'string', config.params[key].description ).isParam=true
38
+ this.addParameter(key,
39
+ { type: config.params[key]?.type??'string',
40
+ title: config.params[key].title
41
+ })
32
42
  this[key]=config.params[key]
33
43
  })
34
44
  this.mac_address = config.mac_address
35
45
 
36
46
  }
37
47
  hasGATT(){
38
- return false
48
+ return this.config.gattParams
39
49
  }
40
50
  getMetadata(){
41
51
  return this.metadata
@@ -47,13 +57,26 @@ class MissingSensor {
47
57
  return ""
48
58
  }
49
59
  getName(){
50
- return this?.name??"Unknown device"
60
+ return `${this?.name??"Unknown device"} (OUT OF RANGE)`
51
61
  }
52
62
  getDisplayName(){
53
- return `OUT OF RANGE DEVICE (${this.getName()} ${this.getMacAddress()})`
63
+ return `(${this.getName()} ${this.getMacAddress()})`
64
+ }
65
+ getRSSI(){
66
+ return NaN
54
67
  }
55
68
  stopListening(){}
56
69
  listen(){}
70
+ isActive(){
71
+ return false
72
+ }
73
+ elapsedTimeSinceLastContact(){
74
+ return NaN
75
+ }
76
+ getSignalStrength(){
77
+ return NaN
78
+ }
79
+
57
80
  }
58
81
  module.exports = function (app) {
59
82
  var adapterID = 'hci0'
@@ -69,8 +92,6 @@ module.exports = function (app) {
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
96
  //Try and load utilities-sk NOTE: should be installed from App Store--
76
97
  //But there's a fail safe because I'm a reasonable man.
@@ -98,82 +119,13 @@ module.exports = function (app) {
98
119
  }, x);
99
120
  });
100
121
  }
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() {
122
+
123
+ function loadClassMap() {
171
124
  const _classMap = utilities_sk.loadClasses(path.join(__dirname, 'sensor_classes'))
172
125
  classMap = new Map([..._classMap].filter(([k, v]) => !k.startsWith("_") ))
173
126
  const libPath = app.config.appPath +(
174
- semver.gt(app.config.version,"2.13.5")?"dist":"lib"
127
+ semver.gt(app.config.version,"2.13.5")?"dist":"lib"
175
128
  )
176
- //+ app.config.version
177
129
  import(libPath+"/modules.js").then( (modulesjs)=>{
178
130
  const { default:defaultExport} = modulesjs
179
131
  const modules = defaultExport.modulesWithKeyword(app.config, "signalk-bt-sensor-class")
@@ -191,265 +143,361 @@ module.exports = function (app) {
191
143
 
192
144
  plugin.schema = {
193
145
  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
146
  required:["adapter","discoveryTimeout", "discoveryInterval"],
199
147
  properties: {
200
148
  adapter: {title: "Bluetooth adapter",
201
149
  type: "string", default: "hci0"},
202
150
  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
-
151
+ type: "string", enum: ["auto","le","bredr"], default: "le", enumNames:["Auto", "LE-Bluetooth Low Energy", "BR/EDR Bluetooth basic rate/enhanced data rate"]},
205
152
  discoveryTimeout: {title: "Default device discovery timeout (in seconds)",
206
153
  type: "integer", default: 30,
207
154
  minimum: 10,
208
155
  maximum: 3600
209
156
  },
210
-
211
157
  discoveryInterval: {title: "Scan for new devices interval (in seconds-- 0 for no new device scanning)",
212
158
  type: "integer",
213
159
  default: 10,
214
- minimum: 0,
215
- multipleOf: 10
160
+ minimum: 0
216
161
  },
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
162
  }
234
163
  }
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
164
 
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()
261
-
262
- const i = mac_addresses_list.indexOf(mac_address)
263
- if (i<0) return // n'existe pas
165
+ const sensorMap=new Map()
166
+
167
+ plugin.started=false
264
168
 
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)
169
+ loadClassMap()
170
+ var discoveryIntervalID, progressID, progressTimeoutID, deviceHealthID
171
+ var adapter
172
+ var adapterPower
173
+ const channel = createChannel()
268
174
 
175
+ plugin.registerWithRouter = function(router) {
176
+ router.get('/sendPluginState', async (req, res) => {
177
+
178
+ res.status(200).json({
179
+ "state":(plugin.started?"started":"stopped")
180
+ })
181
+ });
182
+ router.get("/sse", async (req, res) => {
183
+ const session = await createSession(req, res);
184
+ channel.register(session)
185
+ });
186
+
269
187
  }
270
- function addSensorToList(sensor){
271
- if (!plugin.schema.properties.peripherals.items.dependencies)
272
- plugin.schema.properties.peripherals.items.dependencies={mac_address:{oneOf:[]}}
188
+
189
+ plugin.start = async function (options, restartPlugin) {
190
+ plugin.started=true
191
+ var adapterID=options.adapter
192
+ var foundConfiguredDevices=0
193
+ plugin.registerWithRouter = function(router) {
194
+
195
+ router.post('/sendSensorData', async (req, res) => {
196
+ app.debug(req.body)
197
+ const i = deviceConfigs.findIndex((p)=>p.mac_address==req.body.mac_address)
198
+ if (i<0){
199
+ options.peripherals.push(req.body)
200
+ } else {
201
+ options.peripherals[i] = req.body
202
+ }
203
+ app.savePluginOptions(
204
+ options, async () => {
205
+ app.debug('Plugin options saved')
206
+ res.status(200).json({message: "Sensor updated"})
207
+ const sensor = sensorMap.get(req.body.mac_address)
208
+ if (sensor) {
209
+ removeSensorFromList(sensor)
210
+ if (sensor.isActive()) {
211
+ sensor.stopListening().then(()=>
212
+ initConfiguredDevice(req.body)
213
+ )
214
+ } else {
215
+ initConfiguredDevice(req.body)
216
+ }
217
+ }
218
+
219
+ }
220
+ )
221
+
222
+ });
223
+ router.post('/removeSensorData', async (req, res) => {
224
+ app.debug(req.body)
225
+ const i = deviceConfigs.findIndex((p)=>p.mac_address==req.body.mac_address)
226
+ if (i>=0){
227
+ deviceConfigs.splice(i,1)
228
+ }
229
+ if (sensorMap.has(req.body.mac_address))
230
+ sensorMap.delete(req.body.mac_address)
231
+ app.savePluginOptions(
232
+ options, async () => {
233
+ app.debug('Plugin options saved')
234
+ res.status(200).json({message: "Sensor updated"})
235
+ channel.broadcast({},"resetSensors")
236
+ }
237
+ )
238
+
239
+ });
240
+
241
+ router.post('/sendBaseData', async (req, res) => {
242
+
243
+ app.debug(req.body)
244
+ Object.assign(options,req.body)
245
+ app.savePluginOptions(
246
+ options, () => {
247
+ app.debug('Plugin options saved')
248
+ res.status(200).json({message: "Plugin updated"})
249
+ channel.broadcast({},"pluginRestarted")
250
+ restartPlugin(options)
251
+ }
252
+ )
253
+ });
273
254
 
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
255
 
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
- })
256
+ router.get('/base', (req, res) => {
257
+
258
+ res.status(200).json(
259
+ {
260
+ schema: plugin.schema,
261
+ data: {
262
+ adapter: options.adapter,
263
+ transport: options.transport,
264
+ discoveryTimeout: options.discoveryTimeout,
265
+ discoveryInterval: options.discoveryInterval
266
+ }
267
+ }
268
+ );
269
+ })
270
+ router.get('/sensors', (req, res) => {
271
+ app.debug("Sending sensors")
272
+ const t = sensorsToJSON()
273
+ res.status(200).json(t)
274
+ });
275
+
276
+ router.get('/progress', (req, res) => {
277
+ app.debug("Sending progress")
278
+ const json = {"progress":foundConfiguredDevices/deviceConfigs.length, "maxTimeout": 1,
279
+ "deviceCount":foundConfiguredDevices,
280
+ "totalDevices": deviceConfigs.length}
281
+ res.status(200).json(json)
282
+
283
+ });
284
+
285
+ router.get('/sendPluginState', async (req, res) => {
286
+
287
+ res.status(200).json({
288
+ "state":(plugin.started?"started":"stopped")
289
+ })
290
+ });
291
+ router.get("/sse", async (req, res) => {
292
+ const session = await createSession(req, res);
293
+ channel.register(session)
294
+ });
295
+
298
296
 
299
- if (sensor.hasGATT()){
297
+ };
300
298
 
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
- })
299
+ function sensorsToJSON(){
300
+ return Array.from(
301
+ Array.from(sensorMap.values()).filter((s)=>!(s instanceof BLACKLISTED) ).map(
302
+ sensorToJSON
303
+ ))
310
304
  }
311
305
 
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:{}
306
+ function getSensorInfo(sensor){
307
+ if (sensor.getName()=="vent")
308
+ debugger
309
+ return { mac: sensor.getMacAddress(),
310
+ name: sensor.getName(),
311
+ RSSI: sensor.getRSSI(),
312
+ signalStrength: sensor.getSignalStrength(),
313
+ lastContactDelta: sensor. elapsedTimeSinceLastContact()
314
+ }
317
315
  }
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
316
 
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
317
+ function sensorToJSON(sensor){
318
+ const config = getDeviceConfig(sensor.getMacAddress())
319
+ return {
320
+ info: getSensorInfo(sensor),
321
+ schema: sensor.getJSONSchema(),
322
+ config: config?config:{}
323
+
324
+ }
325
+ }
326
+ async function startScanner(transport) {
332
327
 
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)
328
+ app.debug("Starting scan...");
329
+ //Use adapter.helper directly to get around Adapter::startDiscovery()
330
+ //filter options which can cause issues with Device::Connect()
331
+ //turning off Discovery
332
+ //try {await adapter.startDiscovery()}
333
+ try{
334
+ if (transport) {
335
+ app.debug(`Setting Bluetooth transport option to ${transport}`)
336
+ await adapter.helper.callMethod('SetDiscoveryFilter', {
337
+ Transport: new Variant('s', transport)
338
+ })
339
+ }
340
+ await adapter.helper.callMethod('StartDiscovery')
341
+ }
342
+ catch (error){
343
+ app.debug(error)
348
344
  }
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)
345
+
378
346
  }
379
347
 
380
- }
381
-
382
- const sensorMap=new Map()
383
-
384
- plugin.started=false
348
+ function updateSensor(sensor){
349
+ channel.broadcast(getSensorInfo(sensor), "sensorchanged")
350
+ }
385
351
 
386
- loadClassMap()
387
- var discoveryIntervalID
388
- var adapter
389
- var adapterPower
390
- plugin.start = async function (options, restartPlugin) {
391
- plugin.started=true
352
+ function removeSensorFromList(sensor){
353
+ sensorMap.delete(sensor.getMacAddress())
354
+ channel.broadcast({mac:sensor.getMacAddress()},"removesensor")
355
+ }
392
356
 
393
- var adapterID=options.adapter
357
+ async function addSensorToList(sensor){
358
+ app.debug(`adding sensor to list ${sensor.getMacAddress()}`)
359
+ if (sensorMap.has(sensor.getMacAddress()) )
360
+ debugger
361
+ sensorMap.set(sensor.getMacAddress(),sensor)
362
+ channel.broadcast(sensorToJSON(sensor),"newsensor");
363
+ }
364
+ function deviceNameAndAddress(config){
365
+ return `${config?.name??""}${config.name?" at ":""}${config.mac_address}`
366
+ }
394
367
 
368
+ async function createSensor(adapter, config) {
369
+ return new Promise( ( resolve, reject )=>{
370
+ var s
371
+ const startNumber=starts
372
+ //app.debug(`Waiting on ${deviceNameAndAddress(config)}`)
373
+ adapter.waitDevice(config.mac_address,config.discoveryTimeout*1000)
374
+ .then(async (device)=> {
375
+ if (startNumber != starts ) {
376
+ debugger
377
+ return
378
+ }
379
+ //app.debug(`Found ${config.mac_address}`)
380
+ s = await instantiateSensor(device,config)
381
+
382
+ if (s instanceof BLACKLISTED)
383
+ reject ( `Device is blacklisted (${s.reasonForBlacklisting()}).`)
384
+ else{
385
+
386
+ addSensorToList(s)
387
+ s.on("RSSI",(()=>{
388
+ updateSensor(s)
389
+ }))
390
+ resolve(s)
391
+ }
392
+ })
393
+ .catch((e)=>{
394
+ if (s)
395
+ s.stopListening()
396
+ if (startNumber == starts ) {
397
+ app.debug(`Unable to communicate with device ${deviceNameAndAddress(config)} Reason: ${e?.message??e}`)
398
+ app.debug(e)
399
+ reject( e?.message??e )
400
+ }
401
+ })})
402
+ }
395
403
  function getDeviceConfig(mac){
396
404
  return deviceConfigs.find((p)=>p.mac_address==mac)
397
405
  }
398
-
406
+ async function instantiateSensor(device,config){
407
+ try{
408
+ for (var [clsName, cls] of classMap) {
409
+ if (clsName.startsWith("_")) continue
410
+ const c = await cls.identify(device)
411
+ if (c) {
412
+ c.debug=app.debug
413
+ const sensor = new c(device,config?.params, config?.gattParams)
414
+ sensor.debug=app.debug
415
+ sensor.app=app
416
+ await sensor.init()
417
+ //app.debug(`instantiated ${await BTSensor.getDeviceProp(device,"Address")}`)
418
+
419
+ return sensor
420
+ }
421
+ }} catch(error){
422
+ const msg = `Unable to instantiate ${await BTSensor.getDeviceProp(device,"Address")}: ${error.message} `
423
+ app.debug(msg)
424
+ app.debug(error)
425
+ app.setPluginError(msg)
426
+ }
427
+ //if we're here ain't got no class for the device
428
+ var sensor
429
+ if (config.params?.sensorClass){
430
+ const c = classMap.get(config.params.sensorClass)
431
+ c.debug=app.debug
432
+ sensor = new c(device,config?.params, config?.gattParams)
433
+ sensor.debug=app.debug
434
+ sensor.app=app
435
+ } else{
436
+ sensor = new (classMap.get('UNKNOWN'))(device)
437
+ sensor.app=app
438
+ }
439
+ await sensor.init()
440
+ return sensor
441
+ }
442
+
399
443
  function initConfiguredDevice(deviceConfig){
444
+ const startNumber=starts
400
445
  app.setPluginStatus(`Initializing ${deviceNameAndAddress(deviceConfig)}`);
401
446
  if (!deviceConfig.discoveryTimeout)
402
447
  deviceConfig.discoveryTimeout = options.discoveryTimeout
403
448
  createSensor(adapter, deviceConfig).then((sensor)=>{
404
- deviceConfig.sensor=sensor
449
+ if (startNumber != starts ) {
450
+ return
451
+ }
405
452
  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
-
453
+ app.setPluginStatus(`Listening to ${++foundConfiguredDevices} sensors.`);
454
+ sensor.activate(deviceConfig, plugin)
416
455
  }
417
-
456
+
418
457
  })
419
458
  .catch((error)=>
420
459
  {
460
+ if (deviceConfig.unconfigured) return
461
+ if (startNumber != starts ) {
462
+ return
463
+ }
421
464
  const msg =`Sensor at ${deviceConfig.mac_address} unavailable. Reason: ${error}`
422
465
  app.debug(msg)
423
466
  app.debug(error)
424
467
  if (deviceConfig.active)
425
468
  app.setPluginError(msg)
426
- deviceConfig.sensor=new MissingSensor(deviceConfig)
427
- addSensorToList(deviceConfig.sensor) //add sensor to list with known options
469
+ const sensor=new MissingSensor(deviceConfig)
470
+ ++foundConfiguredDevices
471
+
472
+ addSensorToList(sensor) //add sensor to list with known options
428
473
 
429
474
  })
430
475
  }
431
476
  function findDevices (discoveryTimeout) {
477
+ const startNumber = starts
432
478
  app.setPluginStatus("Scanning for new Bluetooth devices...");
433
479
 
434
480
  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}`))
481
+ if (startNumber != starts ) {
482
+ return
483
+ }
484
+ for (const mac of macs) {
485
+ var deviceConfig = getDeviceConfig(mac)
486
+ const sensor = sensorMap.get(mac)
487
+
488
+ if (sensor) {
489
+ if (sensor instanceof MissingSensor){
490
+ removeSensorFromList(sensor)
491
+ initConfiguredDevice(deviceConfig)
452
492
  }
493
+ } else {
494
+
495
+ if (!deviceConfig) {
496
+ deviceConfig = {mac_address: mac,
497
+ discoveryTimeout: discoveryTimeout*1000,
498
+ active: false, unconfigured: true}
499
+ initConfiguredDevice(deviceConfig)
500
+ }
453
501
  }
454
502
  }
455
503
  })
@@ -458,9 +506,10 @@ module.exports = function (app) {
458
506
  function findDeviceLoop(discoveryTimeout, discoveryInterval, immediate=true ){
459
507
  if (immediate)
460
508
  findDevices(discoveryTimeout)
461
- discoveryIntervalID = setInterval( findDevices, discoveryInterval*1000, discoveryTimeout)
509
+ discoveryIntervalID =
510
+ setInterval( findDevices, discoveryInterval*1000, discoveryTimeout)
462
511
  }
463
-
512
+
464
513
 
465
514
  if (!adapterID || adapterID=="")
466
515
  adapterID = "hci0"
@@ -483,18 +532,15 @@ module.exports = function (app) {
483
532
 
484
533
  await adapter.helper._prepare()
485
534
  adapter.helper._propsProxy.on('PropertiesChanged', async (iface,changedProps,invalidated) => {
535
+ app.debug(changedProps)
486
536
  if (Object.hasOwn(changedProps,"Powered")){
487
537
  if (changedProps.Powered.value==false) {
488
538
  if (plugin.started){ //only call stop() if plugin is started
489
539
  app.setPluginStatus(`Bluetooth Adapter ${adapterID} turned off. Plugin disabled.`)
490
540
  await plugin.stop()
491
- adapterPower=false
492
541
  }
493
542
  } else {
494
- if (!adapterPower) { //only call start() once
495
- adapterPower=true
496
- await plugin.start(options,restartPlugin)
497
- }
543
+ await restartPlugin(options)
498
544
  }
499
545
  }
500
546
  })
@@ -508,12 +554,13 @@ module.exports = function (app) {
508
554
  }
509
555
  adapterPower=true
510
556
 
511
- plugin.uiSchema.peripherals['ui:disabled']=false
512
557
  sensorMap.clear()
558
+ if (channel)
559
+ channel.broadcast({state:"started"},"pluginstate")
513
560
  deviceConfigs=options?.peripherals??[]
514
561
 
515
562
  if (plugin.stopped) {
516
- await sleep(5000) //Make sure plugin.stop() completes first
563
+ //await sleep(5000) //Make sure plugin.stop() completes first
517
564
  //plugin.start is called asynchronously for some reason
518
565
  //and does not wait for plugin.stop to complete
519
566
  plugin.stopped=false
@@ -526,15 +573,10 @@ module.exports = function (app) {
526
573
  plugin.schema.properties.adapter.enum.push(a.adapter)
527
574
  plugin.schema.properties.adapter.enumNames.push(`${a.adapter} @ ${ await a.getAddress()} (${await a.getName()})`)
528
575
  }
529
-
530
- plugin.uiSchema.adapter={'ui:disabled': (activeAdapters.length==1)}
531
-
532
576
 
533
577
  await startScanner(options.transport)
534
578
  if (starts>0){
535
579
  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
580
  } else {
539
581
  app.debug(`Plugin ${packageInfo.version} started` )
540
582
 
@@ -542,43 +584,84 @@ module.exports = function (app) {
542
584
  starts++
543
585
  if (!await adapter.isDiscovering())
544
586
  try{
545
- await adapter.startDiscovery()
587
+ await startScanner()
546
588
  } catch (e){
547
589
  app.debug(`Error starting scan: ${e.message}`)
548
590
  }
549
591
  if (!(deviceConfigs===undefined)){
550
- var found = 0
551
- for (const deviceConfig of deviceConfigs) {
552
- initConfiguredDevice(deviceConfig)
592
+ const maxTimeout=Math.max(...deviceConfigs.map((dc)=>dc?.discoveryTimeout??options.discoveryTimeout))
593
+ var progress = 0
594
+ if (progressID==null)
595
+ progressID = setInterval(()=>{
596
+ channel.broadcast({"progress":++progress, "maxTimeout": maxTimeout, "deviceCount":foundConfiguredDevices, "totalDevices": deviceConfigs.length},"progress")
597
+ if ( foundConfiguredDevices==deviceConfigs.length){
598
+ app.debug("progress complete")
599
+ progressID,progressTimeoutID = null
600
+ clearTimeout(progressTimeoutID)
601
+ clearInterval(progressID)
602
+ progressID = null
603
+ }
604
+ },1000);
605
+ if (progressTimeoutID==null)
606
+ progressTimeoutID = setTimeout(()=> {
607
+ app.debug("progress timed out ")
608
+ if (progressID) {
609
+
610
+ clearInterval(progressID);
611
+ progressID=null
612
+ channel.broadcast({"progress":maxTimeout, "maxTimeout": maxTimeout, "deviceCount":foundConfiguredDevices, "totalDevices": deviceConfigs.length},"progress")
613
+ }
614
+ }, (maxTimeout+1)*1000);
615
+
616
+ for (const config of deviceConfigs) {
617
+ initConfiguredDevice(config)
553
618
  }
554
619
  }
620
+ const minTimeout=Math.min(...deviceConfigs.map((dc)=>dc?.discoveryTimeout??options.discoveryTimeout))
621
+
622
+ deviceHealthID = setInterval( ()=> {
623
+ sensorMap.forEach((sensor)=>{
624
+ const config = getDeviceConfig(sensor.getMacAddress())
625
+ const dt = config?.discoveryTimeout??options.discoveryTimeout
626
+ if (sensor.elapsedTimeSinceLastContact()> dt)
627
+ channel.broadcast(getSensorInfo(sensor), "sensorchanged")
628
+ })
629
+ }, minTimeout*1000)
630
+
555
631
  if (options.discoveryInterval && !discoveryIntervalID)
556
632
  findDeviceLoop(options.discoveryTimeout, options.discoveryInterval)
633
+
557
634
  }
558
635
  plugin.stop = async function () {
559
636
  app.debug("Stopping plugin")
560
637
  plugin.stopped=true
561
638
  plugin.started=false
562
- plugin.uiSchema.peripherals['ui:disabled']=true
639
+ channel.broadcast({state:"stopped"},"pluginstate")
640
+ if (discoveryIntervalID) {
641
+ clearInterval(discoveryIntervalID)
642
+ discoveryIntervalID=null
643
+ }
644
+ if (progressID) {
645
+ clearInterval(progressID)
646
+ progressID=null
647
+ }
648
+ if (progressTimeoutID) {
649
+ clearTimeout(progressTimeoutID)
650
+ progressTimeoutID=null
651
+ }
652
+
563
653
  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)=> {
654
+ for await (const sensorEntry of sensorMap.entries()) {
567
655
  try{
568
- await sensor.stopListening()
569
- app.debug(`No longer listening to ${mac}`)
656
+ await sensorEntry[1].stopListening()
657
+ app.debug(`No longer listening to ${sensorEntry[0]}`)
570
658
  }
571
659
  catch (e){
572
- app.debug(`Error stopping listening to ${mac}: ${e.message}`)
660
+ app.debug(`Error stopping listening to ${sensorEntry[0]}: ${e.message}`)
573
661
  }
574
- })
575
- }
576
-
577
- if (discoveryIntervalID) {
578
- clearInterval(discoveryIntervalID)
579
- discoveryIntervalID=null
662
+ }
580
663
  }
581
-
664
+ sensorMap.clear()
582
665
  if (adapter && await adapter.isDiscovering())
583
666
  try{
584
667
  await adapter.stopDiscovery()
@@ -586,9 +669,10 @@ module.exports = function (app) {
586
669
  } catch (e){
587
670
  app.debug(`Error stopping scan: ${e.message}`)
588
671
  }
672
+
589
673
  app.debug('BT Sensors plugin stopped')
590
674
 
591
675
  }
592
-
676
+
593
677
  return plugin;
594
678
  }