bt-sensors-plugin-sk 1.1.0 → 1.2.0-beta.0.0.2

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