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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -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,10 +139,27 @@ 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) => {
144
160
  app.debug(req.body)
161
+ const sensor = sensorMap.get(req.body.mac_address)
162
+ sensor.prepareConfig(req.body)
145
163
  const i = deviceConfigs.findIndex((p)=>p.mac_address==req.body.mac_address)
146
164
  if (i<0){
147
165
  if (!options.peripherals){
@@ -154,18 +172,17 @@ module.exports = function (app) {
154
172
  } else {
155
173
  options.peripherals[i] = req.body
156
174
  }
175
+ deviceConfigs=options.peripherals
157
176
  app.savePluginOptions(
158
177
  options, async () => {
159
178
  app.debug('Plugin options saved')
160
179
  res.status(200).json({message: "Sensor updated"})
161
- const sensor = sensorMap.get(req.body.mac_address)
162
180
  if (sensor) {
163
181
  removeSensorFromList(sensor)
164
182
  if (sensor.isActive())
165
183
  await sensor.stopListening()
166
- initConfiguredDevice(req.body)
167
184
  }
168
-
185
+ initConfiguredDevice(req.body)
169
186
  }
170
187
  )
171
188
 
@@ -210,12 +227,6 @@ module.exports = function (app) {
210
227
  }
211
228
  )
212
229
  });
213
- router.get('/getDomains', (req, res) => {
214
-
215
- res.status(200).json(
216
- BTSensor.SensorDomains
217
- );
218
- })
219
230
 
