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/BTSensor.js +34 -7
- package/README.md +22 -0
- package/bt-sensors-plugin-sk copy.json +170 -0
- package/diff.txt +2860 -0
- package/index.js +31 -15
- package/package.json +1 -1
- package/public/847.js +1 -1
- package/public/index.html +23 -0
- package/sensor_classes/BTHome/AbstractBTHomeSensor.js +26 -1
- package/sensor_classes/Junctek.js +41 -16
- package/sensor_classes/MercurySmartcraft.js +2 -2
- package/sensor_classes/MopekaTankSensor.js +10 -8
- package/sensor_classes/RuuviTag.js +5 -5
- package/sensor_classes/ShellySBDW002C.js +51 -0
- package/sensor_classes/ShellySBMO003Z.js +1 -2
- package/sensor_classes/ShenzhenLiOnBMS.js +1 -0
- package/sensor_classes/Victron/VictronSensor.js +29 -8
- package/sensor_classes/XiaomiMiBeacon.js +2 -1
- package/src/components/PluginConfigurationPanel.js +86 -155
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
|
-
|
|
254
|
+
let deviceCount = deviceConfigs.filter((dc)=>dc.active).length
|
|
255
|
+
const json = {"progress":foundConfiguredDevices/deviceCount, "maxTimeout": 1,
|
|
244
256
|
"deviceCount":foundConfiguredDevices,
|
|
245
|
-
"totalDevices":
|
|
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":
|
|
583
|
-
if ( foundConfiguredDevices==
|
|
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":
|
|
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.
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
+
emitObject["voltage"]=v/100
|
|
72
78
|
break
|
|
73
79
|
|
|
74
80
|
case 0xC1:
|
|
75
|
-
|
|
81
|
+
emitObject["current"]=()=>{return (v/100)*this.chargeDirection}
|
|
76
82
|
break
|
|
77
83
|
|
|
78
84
|
case 0xD1:
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
94
|
+
|
|
95
|
+
emitObject["remaining"]=v*3.6
|
|
85
96
|
break
|
|
86
97
|
|
|
87
98
|
case 0xD3:
|
|
88
|
-
|
|
99
|
+
emitObject["discharge"]= v/100000
|
|
89
100
|
break
|
|
90
101
|
case 0xD4:
|
|
91
|
-
|
|
102
|
+
emitObject["charge"]=v/100000
|
|
92
103
|
break
|
|
93
104
|
case 0xD6:
|
|
94
|
-
|
|
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
|
-
|
|
115
|
+
emitObject["impedance"]=v/100
|
|
98
116
|
break
|
|
99
117
|
case 0xD8:
|
|
100
|
-
|
|
118
|
+
emitObject["power"]=()=>{
|
|
119
|
+
this.debug(this.chargeDirection)
|
|
120
|
+
return (v/100)*this.chargeDirection
|
|
121
|
+
}
|
|
101
122
|
break
|
|
102
123
|
case 0xD9:
|
|
103
|
-
|
|
124
|
+
emitObject["temperature"]=v + 173.15 //assume C not F -- raw value is c - 100
|
|
104
125
|
break
|
|
105
126
|
case 0xB1:
|
|
106
|
-
|
|
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.
|
|
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:"
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
).
|
|
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
|
-
) .
|
|
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
|
-
) .
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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;
|
|
@@ -15,9 +15,15 @@ function sleep(x) {
|
|
|
15
15
|
|
|
16
16
|
constructor(device,config,gattConfig){
|
|
17
17
|
super(device,config,gattConfig)
|
|
18
|
-
|
|
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
|
|
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
|
|
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.
|
|
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')
|