bt-sensors-plugin-sk 1.2.4-3 → 1.2.5

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 CHANGED
@@ -74,7 +74,7 @@ function preparePath(obj, str) {
74
74
  evalResult= evalResult.call(obj)
75
75
  }
76
76
 
77
- resultString += evalResult !== undefined ? evalResult : `${keyToAccess}_value_undefined`;
77
+ resultString += evalResult !== undefined ? evalResult.replace(/\s+/g,'_') : `${keyToAccess}_value_undefined`;
78
78
  } catch (error) {
79
79
  console.error(`Error accessing key '${keyToAccess}':`, error);
80
80
  resultString += fullMatch; // Keep the original curly braces on error
@@ -405,6 +405,12 @@ class BTSensor extends EventEmitter {
405
405
  if (!param.type)
406
406
  param.type="string"
407
407
 
408
+ if (param.isRequired) {
409
+ if (!Object.hasOwn(this._schema.properties.params, "required"))
410
+ this._schema.properties.params.required=[tag]
411
+ else
412
+ this._schema.properties.params.required.push(tag)
413
+ }
408
414
  this._schema.properties.params.properties[tag]=param
409
415
  return this._schema.properties.params.properties[tag]
410
416
  }
@@ -413,6 +419,13 @@ class BTSensor extends EventEmitter {
413
419
  if (!path.type)
414
420
  path.type="string"
415
421
 
422
+ if (path.isRequired) {
423
+ if (!Object.hasOwn(this._schema.properties.paths, "required"))
424
+ this._schema.properties.paths.required=[tag]
425
+ else
426
+ this._schema.properties.paths.required.push(tag)
427
+ }
428
+
416
429
  if (!path.pattern)
417
430
  path.pattern=//"^(?:[^{}\\s]*\\{[a-zA-Z0-9]+\\}[^{}\\s]*|[^{}\\s]*)$"
418
431
  "^((\\{[a-zA-Z0-9]+\\}|[a-zA-Z0-9]+))(\\.(\\{[a-zA-Z0-9]+\\}|[a-zA-Z0-9]+))*$"
@@ -424,7 +437,13 @@ class BTSensor extends EventEmitter {
424
437
 
425
438
  if (!param.type)
426
439
  param.type="string"
427
-
440
+
441
+ if (param.isRequired) {
442
+ if (!Object.hasOwn(this._schema.properties.gattParams, "required"))
443
+ this._schema.properties.gattParams.required=[tag]
444
+ else
445
+ this._schema.properties.gattParams.required.push(tag)
446
+ }
428
447
  return this._schema.properties.gattParams.properties[tag]=param
429
448
  }
430
449
 
@@ -433,8 +452,10 @@ class BTSensor extends EventEmitter {
433
452
  return this.addPath(tag,Object.assign({}, path))
434
453
  }
435
454
 
436
- addDefaultParam(tag){
437
- return this.addParameter(tag,Object.assign({}, BTSensor.DEFAULTS.params[tag]))
455
+ addDefaultParam(tag,required=false){
456
+ const param = Object.assign({}, BTSensor.DEFAULTS.params[tag])
457
+ param.isRequired=required
458
+ return this.addParameter(tag,param)
438
459
  }
439
460
 
440
461
  getJSONSchema(){
@@ -866,26 +887,29 @@ class BTSensor extends EventEmitter {
866
887
  Object.keys(this.getPaths()).forEach((tag)=>{
867
888
  const pathMeta=this.getPath(tag)
868
889
  const path = config.paths[tag]
869
- if (!(path===undefined))
890
+ if (!(path===undefined)) {
891
+ const preparedPath =
870
892
  this.app.handleMessage(id,
871
893
  {
872
894
  updates:
873
- [{ meta: [{path: preparePath(this, path), value: { units: pathMeta?.unit }}]}]
895
+ [{ meta: [{path: preparePath(this, path), value: { units: pathMeta?.unit }}]}]
874
896
  })
897
+ }
875
898
  })
876
899
  }
877
900
 
878
901
  initPaths(deviceConfig, id){
879
- const source = this.getName()
902
+ const source = `${this.getName()} (bt-sensors-plugin-sk)`
880
903
  Object.keys(this.getPaths()).forEach((tag)=>{
881
904
  const pathMeta=this.getPath(tag)
882
905
  const path = deviceConfig.paths[tag];
883
906
  if (!(path === undefined)) {
907
+ let preparedPath = preparePath(this, path)
884
908
  this.on(tag, (val)=>{
885
909
  if (pathMeta.notify){
886
910
  this.app.notify(tag, val, id )
887
911
  } else {
888
- this.updatePath(preparePath(this,path),val, id, source)
912
+ this.updatePath(preparedPath,val, id, source)
889
913
  }
890
914
  })
891
915
  }
package/README.md CHANGED
@@ -1,6 +1,23 @@
1
1
  # Bluetooth Sensors for [Signal K](http://www.signalk.org)
2
+
2
3
  ## WHAT'S NEW
3
4
 
5
+ # Version 1.2.5
6
+
7
+ - On initial startup, plugin saves default configuration. Fixing the "missing" configured devices after restart.
8
+ - Mopeka Tank Sensor configuration fix
9
+ - Added number of found devices in a domain in the configuration screen's domain tab
10
+
11
+ ## IMPORTANT NOTE TO NEW USERS OF VERSIONS OLDER THAN 1.2.5
12
+
13
+ There's a known issue with saving the configuration after initial installation owing to current ServerAPI limitations.
14
+
15
+ Your device config after you've saved it will appear to be "missing" after restarting. It's in fact saved in the plugin config directory. All you have to do is Submit the main configuration then enable and optionally disable Debug. This, believe it or not, ensures that the config is marked as enabled. You should see your data now and upon restart.
16
+
17
+ # Version 1.2.4-4
18
+
19
+ Junctek support (tested)
20
+
4
21
  # Version 1.2.4-3
5
22
 
6
23
  - Mercury Smartcraft fixes (working now!)
@@ -185,6 +202,11 @@ Poorly shielded USB 3.0 and HDMI (5ghz) can interfere with BT transmission (2.4g
185
202
  - There's no way that I know of to remove a SK Path without restarting the server. So if any active paths are changed by the plugin, you'll still see them hanging around in the data browser growing stale until you restart the server.
186
203
  - RPi 3/4/5/CM400s when running an Access Point on the on board Wifi can cause a problem connecting to devices through the onboard BT. The only known fix is to disable the onboard Bluetooth and use a USB BT adapter. Alternatively, you can use a USB WiFi adapter. NOTE: This only applies to _connected_ devices like Renogy devices, LiTime batteries etc.
187
204
 
205
+ ### Problems saving the configuration after installing plugin for first time (fixed as of Version 1.2.5)
206
+
207
+ Device config after being saved will appear to be "missing" after restarting. The config is in fact saved in the plugin config directory. All you have to do is Submit the main configuration then enable and optionally disable Debug. This, believe it or not, ensures that the config is marked as enabled. You should see your data now and upon restart.
208
+
209
+
188
210
  ## CONFIGURATION
189
211
 
190
212
  After installing and restarting Signalk you should see a "BT Sensors Plugin" option in the Signalk->Server->Plugin Config page.<br><br>
@@ -0,0 +1,170 @@
1
+ {
2
+ "configuration": {
3
+ "discoveryInterval": 10,
4
+ "peripherals": [
5
+ {
6
+ "active": true,
7
+ "discoveryTimeout": 30,
8
+ "params": {
9
+ "name": "ATC_9FF77E",
10
+ "zone": "inside.cabin",
11
+ "parser": "ATC-LE",
12
+ "sensorClass": "ATC"
13
+ },
14
+ "paths": {
15
+ "RSSI": "sensors.{macAndName}.RSSI",
16
+ "batteryStrength": "sensors.{macAndName}.battery.strength",
17
+ "voltage": "sensors.{macAndName}.battery.voltage",
18
+ "temp": "environment.{zone}.temperature",
19
+ "humidity": "environment.{zone}.humidity"
20
+ },
21
+ "mac_address": "A4:C1:38:9F:F7:7E"
22
+ },
23
+ {
24
+ "active": true,
25
+ "discoveryTimeout": 30,
26
+ "params": {
27
+ "name": "tps",
28
+ "zone": "inside.refrigerator",
29
+ "sensorClass": "Inkbird"
30
+ },
31
+ "paths": {
32
+ "RSSI": "sensors.{macAndName}.RSSI",
33
+ "temp": "environment.{zone}.temperature",
34
+ "battery": "sensors.{macAndName}.battery.strength"
35
+ },
36
+ "mac_address": "49:22:05:17:2B:AE"
37
+ },
38
+ {
39
+ "active": true,
40
+ "discoveryTimeout": 30,
41
+ "params": {
42
+ "name": "Ruuvi BC7E",
43
+ "zone": "inside.navdesk",
44
+ "sensorClass": "RuuviTag"
45
+ },
46
+ "paths": {
47
+ "RSSI": "sensors.{macAndName}.RSSI",
48
+ "temp": "environment.{zone}.temperature",
49
+ "humidity": "environment.{zone}.humidity",
50
+ "pressure": "environment.{zone}.pressure",
51
+ "accX": "sensors.{macAndName}.accX",
52
+ "accY": "sensors.{macAndName}.accY",
53
+ "accZ": "sensors.{macAndName}.accZ",
54
+ "battV": "sensors.{macAndName}.battery.voltage",
55
+ "mc": "sensors.{macAndName}.movementCounter",
56
+ "msc": "sensors.{macAndName}.measurementSequenceCounter"
57
+ },
58
+ "mac_address": "F9:B0:6E:63:BC:7E"
59
+ },
60
+ {
61
+ "active": true,
62
+ "discoveryTimeout": 30,
63
+ "params": {
64
+ "name": "SBMO-003Z-db1b",
65
+ "zone": "inside",
66
+ "sensorClass": "ShellySBMO003Z"
67
+ },
68
+ "paths": {
69
+ "RSSI": "sensors.{macAndName}.RSSI",
70
+ "motion": "environment.{zone}.motion",
71
+ "illuminance": "environment.{zone}.illuminance",
72
+ "battery": "sensors.{macAndName}.battery.strength",
73
+ "button": "sensors.{macAndName}.button"
74
+ },
75
+ "mac_address": "E8:E0:7E:97:DB:1B"
76
+ },
77
+ {
78
+ "active": true,
79
+ "discoveryTimeout": 30,
80
+ "params": {
81
+ "name": "LYWSD03MMC",
82
+ "zone": "inside.vberth",
83
+ "encryptionKey": "3985f4ebc032f276cc316f1f6ecea085",
84
+ "sensorClass": "XiaomiMiBeacon"
85
+ },
86
+ "paths": {
87
+ "RSSI": "sensors.{macAndName}.RSSI",
88
+ "temp": "environment.{zone}.temperature",
89
+ "humidity": "environment.{zone}.humidity",
90
+ "batteryStrength": "sensors.{macAndName}.battery.strength",
91
+ "voltage": "sensors.{macAndName}.battery.voltage"
92
+ },
93
+ "gattParams": {
94
+ "useGATT": false
95
+ },
96
+ "mac_address": "A4:C1:38:3E:7E:94"
97
+ },
98
+ {
99
+ "active": true,
100
+ "discoveryTimeout": 30,
101
+ "params": {
102
+ "name": "SmartShunt HQ2204C2GHD",
103
+ "batteryID": "house",
104
+ "encryptionKey": "8cce8529307cf9dd0c85611c4fef42d9",
105
+ "sensorClass": "VictronBatteryMonitor"
106
+ },
107
+ "paths": {
108
+ "RSSI": "sensors.{macAndName}.RSSI",
109
+ "current": "electrical.batteries.{batteryID}.current",
110
+ "power": "electrical.batteries.{batteryID}.power",
111
+ "voltage": "electrical.batteries.{batteryID}.voltage",
112
+ "alarm": "electrical.batteries.{batteryID}.alarm",
113
+ "consumed": "electrical.batteries.{batteryID}.capacity.ampHoursConsumed",
114
+ "soc": "electrical.batteries.{batteryID}.capacity.stateOfCharge",
115
+ "ttg": "electrical.batteries.{batteryID}.capacity.timeRemaining",
116
+ "starterVoltage": "electrical.batteries.secondary.voltage"
117
+ },
118
+ "gattParams": {
119
+ "useGATT": false
120
+ },
121
+ "mac_address": "D4:50:46:39:38:C5"
122
+ },
123
+ {
124
+ "active": true,
125
+ "discoveryTimeout": 90,
126
+ "params": {
127
+ "name": "aft propane tank",
128
+ "medium": "PROPANE",
129
+ "tankHeight": "325",
130
+ "id": "propane",
131
+ "sensorClass": "MopekaTankSensor"
132
+ },
133
+ "paths": {
134
+ "RSSI": "sensors.{macAndName}.RSSI",
135
+ "battVolt": "sensors.{macAndName}.battery.voltage",
136
+ "battStrength": "sensors.{macAndName}.battery.strength",
137
+ "temp": "tanks.{id}.temperature",
138
+ "tankLevel": "tanks.{id}.currentLevel",
139
+ "readingQuality": "sensors.{macAndName}.readingQuality",
140
+ "accX": "sensors.{macAndName}.accelerationXAxis",
141
+ "accY": "sensors.{macAndName}.accelerationYAxis"
142
+ },
143
+ "mac_address": "D4:40:59:BE:2A:8C"
144
+ },
145
+ {
146
+ "active": true,
147
+ "discoveryTimeout": 30,
148
+ "params": {
149
+ "name": "SBHT-003C",
150
+ "zone": "outside.deck",
151
+ "sensorClass": "ShellySBHT003C"
152
+ },
153
+ "paths": {
154
+ "RSSI": "sensors.{macAndName}.RSSI",
155
+ "battery": "sensors.{macAndName}.battery.strength",
156
+ "temp": "environment.{zone}.temperature",
157
+ "humidity": "environment.{zone}.humidity",
158
+ "button": "sensors.{macAndName}.button"
159
+ },
160
+ "mac_address": "7C:C6:B6:AF:49:5F"
161
+ }
162
+ ],
163
+ "adapter": "hci0",
164
+ "transport": "le",
165
+ "duplicateData": false,
166
+ "discoveryTimeout": 30
167
+ },
168
+ "enabled": true,
169
+ "enableDebug": false
170
+ }
package/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const packageInfo = require("./package.json")
2
2
 
3
+
3
4
  const {createBluetooth} = require('node-ble')
4
5
  const {Variant} = require('dbus-next')
5
6
  const {bluetooth, destroy} = createBluetooth()
@@ -138,6 +139,21 @@ module.exports = function (app) {
138
139
  var adapterID=options.adapter
139
140
  var foundConfiguredDevices=0
140
141
 
142
+ if (Object.keys(options).length==0){ //empty config means initial startup. save defaults and enabled=true.
143
+ let json = {configuration:{adapter:"hci0", transport:"le", discoveryTimeout:30, discoveryInterval:10}, enabled:true, enableDebug:false}
144
+ let appDataDirPath = app.getDataDirPath()
145
+ let jsonFile = appDataDirPath+'.json'
146
+ const fs = require("node:fs")
147
+ try {
148
+ fs.writeFileSync(jsonFile, JSON.stringify(json, null,2))
149
+ options=json
150
+ } catch(err){
151
+ console.log(`Error writing initial config: ${err.message} `)
152
+ console.log(err)
153
+ }
154
+
155
+ }
156
+
141
157
  plugin.registerWithRouter = function(router) {
142
158
 
143
159
  router.post('/updateSensorData', async (req, res) => {
@@ -154,6 +170,7 @@ module.exports = function (app) {
154
170
  } else {
155
171
  options.peripherals[i] = req.body
156
172
  }
173
+ deviceConfigs=options.peripherals
157
174
  app.savePluginOptions(
158
175
  options, async () => {
159
176
  app.debug('Plugin options saved')
@@ -163,9 +180,8 @@ module.exports = function (app) {
163
180
  removeSensorFromList(sensor)
164
181
  if (sensor.isActive())
165
182
  await sensor.stopListening()
166
- initConfiguredDevice(req.body)
167
183
  }
168
-
184
+ initConfiguredDevice(req.body)
169
185
  }
170
186
  )
171
187
 
@@ -240,9 +256,10 @@ module.exports = function (app) {
240
256
 
241
257
  router.get('/getProgress', (req, res) => {
242
258
  app.debug("Sending progress")
243
- const json = {"progress":foundConfiguredDevices/deviceConfigs.length, "maxTimeout": 1,
259
+ let deviceCount = deviceConfigs.filter((dc)=>dc.active).length
260
+ const json = {"progress":foundConfiguredDevices/deviceCount, "maxTimeout": 1,
244
261
  "deviceCount":foundConfiguredDevices,
245
- "totalDevices": deviceConfigs.length}
262
+ "totalDevices": deviceCount}
246
263
  res.status(200).json(json)
247
264
 
248
265
  });
@@ -576,11 +593,13 @@ module.exports = function (app) {
576
593
  }
577
594
  if (!(deviceConfigs===undefined)){
578
595
  const maxTimeout=Math.max(...deviceConfigs.map((dc)=>dc?.discoveryTimeout??options.discoveryTimeout))
596
+ const totalDevices = deviceConfigs.filter((dc)=>dc.active).length
597
+
579
598
  var progress = 0
580
599
  if (progressID==null)
581
600
  progressID = setInterval(()=>{
582
- channel.broadcast({"progress":++progress, "maxTimeout": maxTimeout, "deviceCount":foundConfiguredDevices, "totalDevices": deviceConfigs.length},"progress")
583
- if ( foundConfiguredDevices==deviceConfigs.length){
601
+ channel.broadcast({"progress":++progress, "maxTimeout": maxTimeout, "deviceCount":foundConfiguredDevices, "totalDevices": totalDevices},"progress")
602
+ if ( foundConfiguredDevices==totalDevices){
584
603
  progressID,progressTimeoutID = null
585
604
  clearTimeout(progressTimeoutID)
586
605
  clearInterval(progressID)
@@ -593,7 +612,7 @@ module.exports = function (app) {
593
612
 
594
613
  clearInterval(progressID);
595
614
  progressID=null
596
- channel.broadcast({"progress":maxTimeout, "maxTimeout": maxTimeout, "deviceCount":foundConfiguredDevices, "totalDevices": deviceConfigs.length},"progress")
615
+ channel.broadcast({"progress":maxTimeout, "maxTimeout": maxTimeout, "deviceCount":foundConfiguredDevices, "totalDevices": totalDevices},"progress")
597
616
  }
598
617
  }, (maxTimeout+1)*1000);
599
618
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bt-sensors-plugin-sk",
3
- "version": "1.2.4-3",
3
+ "version": "1.2.5",
4
4
  "description": "Bluetooth Sensors for Signalk - see https://www.npmjs.com/package/bt-sensors-plugin-sk#supported-sensors for a list of supported sensors",
5
5
  "main": "index.js",
6
6
  "dependencies": {
package/public/847.js CHANGED
@@ -1 +1 @@
1
- (self.webpackChunkbt_sensors_plugin_sk=self.webpackChunkbt_sensors_plugin_sk||[]).push([[847],{9337:()=>{},62995:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>k});var a=n(73490),o=n(74810),s=n(86528),r=n.n(s),l=n(40334),c=n(27606),i=n(86452),u=n(82096),g=n(3768),d=n(71431),m=n(39676),f=n(47041),h=n(86038),p=n(95027),E=n(43540),w=n(38250),y=n(31008),v=n(20455),S=n(23399);const b=e=>console.log.bind(console,e);var A,D={};function k(e){const t=(0,u.A)((e=>({root:{"& > *":{margin:e.spacing(1)}}}))),[n,k]=(0,s.useState)({}),[C,_]=(0,s.useState)({}),[x,$]=(0,s.useState)({}),[T,N]=(0,s.useState)({"ui:options":{label:!1},title:{"ui:widget":"hidden"}}),[j,M]=(0,s.useState)([]),[O,U]=(0,s.useState)(),[L,J]=(0,s.useState)(new Map),[B,I]=(0,s.useState)({progress:0,maxTimeout:100,deviceCount:0,totalDevices:0}),[P,R]=(0,s.useState)("unknown"),[G,H]=(0,s.useState)(),K=t();function F(e,t){console.log(`sending ${e}`),console.log(t);const n=new Headers;return n.append("Content-Type","application/json"),fetch(`/plugins/bt-sensors-plugin-sk/${e}`,{credentials:"include",method:"POST",body:JSON.stringify(t),headers:n})}async function W(e){var t;console.log(`fetching ${e}`);try{t=fetch(`/plugins/bt-sensors-plugin-sk/${e}`,{credentials:"include"})}catch(e){t={status:500,statusText:e.toString()}}return t}function Y(e){return Object.keys(e.config).length>0}function q(e){const t=Y(e);return r().createElement(v.A,{action:!0,onClick:()=>{e.config.mac_address=e.info.mac,$(e.schema),U(e.config)}},r().createElement("div",{class:"d-flex justify-content-between align-items-center",style:t?{fontWeight:"normal"}:{fontStyle:"italic"}},`${e._changesMade?"*":""}${e.info.name} MAC: ${e.info.mac} RSSI: ${n=e.info.RSSI,null==n?NaN:n}`,r().createElement("div",{class:"d-flex justify-content-between "},function(e){return null==e.info.lastContactDelta||e.info.lastContactDelta>e.config.discoveryTimeout?r().createElement(g.A,null):e.info.signalStrength>80?r().createElement(d.A,null):e.info.signalStrength>60?r().createElement(m.A,null):e.info.signalStrength>40?r().createElement(f.A,null):e.info.signalStrength>20?r().createElement(h.A,null):r().createElement(p.A,null)}(e))));var n}function z(e){window.open(e,"_blank","noreferrer")}return(0,s.useEffect)((()=>{console.log("useEffect([])");let e=null;return W("getPluginState").then((async t=>{if(404==t.status)throw R("unknown"),new Error("unable to get plugin state");const n=await t.json();console.log("Setting up eventsource"),e=new EventSource("/plugins/bt-sensors-plugin-sk/sse",{withCredentials:!0}),R(n.state),await async function(){console.log("getDomains");const e=await W("getDomains");if(200!=e.status)throw new Error(`Unable get domain data: ${e.statusText} (${e.status}) `);const t=await e.json();return console.log(t),t}(),e.addEventListener("newsensor",(e=>{console.log("newsensor");let t=JSON.parse(e.data);A.has(t.info.mac)||(console.log(`New sensor: ${t.info.mac}`),J(new Map(A.set(t.info.mac,t))))})),e.addEventListener("sensorchanged",(e=>{let t=JSON.parse(e.data);if(console.log("sensorchanged"),console.log(t),A.has(t.mac)){let e=A.get(t.mac);Object.assign(e.info,t),J(new Map(A))}})),e.addEventListener("progress",(e=>{console.log("progress");const t=JSON.parse(e.data);I(t),console.log(t)})),e.addEventListener("pluginstate",(e=>{console.log("pluginstate");const t=JSON.parse(e.data);R(t.state)}))})).catch((e=>{H(e)})),()=>{console.log("Closing connection to SSE"),e.close()}}),[]),(0,s.useEffect)((()=>{console.log("useEffect([pluginState])"),"started"==P?(console.log("refreshing sensor map"),async function(){console.log("getSensors");const e=await W("getSensors");if(200!=e.status)throw new Error(`Unable get sensor data: ${e.statusText} (${e.status}) `);const t=await e.json();return console.log(t),t}().then((e=>{J(new Map(e.map((e=>[e.info.mac,e]))))})).catch((e=>{H(e)})),async function(){console.log("getBaseData");const e=await W("getBaseData");if(200!=e.status)throw new Error(`Unable to get base data: ${e.statusText} (${e.status}) `);const t=await e.json();return console.log(t),t.schema.htmlDescription=r().createElement("div",null,(0,l.Ay)(t.schema.htmlDescription),r().createElement("p",null)),t}().then((e=>{k(e.schema),_(e.data)})).catch((e=>{H(e)})),async function(){console.log("getProgress");const e=await W("getProgress");if(200!=e.status)throw new Error(`Unable to get progress: ${e.statusText} (${e.status}) `);const t=await e.json();return console.log(t),t}().then((e=>{I(e)})).catch((e=>{H(e)}))):(J(new Map),k({}),_({}))}),[P]),(0,s.useEffect)((()=>{console.log("useEffect([sensorMap])"),A=L;const e=new Set(L.entries().map((e=>e[1].info.domain))),t={_configured:Array.from(L.entries()).filter((e=>Y(e[1]))).map((e=>q(L.get(e[0]))))};e.forEach((e=>{var n;t[e]=(n=e,Array.from(L.entries()).filter((e=>e[1].info.domain===n))).map((e=>q(L.get(e[0]))))})),D=t}),[L]),"stopped"==P||"unknown"==P?r().createElement("h3",null,"Enable plugin to see configuration"):r().createElement("div",null,r().createElement("div",{className:K.root},r().createElement(i.A,{variant:"contained",onClick:()=>{z("https://github.com/naugehyde/bt-sensors-plugin-sk/tree/1.2.0-beta#configuration")}},"Documentation"),r().createElement(i.A,{variant:"contained",onClick:()=>{z("https://github.com/naugehyde/bt-sensors-plugin-sk/issues/new/choose")}},"Report Issue"),r().createElement(i.A,{variant:"contained",onClick:()=>{z("https://discord.com/channels/1170433917761892493/1295425963466952725")}},"Discord Thread"),r().createElement("p",null),r().createElement("p",null)),r().createElement("hr",{style:{width:"100%",height:"1px",color:"gray","background-color":"gray","text-align":"left","margin-left":0}}),G?r().createElement("h2",{style:"color: red;"},G):"",r().createElement(a.Ay,{schema:n,validator:o.Ay,uiSchema:{"ui:field":"LayoutGridField","ui:layoutGrid":{"ui:row":[{"ui:row":{className:"row",children:[{"ui:columns":{className:"col-xs-4",children:["adapter","transport","duplicateData","discoveryTimeout","discoveryInterval"]}}]}}]}},onChange:e=>_(e.formData),onSubmit:({formData:e},t)=>{var n;n=e,console.log("updateBaseData"),F("updateBaseData",n).then((e=>{200!=e.status&&H(new Error(`Unable to update base data: ${e.statusText} (${e.status})`))})),$({})},onError:b("errors"),formData:C}),r().createElement("hr",{style:{width:"100%",height:"1px",color:"gray","background-color":"gray","text-align":"left","margin-left":0}}),r().createElement("p",null),r().createElement("p",null),B.deviceCount<B.totalDevices?r().createElement(S.A,{max:B.maxTimeout,now:B.progress}):"",r().createElement("p",null),r().createElement(w.A,{defaultActiveKey:"_configured",id:"domain-tabs",className:"mb-3",onClick:()=>{$({})}},Object.keys(D).map((e=>{return n=(t=e).slice("_"===t.charAt(0)?1:0),r().createElement(y.A,{eventKey:t,title:n.charAt(0).toUpperCase()+n.slice(1)},r().createElement(E.A,{style:{maxHeight:"300px",overflowY:"auto"}},function(e){return D[e]}(t)));var t,n}))),r().createElement("div",{style:{paddingLeft:10,paddingTop:10,display:0==Object.keys(x).length?"none":""}},r().createElement(c.A,{container:!0,direction:"column",style:{spacing:5}},r().createElement(c.A,{item:!0},r().createElement("h2",null,x?.title),r().createElement("p",null)),r().createElement(c.A,{item:!0},(0,l.Ay)(x?.htmlDescription))),r().createElement(a.Ay,{schema:x,validator:o.Ay,uiSchema:T,onChange:e=>{L.get(e.formData.mac_address)._changesMade=!0,U(e.formData)},onSubmit:({formData:e},t)=>{var n;n=e,console.log("updateSensorData"),F("updateSensorData",n).then((e=>{if(200!=e.status)throw new Error(e.statusText);L.get(n.mac_address)._changesMade=!1,L.get(n.mac_address).config=n})),$({}),alert("Changes saved")},onError:b("errors"),formData:O},r().createElement("div",{className:K.root},r().createElement(i.A,{type:"submit",color:"primary",variant:"contained"},"Save"),r().createElement(i.A,{variant:"contained",onClick:()=>{var e;e=O.mac_address,console.log("undoChanges"),L.get(e)._changesMade=!1,U(L.get(e).config)}},"Undo"),r().createElement(i.A,{variant:"contained",color:"secondary",onClick:e=>{return t=O.mac_address,void(window.confirm(`Delete configuration for ${t}?`)&&function(e){console.log("removeSensorData");try{F("removeSensorData",{mac_address:e}).then((e=>{if(200!=e.status)throw new Error(e.statusText)})),A.delete(e),J(new Map(A)),$({})}catch{}}(t));var t}},"Delete")))))}}}]);
1
+ (self.webpackChunkbt_sensors_plugin_sk=self.webpackChunkbt_sensors_plugin_sk||[]).push([[847],{9337:()=>{},62995:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>k});var a=n(73490),o=n(74810),s=n(86528),r=n.n(s),l=n(40334),c=n(27606),i=n(86452),u=n(82096),g=n(3768),d=n(71431),m=n(39676),f=n(47041),h=n(86038),p=n(95027),E=n(43540),w=n(38250),y=n(31008),v=n(20455),S=n(23399);const b=e=>console.log.bind(console,e);var A,D={};function k(e){const t=(0,u.A)((e=>({root:{"& > *":{margin:e.spacing(1)}}}))),[n,k]=(0,s.useState)({}),[$,C]=(0,s.useState)({}),[_,x]=(0,s.useState)({}),[T,N]=(0,s.useState)({"ui:options":{label:!1},title:{"ui:widget":"hidden"}}),[j,M]=(0,s.useState)([]),[O,U]=(0,s.useState)(),[L,J]=(0,s.useState)(new Map),[B,I]=(0,s.useState)({progress:0,maxTimeout:100,deviceCount:0,totalDevices:0}),[P,R]=(0,s.useState)("unknown"),[G,H]=(0,s.useState)(),K=t();function F(e,t){console.log(`sending ${e}`),console.log(t);const n=new Headers;return n.append("Content-Type","application/json"),fetch(`/plugins/bt-sensors-plugin-sk/${e}`,{credentials:"include",method:"POST",body:JSON.stringify(t),headers:n})}async function W(e){var t;console.log(`fetching ${e}`);try{t=fetch(`/plugins/bt-sensors-plugin-sk/${e}`,{credentials:"include"})}catch(e){t={status:500,statusText:e.toString()}}return t}function Y(e){return Object.keys(e.config).length>0}function q(e){const t=Y(e);return r().createElement(v.A,{action:!0,onClick:()=>{e.config.mac_address=e.info.mac,x(e.schema),U(e.config)}},r().createElement("div",{class:"d-flex justify-content-between align-items-center",style:t?{fontWeight:"normal"}:{fontStyle:"italic"}},`${e._changesMade?"*":""}${e.info.name} MAC: ${e.info.mac} RSSI: ${n=e.info.RSSI,null==n?NaN:n}`,r().createElement("div",{class:"d-flex justify-content-between "},function(e){return null==e.info.lastContactDelta||e.info.lastContactDelta>e.config.discoveryTimeout?r().createElement(g.A,null):e.info.signalStrength>80?r().createElement(d.A,null):e.info.signalStrength>60?r().createElement(m.A,null):e.info.signalStrength>40?r().createElement(f.A,null):e.info.signalStrength>20?r().createElement(h.A,null):r().createElement(p.A,null)}(e))));var n}function z(){return Array.from(L.entries()).filter((e=>Y(e[1])))}function Q(e){return"_configured"===e?z():Array.from(L.entries()).filter((t=>t[1].info.domain===e))}function V(e){window.open(e,"_blank","noreferrer")}return(0,s.useEffect)((()=>{console.log("useEffect([])");let e=null;return W("getPluginState").then((async t=>{if(404==t.status)throw R("unknown"),new Error("unable to get plugin state");const n=await t.json();console.log("Setting up eventsource"),e=new EventSource("/plugins/bt-sensors-plugin-sk/sse",{withCredentials:!0}),R(n.state),await async function(){console.log("getDomains");const e=await W("getDomains");if(200!=e.status)throw new Error(`Unable get domain data: ${e.statusText} (${e.status}) `);const t=await e.json();return console.log(t),t}(),e.addEventListener("newsensor",(e=>{console.log("newsensor");let t=JSON.parse(e.data);A.has(t.info.mac)||(console.log(`New sensor: ${t.info.mac}`),J(new Map(A.set(t.info.mac,t))))})),e.addEventListener("sensorchanged",(e=>{let t=JSON.parse(e.data);if(console.log("sensorchanged"),console.log(t),A.has(t.mac)){let e=A.get(t.mac);Object.assign(e.info,t),J(new Map(A))}})),e.addEventListener("progress",(e=>{console.log("progress");const t=JSON.parse(e.data);I(t),console.log(t)})),e.addEventListener("pluginstate",(e=>{console.log("pluginstate");const t=JSON.parse(e.data);R(t.state)}))})).catch((e=>{H(e)})),()=>{console.log("Closing connection to SSE"),e.close()}}),[]),(0,s.useEffect)((()=>{console.log("useEffect([pluginState])"),"started"==P?(console.log("refreshing sensor map"),async function(){console.log("getSensors");const e=await W("getSensors");if(200!=e.status)throw new Error(`Unable get sensor data: ${e.statusText} (${e.status}) `);const t=await e.json();return console.log(t),t}().then((e=>{J(new Map(e.map((e=>[e.info.mac,e]))))})).catch((e=>{H(e)})),async function(){console.log("getBaseData");const e=await W("getBaseData");if(200!=e.status)throw new Error(`Unable to get base data: ${e.statusText} (${e.status}) `);const t=await e.json();return console.log(t),t.schema.htmlDescription=r().createElement("div",null,(0,l.Ay)(t.schema.htmlDescription),r().createElement("p",null)),t}().then((e=>{k(e.schema),C(e.data)})).catch((e=>{H(e)})),async function(){console.log("getProgress");const e=await W("getProgress");if(200!=e.status)throw new Error(`Unable to get progress: ${e.statusText} (${e.status}) `);const t=await e.json();return console.log(t),t}().then((e=>{I(e)})).catch((e=>{H(e)}))):(J(new Map),k({}),C({}))}),[P]),(0,s.useEffect)((()=>{console.log("useEffect([sensorMap])"),A=L;const e=[...new Set(L.entries().map((e=>e[1].info.domain)))].sort(),t={_configured:0==z().length?"Select a device from its domain tab (Electrical etc.) and configure it.":z().map((e=>q(L.get(e[0]))))};e.forEach((e=>{t[e]=Q(e).map((e=>q(L.get(e[0]))))})),D=t}),[L]),"stopped"==P||"unknown"==P?r().createElement("h3",null,"Enable plugin to see configuration"):r().createElement("div",null,r().createElement("div",{className:K.root},r().createElement(i.A,{variant:"contained",onClick:()=>{V("https://github.com/naugehyde/bt-sensors-plugin-sk/tree/1.2.0-beta#configuration")}},"Documentation"),r().createElement(i.A,{variant:"contained",onClick:()=>{V("https://github.com/naugehyde/bt-sensors-plugin-sk/issues/new/choose")}},"Report Issue"),r().createElement(i.A,{variant:"contained",onClick:()=>{V("https://discord.com/channels/1170433917761892493/1295425963466952725")}},"Discord Thread"),r().createElement("p",null),r().createElement("p",null)),r().createElement("hr",{style:{width:"100%",height:"1px",color:"gray","background-color":"gray","text-align":"left","margin-left":0}}),G?r().createElement("h2",{style:"color: red;"},G):"",r().createElement(a.Ay,{schema:n,validator:o.Ay,uiSchema:{"ui:field":"LayoutGridField","ui:layoutGrid":{"ui:row":[{"ui:row":{className:"row",children:[{"ui:columns":{className:"col-xs-4",children:["adapter","transport","duplicateData","discoveryTimeout","discoveryInterval"]}}]}}]}},onChange:e=>C(e.formData),onSubmit:({formData:e},t)=>{var n;n=e,console.log("updateBaseData"),F("updateBaseData",n).then((e=>{200!=e.status&&H(new Error(`Unable to update base data: ${e.statusText} (${e.status})`))})),x({})},onError:b("errors"),formData:$}),r().createElement("hr",{style:{width:"100%",height:"1px",color:"gray","background-color":"gray","text-align":"left","margin-left":0}}),r().createElement("p",null),r().createElement("p",null),B.deviceCount<B.totalDevices?r().createElement(S.A,{max:B.maxTimeout,now:B.progress}):"",r().createElement("p",null),r().createElement(w.A,{defaultActiveKey:"_configured",id:"domain-tabs",className:"mb-3",onClick:()=>{x({})}},Object.keys(D).map((e=>function(e){var t=e.slice("_"===e.charAt(0)?1:0);let n=function(e){return D[e]}(e);return r().createElement(y.A,{eventKey:e,title:`${t.charAt(0).toUpperCase()}${t.slice(1)}${0==Q(e).length?"":" ("+Q(e).length+")"}`},r().createElement(E.A,{style:{maxHeight:"300px",overflowY:"auto"}},n))}(e)))),r().createElement("div",{style:{paddingLeft:10,paddingTop:10,display:0==Object.keys(_).length?"none":""}},r().createElement(c.A,{container:!0,direction:"column",style:{spacing:5}},r().createElement(c.A,{item:!0},r().createElement("h2",null,_?.title),r().createElement("p",null)),r().createElement(c.A,{item:!0},(0,l.Ay)(_?.htmlDescription))),r().createElement(a.Ay,{schema:_,validator:o.Ay,uiSchema:T,onChange:e=>{L.get(e.formData.mac_address)._changesMade=!0,U(e.formData)},onSubmit:({formData:e},t)=>{var n;n=e,console.log("updateSensorData"),F("updateSensorData",n).then((e=>{if(200!=e.status)throw new Error(e.statusText);L.get(n.mac_address)._changesMade=!1,L.get(n.mac_address).config=n})),x({}),alert("Changes saved")},onError:b("errors"),formData:O},r().createElement("div",{className:K.root},r().createElement(i.A,{type:"submit",color:"primary",variant:"contained"},"Save"),r().createElement(i.A,{variant:"contained",onClick:()=>{var e;e=O.mac_address,console.log("undoChanges"),L.get(e)._changesMade=!1,U(L.get(e).config)}},"Undo"),r().createElement(i.A,{variant:"contained",color:"secondary",onClick:e=>{return t=O.mac_address,void(window.confirm(`Delete configuration for ${t}?`)&&function(e){console.log("removeSensorData");try{F("removeSensorData",{mac_address:e}).then((e=>{if(200!=e.status)throw new Error(e.statusText)})),A.delete(e),J(new Map(A)),x({})}catch{}}(t));var t}},"Delete")))))}}}]);
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE html>
2
+ <meta charset="utf-8">
3
+ <style>
4
+ .line {
5
+ fill: none;
6
+ stroke: steelblue;
7
+ stroke-width: 20px;
8
+ }
9
+ </style>
10
+
11
+ <body>
12
+ <h1>Bluetooth Sensors for SignalK</h1>
13
+
14
+ <script>
15
+
16
+ var margin = { top: 20, right: 20, bottom: 30, left: 50 },
17
+ width = 960 - margin.left - margin.right,
18
+ height = 300 - margin.top - margin.bottom;
19
+
20
+
21
+
22
+ </script>
23
+ <script src="main.js"></script><script src="remoteEntry.js"></script></body>
@@ -235,7 +235,7 @@ class AbstractBTHomeSensor extends BTSensor {
235
235
  * Extracts motion detection from the given BTHome data.
236
236
  *
237
237
  * @param btHomeData {BTHomeServiceData.BthomeServiceData} The BTHome data provided by the device.
238
- * @returns {Boolean|null} The device's button press state.
238
+ * @returns {Boolean|null} Wether the device has detected motion.
239
239
  */
240
240
  parseMotion(btHomeData) {
241
241
  const motion = this.getSensorDataByObjectId(
@@ -304,6 +304,31 @@ class AbstractBTHomeSensor extends BTSensor {
304
304
  return null;
305
305
  }
306
306
 
307
+ /**
308
+ * Parses the opening (window/door) state from BTHome service data.
309
+ * @param {BTHomeServiceData.BthomeServiceData} btHomeData - The parsed BTHome data object.
310
+ * @returns {string|null} "open", "closed", or null if not present.
311
+ */
312
+ parseWindowState(btHomeData) {
313
+ const state = this.getSensorDataByObjectId(btHomeData, BTHomeServiceData.BthomeObjectId.BINARY_WINDOW)?.window;
314
+ if (state.intValue === 1) return "open";
315
+ if (state.intValue === 0) return "closed";
316
+ return null;
317
+ }
318
+
319
+ /**
320
+ * Parses the rotation from BTHome service data.
321
+ * @param {BTHomeServiceData.BthomeServiceData} btHomeData - The parsed BTHome data object.
322
+ * @returns {number|null} rotation in degrees from the closed position, or null if not present.
323
+ */
324
+ parseRotation(btHomeData) {
325
+ // Use the correct object ID for window event (0x2D, which is 45 in decimal, which is already specified in BthomeObjectId).
326
+ const sensorRotation = this.getSensorDataByObjectId(btHomeData, BTHomeServiceData.BthomeObjectId.SENSOR_ROTATION_0_1)?.rotation;
327
+ if (sensorRotation) {
328
+ return Number.parseFloat(sensorRotation.toFixed(2));
329
+ }
330
+ return null;
331
+ }
307
332
 
308
333
  propertiesChanged(props) {
309
334
  super.propertiesChanged(props);
@@ -5,7 +5,7 @@ const BTSensor = require("../BTSensor");
5
5
  function bytesToBase10String(bytes){
6
6
  let s = ""
7
7
  for (let byte of bytes){
8
- s+=byte.toString(16)
8
+ s+=byte.toString(16).padStart(2,'0')
9
9
  }
10
10
  return s
11
11
  }
@@ -22,6 +22,8 @@ class JunctekBMS extends BTSensor{
22
22
  return null
23
23
  }
24
24
 
25
+ chargeDirection = 1
26
+
25
27
  hasGATT(){
26
28
  return true
27
29
  }
@@ -47,12 +49,14 @@ class JunctekBMS extends BTSensor{
47
49
  this.addDefaultPath("charge",'electrical.batteries.capacity.charge')
48
50
  this.addDefaultPath("temperature",'electrical.batteries.temperature')
49
51
  this.addDefaultPath("actualCapacity",'electrical.batteries.capacity.actual')
52
+ this.addMetadatum('timeToCharged','s', 'time in seconds to battery is fully charged')
53
+ .default='electrical.batteries.{batteryID}.timeToCharged'
50
54
  this.addMetadatum('impedance','mOhm', 'measured resistance')
51
55
  .default='electrical.batteries.{batteryID}.impedance'
52
56
  }
53
57
 
54
58
  emitFrom(buffer){
55
- var value=[], chargeDirection = 1
59
+ let value=[], emitObject={}
56
60
  this.debug(buffer)
57
61
  for (let byte of buffer){
58
62
  if (byte==0xBB) {
@@ -65,45 +69,62 @@ class JunctekBMS extends BTSensor{
65
69
 
66
70
  if (isNaN(parseInt(byte.toString(16)))){ //not a base-10 number. seriously. that's how Junctek does this.
67
71
  const v = parseInt(bytesToBase10String(value))
72
+ //if (byte!==0xd5)
73
+ // this.debug(`0x${(byte).toString(16)}: ${(value).map((v)=>'0x'+v.toString(16))} (${v})`)
68
74
  value=[]
69
75
  switch (byte){
70
76
  case 0xC0:
71
- this.emit("voltage",v/100)
77
+ emitObject["voltage"]=v/100
72
78
  break
73
79
 
74
80
  case 0xC1:
75
- this.emit("current",(v/100)*chargeDirection)
81
+ emitObject["current"]=()=>{return (v/100)*this.chargeDirection}
76
82
  break
77
83
 
78
84
  case 0xD1:
79
- if (byte==0)
80
- chargeDirection=-1
85
+ this.debug(v)
86
+ if (v==0)
87
+ this.chargeDirection=-1
88
+ else
89
+ this.chargeDirection= 1
90
+ this.debug(this.chargeDirection)
81
91
  break
82
92
 
83
93
  case 0xD2:
84
- this.emit("remaining",v*3.6)
94
+
95
+ emitObject["remaining"]=v*3.6
85
96
  break
86
97
 
87
98
  case 0xD3:
88
- this.emit("discharge",v/100000)
99
+ emitObject["discharge"]= v/100000
89
100
  break
90
101
  case 0xD4:
91
- this.emit("charge",v/100000)
102
+ emitObject["charge"]=v/100000
92
103
  break
93
104
  case 0xD6:
94
- this.emit("timeRemaining",v*60)
105
+ if (chargeDirection==-1){
106
+ emitObject["timeToCharged"] = NaN
107
+ emitObject["timeRemaining"] = v*60
108
+ }
109
+ else {
110
+ emitObject["timeRemaining"] = NaN
111
+ emitObject["timeToCharged"] = v*60
112
+ }
95
113
  break
96
114
  case 0xD7:
97
- this.emit("impedance",v/100)
115
+ emitObject["impedance"]=v/100
98
116
  break
99
117
  case 0xD8:
100
- this.emit("power",(v/100)*chargeDirection)
118
+ emitObject["power"]=()=>{
119
+ this.debug(this.chargeDirection)
120
+ return (v/100)*this.chargeDirection
121
+ }
101
122
  break
102
123
  case 0xD9:
103
- this.emit("temperature",v + 273.15) //assume C not F
124
+ emitObject["temperature"]=v + 173.15 //assume C not F -- raw value is c - 100
104
125
  break
105
126
  case 0xB1:
106
- this.emit("capacityActual",v /10 )
127
+ emitObject["capacityActual"]=v /10
107
128
  break
108
129
  }
109
130
  }
@@ -111,12 +132,16 @@ class JunctekBMS extends BTSensor{
111
132
  value.push(byte)
112
133
  }
113
134
  }
135
+ for (const [key, value] of Object.entries(emitObject)) {
136
+ this.emit(key,value instanceof Function?value():value)
137
+ }
138
+ emitObject = {}
114
139
  }
115
140
  emitGATT(){
116
- this.battCharacteristic.readValue()
141
+ /*this.battCharacteristic.readValue()
117
142
  .then((buffer)=>{
118
143
  this.emitFrom(buffer)
119
- })
144
+ })*/
120
145
  }
121
146
  initGATTConnection(){
122
147
  return new Promise((resolve,reject )=>{ this.deviceConnect().then(async ()=>{
@@ -45,10 +45,10 @@ class MercurySmartcraft extends BTSensor{
45
45
  "id",
46
46
  {
47
47
  "title": "Engine ID",
48
- "examples": ["port","starboard","p0","p1"]
48
+ "examples": ["port","starboard","p0","p1"],
49
+ "isRequired": true
49
50
  }
50
51
  )
51
- this._schema.properties.params.required=["id"]
52
52
 
53
53
  this.addMetadatum("rpm","Hz","engine revolutions per sec",
54
54
  (buffer)=>{return buffer.readUInt16LE(bo)/60}
@@ -280,17 +280,19 @@ class MopekaTankSensor extends BTSensor{
280
280
  this.addParameter("medium",
281
281
  {
282
282
  title:"type of liquid in tank",
283
- enum: Object.keys(Media)
283
+ enum: Object.keys(Media),
284
+ isRequired: true
284
285
  }
285
286
  )
286
287
  this.addParameter("tankHeight",
287
288
  {
288
289
  title:"height of tank (in mm)",
289
- type:"number",
290
- unit:"mm"
290
+ type:"integer",
291
+ unit:"mm",
292
+ isRequired: true
291
293
  }
292
294
  )
293
- this.addDefaultParam("id")
295
+ this.addDefaultParam("id", true)
294
296
 
295
297
  this.addDefaultPath("battVolt","sensors.batteryVoltage")
296
298
  .read=((buffer)=>{
@@ -0,0 +1,51 @@
1
+ const BTHomeServiceData = require("./BTHome/BTHomeServiceData");
2
+ const AbstractBTHomeSensor = require("./BTHome/AbstractBTHomeSensor");
3
+
4
+ /**
5
+ * Sensor class representing the Shelly BLU Door/Window.
6
+ * Specification on @see https://shelly-api-docs.shelly.cloud/docs-ble/Devices/dw
7
+ * This sensor is publishing data utilising the BTHome format and inherits from {@link AbstractBTHomeSensor}.
8
+ */
9
+ class ShellySBDW002C extends AbstractBTHomeSensor {
10
+ static Domain = this.SensorDomains.environmental;
11
+
12
+ /**
13
+ * The shortened local name as advertised by the Shelly BLU H&T.
14
+ * @type {string}
15
+ */
16
+ static SHORTENED_LOCAL_NAME = "SBDW-002C";
17
+ static LOCAL_NAME = "Shelly BLU Door/Window";
18
+
19
+ getDescription() {
20
+ return `For more information about the sensor go here: <a href=https://us.shelly.com/products/shelly-blu-door-window-white target="_blank">Shelly Blu Door/Window</a>.`;
21
+ }
22
+
23
+ initSchema() {
24
+ super.initSchema();
25
+ this.addDefaultParam("zone", true).default = "cabin";
26
+
27
+ this.addParameter("opening", {
28
+ title: "Name of opening",
29
+ examples: ["companionWay", "porthole", "hatch"],
30
+ isRequired: true,
31
+ default: "companionWay",
32
+ });
33
+
34
+ this.addMetadatum("opening", "", "state of opening (door/window) as 'open' or 'closed'; null if not present. ", this.parseWindowState.bind(this)).default =
35
+ "environment.{zone}.{opening}.state";
36
+
37
+ // The second parameter is not the signalk path, but a JSON object in plugin_defaults.json; that provides the default signalk path
38
+ this.addDefaultPath("battery", "sensors.batteryStrength").read = this.parseBatteryLevel.bind(this);
39
+
40
+ this.addMetadatum("illuminance", "lx", "illuminance measured in lux, scaled 0.01", this.parseIlluminance.bind(this)).default =
41
+ "sensors.{macAndName}.illuminance";
42
+
43
+ this.addMetadatum("rotation", "deg", "rotation in degrees from the closed position; null if not present", this.parseRotation.bind(this)).default =
44
+ "sensors.{macAndName}.rotation";
45
+
46
+ this.addMetadatum("button", "", "button 'press' or 'hold_press'; null if not present", this.parseShellyButton.bind(this)).default =
47
+ "sensors.{macAndName}.button";
48
+ }
49
+ }
50
+
51
+ module.exports = ShellySBDW002C;
@@ -27,8 +27,7 @@ class ShellySBMO003Z extends AbstractBTHomeSensor {
27
27
 
28
28
  initSchema() {
29
29
  super.initSchema()
30
- this.addDefaultParam("zone")
31
-
30
+ this.addDefaultParam("zone", true)
32
31
 
33
32
  this.addMetadatum(
34
33
  "motion",
@@ -60,6 +60,7 @@ class ShenzhenLiONBMS extends BTSensor{
60
60
  {
61
61
  title:"Number of cells",
62
62
  type: "integer",
63
+ isRequired: true,
63
64
  default: 4,
64
65
  minimum: 1,
65
66
  maximum: 16
@@ -218,7 +218,8 @@ class XiaomiMiBeacon extends BTSensor{
218
218
  this.addParameter(
219
219
  "encryptionKey",
220
220
  {
221
- title: "encryptionKey (AKA bindKey) for decryption"
221
+ title: "encryptionKey (AKA bindKey) for decryption",
222
+
222
223
  }
223
224
  )
224
225
  this.addDefaultPath('temp','environment.temperature')
@@ -381,6 +381,9 @@ function configuredDevices(){
381
381
  }
382
382
 
383
383
  function devicesInDomain(domain){
384
+ if (domain==="_configured")
385
+ return configuredDevices()
386
+ else
384
387
  return Array.from(sensorMap.entries()).filter((entry)=>entry[1].info.domain===domain)
385
388
  }
386
389
 
@@ -390,9 +393,11 @@ useEffect(()=>{
390
393
 
391
394
  _sensorMap = sensorMap
392
395
 
393
- const _sensorDomains = new Set(sensorMap.entries().map((entry)=>{ return entry[1].info.domain}))
396
+ const _sensorDomains = [... (new Set(sensorMap.entries().map((entry)=>{ return entry[1].info.domain})))].sort()
394
397
  const sl = {
395
- _configured: configuredDevices().map((entry) => {
398
+ _configured: configuredDevices().length==0?
399
+ "Select a device from its domain tab (Electrical etc.) and configure it.":
400
+ configuredDevices().map((entry) => {
396
401
  return createListGroupItem(sensorMap.get(entry[0]))
397
402
  })
398
403
  }
@@ -425,11 +430,12 @@ useEffect(()=>{
425
430
 
426
431
  function getTab(key){
427
432
  var title = key.slice(key.charAt(0)==="_"?1:0)
433
+ let sl = getSensorList(key)
428
434
 
429
- return <Tab eventKey={key} title={title.charAt(0).toUpperCase()+title.slice(1) }>
435
+ return <Tab eventKey={key} title={`${title.charAt(0).toUpperCase()}${title.slice(1)}${devicesInDomain(key).length==0?'':' ('+devicesInDomain(key).length+')'}` } >
430
436
 
431
437
  <ListGroup style={{ maxHeight: '300px', overflowY: 'auto' }}>
432
- {getSensorList(key)}
438
+ {sl}
433
439
  </ListGroup>
434
440
 
435
441