220
231
  router.get('/getBaseData', (req, res) => {
221
232
 
@@ -240,9 +251,10 @@ module.exports = function (app) {
240
251
 
241
252
  router.get('/getProgress', (req, res) => {
242
253
  app.debug("Sending progress")
243
- const json = {"progress":foundConfiguredDevices/deviceConfigs.length, "maxTimeout": 1,
254
+ let deviceCount = deviceConfigs.filter((dc)=>dc.active).length
255
+ const json = {"progress":foundConfiguredDevices/deviceCount, "maxTimeout": 1,
244
256
  "deviceCount":foundConfiguredDevices,
245
- "totalDevices": deviceConfigs.length}
257
+ "totalDevices": deviceCount}
246
258
  res.status(200).json(json)
247
259
 
248
260
  });
@@ -287,10 +299,12 @@ module.exports = function (app) {
287
299
  const config = getDeviceConfig(sensor.getMacAddress())
288
300
  const schema = sensor.getJSONSchema()
289
301
  schema.htmlDescription = sensor.getDescription()
302
+
290
303
  return {
291
304
  info: getSensorInfo(sensor),
292
305
  schema: schema,
293
- config: config?config:{}
306
+ config: config?config:{},
307
+ configCopy: JSON.parse(JSON.stringify(config?config:{}))
294
308
  }
295
309
  }
296
310
 
@@ -576,11 +590,13 @@ module.exports = function (app) {
576
590
  }
577
591
  if (!(deviceConfigs===undefined)){
578
592
  const maxTimeout=Math.max(...deviceConfigs.map((dc)=>dc?.discoveryTimeout??options.discoveryTimeout))
593
+ const totalDevices = deviceConfigs.filter((dc)=>dc.active).length
594
+
579
595
  var progress = 0
580
596
  if (progressID==null)
581
597
  progressID = setInterval(()=>{
582
- channel.broadcast({"progress":++progress, "maxTimeout": maxTimeout, "deviceCount":foundConfiguredDevices, "totalDevices": deviceConfigs.length},"progress")
583
- if ( foundConfiguredDevices==deviceConfigs.length){
598
+ channel.broadcast({"progress":++progress, "maxTimeout": maxTimeout, "deviceCount":foundConfiguredDevices, "totalDevices": totalDevices},"progress")
599
+ if ( foundConfiguredDevices==totalDevices){
584
600
  progressID,progressTimeoutID = null
585
601
  clearTimeout(progressTimeoutID)
586
602
  clearInterval(progressID)
@@ -593,7 +609,7 @@ module.exports = function (app) {
593
609
 
594
610
  clearInterval(progressID);
595
611
  progressID=null
596
- channel.broadcast({"progress":maxTimeout, "maxTimeout": maxTimeout, "deviceCount":foundConfiguredDevices, "totalDevices": deviceConfigs.length},"progress")
612
+ channel.broadcast({"progress":maxTimeout, "maxTimeout": maxTimeout, "deviceCount":foundConfiguredDevices, "totalDevices": totalDevices},"progress")
597
613
  }
598
614
  }, (maxTimeout+1)*1000);
599
615
 
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-1",
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,{BTConfig:()=>A,default:()=>k});var a=n(73490),s=n(74810),r=n(40334),o=n(86528),c=n.n(o),i=n(86452),l=n(27606),u=n(82096),m=n(3768),d=n(71431),g=n(39676),f=n(47041),p=n(86038),h=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);function A(e){const t=(0,u.A)((e=>({root:{"& > *":{margin:e.spacing(1)}}}))),[n,A]=(0,o.useState)({}),[k,D]=(0,o.useState)({}),[C,x]=(0,o.useState)({}),[$,_]=(0,o.useState)({"ui:options":{label:!1},title:{"ui:widget":"hidden"}}),[T,N]=(0,o.useState)(),[O,j]=(0,o.useState)(new Map),[M,J]=(0,o.useState)({progress:0,maxTimeout:100,deviceCount:0,totalDevices:0}),[L,U]=(0,o.useState)("unknown"),[I,B]=(0,o.useState)(),P=t();function R(e,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 G(e){var t;try{t=fetch(`/plugins/bt-sensors-plugin-sk/${e}`,{credentials:"include"})}catch(e){t={status:500,statusText:e.toString()}}return t}function H(e){return Object.keys(e.configCopy).length>0}function K(e){const t=H(e);return c().createElement(v.A,{action:!0,onClick:()=>{e.config.mac_address=e.info.mac,x(e.schema),N(e.config)}},c().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}`,c().createElement("div",{class:"d-flex justify-content-between "},function(e){return null==e.info.lastContactDelta||e.info.lastContactDelta>e.config.discoveryTimeout?c().createElement(m.A,null):e.info.signalStrength>80?c().createElement(d.A,null):e.info.signalStrength>60?c().createElement(g.A,null):e.info.signalStrength>40?c().createElement(f.A,null):e.info.signalStrength>20?c().createElement(p.A,null):c().createElement(h.A,null)}(e))));var n}function F(e){window.open(e,"_blank","noreferrer")}return(0,o.useEffect)((()=>{let e=null;return G("getPluginState").then((async t=>{if(404==t.status)throw U("unknown"),new Error("unable to get plugin state");const n=await t.json();e=new EventSource("/plugins/bt-sensors-plugin-sk/sse",{withCredentials:!0}),e.addEventListener("newsensor",(e=>{!function(e){let t=JSON.parse(e.data);console.log(`New sensor: ${t.info.mac}`),j((e=>(e.set(t.info.mac,t),new Map(e))))}(e)})),e.addEventListener("sensorchanged",(e=>{!function(e){console.log("sensorchanged");const t=JSON.parse(e.data);j((e=>{const n=e.get(t.mac);return n&&Object.assign(n.info,t),new Map(e)}))}(e)})),e.addEventListener("progress",(e=>{const t=JSON.parse(e.data);J(t)})),e.addEventListener("pluginstate",(e=>{const t=JSON.parse(e.data);U(t.state)})),U(n.state),(async()=>{const e=await async function(){const e=await G("getSensors");if(200!=e.status)throw new Error(`Unable get sensor data: ${e.statusText} (${e.status}) `);return await e.json()}();j(new Map(e.map((e=>[e.info.mac,e]))))})()})).catch((e=>{B(e)})),()=>{console.log("Closing connection to SSE"),e.close()}}),[]),(0,o.useEffect)((()=>{"started"==L?(async function(){const e=await G("getBaseData");if(200!=e.status)throw new Error(`Unable to get base data: ${e.statusText} (${e.status}) `);const t=await e.json();return t.schema.htmlDescription=c().createElement("div",null,(0,r.Ay)(t.schema.htmlDescription),c().createElement("p",null)),t}().then((e=>{A(e.schema),D(e.data)})).catch((e=>{B(e)})),async function(){const e=await G("getProgress");if(200!=e.status)throw new Error(`Unable to get progress: ${e.statusText} (${e.status}) `);return await e.json()}().then((e=>{J(e)})).catch((e=>{B(e)}))):(A({}),D({}))}),[L]),"stopped"==L||"unknown"==L?c().createElement("h3",null,"Enable plugin to see configuration"):c().createElement("div",null,c().createElement("div",{className:P.root},c().createElement(i.A,{variant:"contained",onClick:()=>{F("https://github.com/naugehyde/bt-sensors-plugin-sk/tree/1.2.0-beta#configuration")}},"Documentation"),c().createElement(i.A,{variant:"contained",onClick:()=>{F("https://github.com/naugehyde/bt-sensors-plugin-sk/issues/new/choose")}},"Report Issue"),c().createElement(i.A,{variant:"contained",onClick:()=>{F("https://discord.com/channels/1170433917761892493/1295425963466952725")}},"Discord Thread"),c().createElement("p",null),c().createElement("p",null)),c().createElement("hr",{style:{width:"100%",height:"1px",color:"gray","background-color":"gray","text-align":"left","margin-left":0}}),I?c().createElement("h2",{style:"color: red;"},I):"",c().createElement(a.Ay,{schema:n,validator:s.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=>D(e.formData),onSubmit:({formData:e},t)=>{var n;n=e,j(new Map),R("updateBaseData",n).then((e=>{200!=e.status&&B(new Error(`Unable to update base data: ${e.statusText} (${e.status})`))})),x({})},onError:b("errors"),formData:k}),c().createElement("hr",{style:{width:"100%",height:"1px",color:"gray","background-color":"gray","text-align":"left","margin-left":0}}),c().createElement("p",null),c().createElement("p",null),M.deviceCount<M.totalDevices?c().createElement(S.A,{max:M.maxTimeout,now:M.progress}):"",c().createElement("p",null),c().createElement(w.A,{defaultActiveKey:"_configured",id:"domain-tabs",className:"mb-3"},function(){const e=[...new Set(O.entries().map((e=>e[1].info.domain)))].sort(),t=Array.from(O.entries()).filter((e=>H(e[1])));let n={};return n._configured=0==t.length?"Select a device from its domain tab (Electrical etc.) and configure it.":t.map((e=>K(O.get(e[0])))),e.forEach((e=>{var t;n[e]=(t=e,Array.from(O.entries()).filter((e=>e[1].info.domain===t))).map((e=>K(O.get(e[0]))))})),Object.keys(n).map((e=>function(e,t){let n=e.slice("_"===e.charAt(0)?1:0);return c().createElement(y.A,{eventKey:e,title:`${n.charAt(0).toUpperCase()}${n.slice(1)}${"string"==typeof t?"":" ("+t.length+")"}`},c().createElement(E.A,{style:{maxHeight:"300px",overflowY:"auto"}},t))}(e,n[e])))}()),c().createElement("div",{style:{paddingLeft:10,paddingTop:10,display:0==Object.keys(C).length?"none":""}},c().createElement(l.A,{container:!0,direction:"column",style:{spacing:5}},c().createElement(l.A,{item:!0},c().createElement("h2",null,C?.title),c().createElement("p",null)),c().createElement(l.A,{item:!0},(0,r.Ay)(C?.htmlDescription))),c().createElement(a.Ay,{schema:C,validator:s.Ay,uiSchema:$,onChange:e=>{const t=O.get(e.formData.mac_address);t&&(t._changesMade=!0,t.config=e.formData,N(e.formData))},onSubmit:({formData:e},t)=>{var n;R("updateSensorData",n=e).then((e=>{if(200!=e.status)throw new Error(e.statusText);j((e=>(e.delete(n.mac_address),new Map(e)))),x({})})),alert("Changes saved")},onError:b("errors"),formData:T},c().createElement("div",{className:P.root},c().createElement(i.A,{type:"submit",color:"primary",variant:"contained"},"Save"),c().createElement(i.A,{variant:"contained",onClick:()=>{var e;e=T.mac_address,O.get(e)._changesMade=!1,O.get(e).config=JSON.parse(JSON.stringify(O.get(e).configCopy)),N(O.get(e).config)}},"Undo"),c().createElement(i.A,{variant:"contained",color:"secondary",onClick:e=>function(e){const t=O.get(e);(!H(t)||window.confirm(`Delete configuration for ${t.info.name}?`))&&function(e){try{R("removeSensorData",{mac_address:e}).then((e=>{if(200!=e.status)throw new Error(e.statusText)})),j((t=>(t.delete(e),new Map(t)))),x({})}catch{}}(e)}(T.mac_address)},"Delete")))))}const k=A}}]);
@@ -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}
@@ -259,8 +259,8 @@ class MopekaTankSensor extends BTSensor{
259
259
 
260
260
  async init(){
261
261
  await super.init()
262
- const md = this.valueIfVariant(this.getManufacturerData(this.constructor.manufacturerID))
263
- this.modelID = md[0]
262
+ const md = this.getManufacturerData(this.constructor.manufacturerID)
263
+ this.modelID = md?md[0]:0
264
264
  }
265
265
 
266
266
  getMedium(){
@@ -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)=>{
@@ -321,12 +323,12 @@ class MopekaTankSensor extends BTSensor{
321
323
  this.addMetadatum("accX","Mg","acceleration on X-axis",
322
324
  (buffer)=>{ return buffer.readUInt8(8)}
323
325
  )
324
- .default="sensors.{macAndName}.accelerationXAxis"
326
+ .examples=["sensors.{macAndName}.accelerationXAxis"]
325
327
 
326
328
  this.addMetadatum("accY","Mg","acceleration on Y-axis",
327
329
  (buffer)=>{ return buffer.readUInt8(9)}
328
330
  )
329
- .default="sensors.{macAndName}.accelerationYAxis"
331
+ .examples=["sensors.{macAndName}.accelerationYAxis"]
330
332
  }
331
333
 
332
334
  propertiesChanged(props){
@@ -50,15 +50,15 @@ class RuuviTag extends BTSensor{
50
50
 
51
51
  this.addMetadatum("accX","Mg","acceleration on X-axis",
52
52
  (buffer)=>{ return buffer.readInt16BE(7)}
53
- ).default="sensors.{macAndName}.accX"
53
+ ).examples=["sensors.{macAndName}.accX"]
54
54
 
55
55
  this.addMetadatum("accY","Mg","acceleration on Y-axis",
56
56
  (buffer)=>{ return buffer.readInt16BE(9)}
57
- ) .default="sensors.{macAndName}.accY"
57
+ ) .examples=["sensors.{macAndName}.accY"]
58
58
 
59
59
  this.addMetadatum("accZ","Mg","acceleration on Z-axis",
60
60
  (buffer)=>{ return buffer.readInt16BE(11)}
61
- ) .default="sensors.{macAndName}.accZ"
61
+ ) .examples=["sensors.{macAndName}.accZ"]
62
62
 
63
63
  this.addDefaultPath("battV","sensors.batteryVoltage")
64
64
  .read=(buffer)=>{ return parseFloat((1.6+(buffer.readUInt16BE(13)>>5)/1000).toFixed(2))}
@@ -66,12 +66,12 @@ class RuuviTag extends BTSensor{
66
66
  this.addMetadatum("mc","","movement counter",
67
67
  (buffer)=>{ return buffer.readUInt16BE(13) && 0x1F}
68
68
  )
69
- .default="sensors.{macAndName}.movementCounter"
69
+ .examples=["sensors.{macAndName}.movementCounter"]
70
70
 
71
71
  this.addMetadatum("msc","","measurement sequence counter",
72
72
  (buffer)=>{ return buffer.readUInt16BE(15)}
73
73
  )
74
- .default="sensors.{macAndName}.measurementSequenceCounter"
74
+ .examples=["sensors.{macAndName}.measurementSequenceCounter"]
75
75
 
76
76
  }
77
77
 
@@ -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
@@ -15,9 +15,15 @@ function sleep(x) {
15
15
 
16
16
  constructor(device,config,gattConfig){
17
17
  super(device,config,gattConfig)
18
- this.encryptionKey = config?.encryptionKey
18
+
19
+ if(device.modelID)
20
+ this.modelID=device.modelID
21
+
19
22
  }
20
23
 
24
+ static getModelID(md) {
25
+ return md[0x2e1]?.value.readUInt16LE(2)??-1
26
+ }
21
27
  static async identifyMode(device, mode){
22
28
 
23
29
  var md = await this.getDeviceProp(device,'ManufacturerData')
@@ -26,8 +32,10 @@ function sleep(x) {
26
32
  else {
27
33
 
28
34
  if (md[0x2e1].value[0]==0x10) {
29
- if (md[0x2e1].value[4]==mode)
35
+ if (md[0x2e1].value[4]==mode) {
36
+ device.modelID=this.getModelID(md)
30
37
  return this
38
+ }
31
39
  else
32
40
  return null
33
41
  }
@@ -45,8 +53,10 @@ function sleep(x) {
45
53
  await sleep(500)
46
54
  }
47
55
  device.helper.removeListeners()
48
- if (md[0x2e1].value[4]==mode)
56
+ if (md[0x2e1].value[4]==mode) {
57
+ device.modelID=this.getModelID(md)
49
58
  return this
59
+ }
50
60
  else
51
61
  return null
52
62
  }
@@ -60,14 +70,13 @@ function sleep(x) {
60
70
  title:"Encryption Key"
61
71
  }
62
72
  )
63
- this.model_id=this.getManufacturerData(0x2e1)?.readUInt16LE(2)??"Unknown"
64
- this._schema.title = this.getName()
65
73
  }
66
74
  alarmReason(alarmValue){
67
75
  return this.constructor.AlarmReason[alarmValue]
68
76
  }
69
77
  getModelName(){
70
- const m = VC.MODEL_ID_MAP[this.model_id]
78
+ const mID = this.getModelID()
79
+ const m = VC.MODEL_ID_MAP[mID]
71
80
  if(m) {
72
81
  if(typeof m == 'string' || m instanceof String ) {
73
82
  return m
@@ -75,7 +84,7 @@ function sleep(x) {
75
84
  return m.name
76
85
  }
77
86
  }
78
- return this.constructor.name+" (Model ID:"+this.model_id+")"
87
+ return this.constructor.name+` (Model ID: ${mID==-1?"Unknown":mID})`
79
88
  }
80
89
 
81
90
  decrypt(data){
@@ -98,6 +107,13 @@ function sleep(x) {
98
107
  return Buffer.from(decData)
99
108
 
100
109
  }
110
+ getModelID(){
111
+ if (!this.modelID ||this.modelID==-1)
112
+ this.modelID=this.getManufacturerData(0x2e1)?.readUInt16LE(2)??-1
113
+
114
+ return this.modelID
115
+ }
116
+
101
117
  getName(){
102
118
  return `Victron ${this.getModelName()}`
103
119
  }
@@ -122,7 +138,7 @@ function sleep(x) {
122
138
  }
123
139
 
124
140
  getImage(){
125
- const m = VC.MODEL_ID_MAP[this.model_id]
141
+ const m = VC.MODEL_ID_MAP[this.getModelID()]
126
142
  if (m && m.image)
127
143
  return m.image
128
144
  else
@@ -138,5 +154,10 @@ function sleep(x) {
138
154
  To get the encryption key for your device, follow the instructions <a href=https://communityarchive.victronenergy.com/questions/187303/victron-bluetooth-advertising-protocol.html target="_victron_encrypt">here</a>`
139
155
  }
140
156
 
157
+ prepareConfig(config){
158
+ super.prepareConfig(config)
159
+ config.params.modelID=this.getModelID()
160
+ }
161
+
141
162
  }
142
163
  module.exports=VictronSensor
@@ -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')