bt-sensors-plugin-sk 1.1.0-beta.2.2.6 → 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/BTSensor.js +257 -107
- package/README.md +2 -34
- package/definitions.json +941 -0
- package/index.js +401 -317
- package/package.json +51 -6
- package/plugin_defaults.json +57 -0
- package/public/159.js +2 -0
- package/public/159.js.LICENSE.txt +14 -0
- package/public/30.js +8 -0
- package/public/30.js.LICENSE.txt +65 -0
- package/public/540.js +2 -0
- package/public/540.js.LICENSE.txt +8 -0
- package/public/893.js +1 -0
- package/public/main.js +1 -0
- package/public/remoteEntry.js +1 -0
- package/sensor_classes/ATC.js +30 -20
- package/sensor_classes/BlackListedDevice.js +4 -0
- package/sensor_classes/GoveeH510x.js +3 -3
- package/sensor_classes/IBeacon.js +2 -3
- package/sensor_classes/Inkbird.js +4 -4
- package/sensor_classes/MopekaTankSensor.js +13 -6
- package/sensor_classes/Renogy/RenogySensor.js +14 -5
- package/sensor_classes/UNKNOWN.js +8 -4
- package/sensor_classes/Victron/VictronSensor.js +6 -2
- package/sensor_classes/VictronBatteryMonitor.js +8 -6
- package/sensor_classes/VictronDCEnergyMeter.js +1 -1
- package/sensor_classes/VictronInverter.js +1 -1
- package/sensor_classes/VictronInverterRS.js +1 -1
- package/sensor_classes/XiaomiMiBeacon.js +17 -8
- package/spec/electrical.json +688 -0
- package/spec/environment.json +401 -0
- package/spec/sensors.json +39 -0
- package/spec/tanks.json +115 -0
- package/src/components/PluginConfigurationPanel.js +368 -0
- package/src/index.js +0 -0
- package/webpack.config.js +71 -0
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 {
|
|
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.
|
|
19
|
-
this.
|
|
20
|
-
this.
|
|
21
|
-
|
|
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
|
-
|
|
30
|
+
|
|
26
31
|
keys.forEach((key)=>{
|
|
27
|
-
this.
|
|
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.
|
|
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
|
|
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 `
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
297
|
+
};
|
|
300
298
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
plugin.started=false
|
|
348
|
+
function updateSensor(sensor){
|
|
349
|
+
channel.broadcast(getSensorInfo(sensor), "sensorchanged")
|
|
350
|
+
}
|
|
385
351
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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
|
-
|
|
449
|
+
if (startNumber != starts ) {
|
|
450
|
+
return
|
|
451
|
+
}
|
|
405
452
|
if (deviceConfig.active) {
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
587
|
+
await startScanner()
|
|
546
588
|
} catch (e){
|
|
547
589
|
app.debug(`Error starting scan: ${e.message}`)
|
|
548
590
|
}
|
|
549
591
|
if (!(deviceConfigs===undefined)){
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
569
|
-
app.debug(`No longer listening to ${
|
|
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 ${
|
|
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
|
}
|