From 40db573458e17543221c7d2699f3224a5e8b3ab8 Mon Sep 17 00:00:00 2001 From: Marco H Date: Mon, 7 Mar 2022 17:59:18 +0100 Subject: [PATCH] Initial version of Barometer Alarm Widget --- apps/widbaroalarm/ChangeLog | 1 + apps/widbaroalarm/README.md | 24 ++++ apps/widbaroalarm/default.json | 10 ++ apps/widbaroalarm/metadata.json | 19 +++ apps/widbaroalarm/screenshot.png | Bin 0 -> 2707 bytes apps/widbaroalarm/settings.js | 85 +++++++++++++ apps/widbaroalarm/widget.js | 199 +++++++++++++++++++++++++++++++ apps/widbaroalarm/widget.png | Bin 0 -> 9872 bytes apps/widbaroalarm/widget24.png | Bin 0 -> 10100 bytes 9 files changed, 338 insertions(+) create mode 100644 apps/widbaroalarm/ChangeLog create mode 100644 apps/widbaroalarm/README.md create mode 100644 apps/widbaroalarm/default.json create mode 100644 apps/widbaroalarm/metadata.json create mode 100644 apps/widbaroalarm/screenshot.png create mode 100644 apps/widbaroalarm/settings.js create mode 100644 apps/widbaroalarm/widget.js create mode 100644 apps/widbaroalarm/widget.png create mode 100644 apps/widbaroalarm/widget24.png diff --git a/apps/widbaroalarm/ChangeLog b/apps/widbaroalarm/ChangeLog new file mode 100644 index 000000000..ec66c5568 --- /dev/null +++ b/apps/widbaroalarm/ChangeLog @@ -0,0 +1 @@ +0.01: Initial version diff --git a/apps/widbaroalarm/README.md b/apps/widbaroalarm/README.md new file mode 100644 index 000000000..6708b1e2b --- /dev/null +++ b/apps/widbaroalarm/README.md @@ -0,0 +1,24 @@ +# Barometer alarm widget + +Get a notification when the pressure reaches defined thresholds. + +![Screenshot](screenshot.png) + +## Settings + +* Interval: check interval of sensor data in minutes. 0 to disable automatic check. +* Low alarm: Toggle low alarm + * Low threshold: Warn when pressure drops below this value +* High alarm: Toggle high alarm + * High threshold: Warn when pressure exceeds above this value +* Change alarm: Warn when pressure changes more than this value in the recent 3 hours (having at least 30 min of data) + 0 to disable this alarm. +* Show widget: Enable/disable widget visibility +* Buzz on alarm: Enable/disable buzzer on alarm + + +## Widget +The widget shows two rows: pressure value of last measurement and pressure average of the the last three hours. + +## Creator +Marco ([myxor](https://github.com/myxor)) diff --git a/apps/widbaroalarm/default.json b/apps/widbaroalarm/default.json new file mode 100644 index 000000000..7dbe6fc17 --- /dev/null +++ b/apps/widbaroalarm/default.json @@ -0,0 +1,10 @@ +{ + "buzz": true, + "lowalarm": false, + "min": 950, + "highalarm": false, + "max": 1030, + "changeIn3h": 2, + "show": true, + "interval": 15 +} diff --git a/apps/widbaroalarm/metadata.json b/apps/widbaroalarm/metadata.json new file mode 100644 index 000000000..3dd02019d --- /dev/null +++ b/apps/widbaroalarm/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "widbaroalarm", + "name": "Barometer alarm widget", + "shortName": "Barometer alarm", + "version": "0.01", + "description": "A widget that can alarm on when the pressure reaches defined thresholds.", + "icon": "widget.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "widget", + "tags": "tool,barometer", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"widbaroalarm.wid.js","url":"widget.js"}, + {"name":"widbaroalarm.settings.js","url":"settings.js"}, + {"name":"widbaroalarm.default.json","url":"default.json"} + ], + "data": [{"name":"widbaroalarm.json"}, {"name":"widbaroalarm.log"}] +} diff --git a/apps/widbaroalarm/screenshot.png b/apps/widbaroalarm/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..c398b0aa875007e1d4f9d3b8aa0eaafad35a3b1a GIT binary patch literal 2707 zcmd6p`#0448pl8188bAEXey+HjdB}NWN*i%CJu7Tq)CRM+Dy4e7}xoZ5@L5z$caYb z*fxW&lg4Ej(~Zb9Be}<%**~7Y;GEA|pXXVh^;zqAp7lJR^?rS_k9#^R z<1}#q0A*JfN3z`4|FxB{a$DE(fhKoQEZLa^YG3W1mLC*iNv=Lh@|B|Wa~1$NJ6Fde zJ`9?;gjGWHO zt6a&(0jYJFRFe3WOVmi&_--QJ*Z+wx-3EIRidC)o@F9`Qruq+;!ggHA;+3Xh)L`}K zzZJFCt7^>?9fSw!6{5UNlSmuSONP1$mz^fK%zGgeK-&^}xL8fJr<7iF)5s;;Mv~EN z?`ikZ3Q^8-c(-m^Br~$EjdZ9UUA_NFu!}|}S<-_5MbEtB9P1ICk&s;N4n%L~)w_?PGTxm;_DJk~bi=1ruiwOvp7IjslLHC*^TDN-0J?KaYq zeeF1z6B%B{Jy7{OHZo*#kupDSFg+St9p5P-`M$-x%Q<2?_S|JtbU z6jb16BA6#UTC@B6a{s6dp;<{p0a-RPo0;QyJYtda;U>qDzDd^zjpUKkBOJC1V-#P- z?DI_AdJ1hSCb_&F3qTi|b|j$vVsaO~Fdf0&lXD9+X?aBBX^@~jc?;x^LKkyUncZuK zG1@%R9X3uXENMNp^h)b-k~%4M$}`#Gab9w)dOF?~>z8v4aW%JSlRjTCag?$#S>{ZHF(M1cokLWnV4Qpmk`^btfo4ll5gXmH( zl;kw?2&WUL&emh?VpA$U={(@}FmxBMv!@xI+X{{RmRBDDfOZPSfFI+QqxZmPbed40 z*e2yi6~z3e9|SzL?28qHG+a0URBq9Jg*L~RQviG-Q)LR$z>-Y>Qq_|=5Af)!9Y8er zwkHu>Wews$^o;baE$BJ?zXxLDG?n>)#{~fy8L{s#Wuy)10EV%ooC3 zBcd&bxguQ`%^P0r}v%;&F@@%Bb}y;ExeqQ^`;`cqneU4$~@|IMmd&$XVB9WEU*#Qf*jug9`Sj)~)AV zs=lC=ui8lW{9Ne5nHyNOIQ!;Mn5Y4}9d0>Vi&2EDr`#&=`oZ-!ru>b8xq)L7?Lj&6 zAqu%0em7cBVQ(e`f!BDHwLkH;CZ&WRKJ2%qtqfb0=Gi;1MJ+tDnTV5lz{yuDa`k@F zFPSONDCOuj8DyS}u3|>9T-s(i83xRnoiT%`!dgttd_>7Kv~2C?3;{$1qtB(<5T7*? zrfLb@Z6xk`1wzF<3egXfjm$TVvc+9K56|=NQZHgf`8I!Lr9!vWejPL;ibgh=T8M!T znK&jmRX}Ov*EUhc*8&|*Pnkf1u%TB}lrVH6*`vIq?lG3HHd2PE+W)SRHJAQSi@uoB z1lher+2TY6X109|g=*J78`meW+^oz2dR!OR2NDl-u)*4UboeHoDBieWy~8}EZXW=*Cl*U z@4Hav0k9(OZ1KBRAFvd;5E_-Zw7n%nUre0p-&*i}a7J%rr+CRd8Fc8O(NcH)h~^W( zG#FnMkoOcPxTnjZL?DranuHb^$xl z?ulu|h-@x@8-1xT6JNzl_2+rkvA;d0_!6H|Qky5EAotsbNkwU&V>$MEYaSTi%|0eQ zX3zLVXA}@l-wC>CVVFy(*yvcn{VMpO?zFCeut<{jdPj2dpv2EceMwB zr>PT$6%vL~66=|BtP`&jj1f)6+QDZiN&ckPPwL>h5Bt$L4%Ew4$5GzQTQdinsSt>F z%VN36F-T7BjhAyRa+8H>6#&k^@rENYV3??j{XwodE(<#aaul{3JUv{F0=uj1&$Y|P z&~im1%Ll-EZ|LDIC@@=~ioPXhVEJmUSkB6E!dV?M^#7g|VG86z?C9gyC;wOiS0_)$ JT2fH@e*hao { + return x != 0 ? x + ' min' : 'off'; + }, + onchange: x => save("interval", x) + }, + "Low alarm": { + value: settings.lowalarm, + format: x => { + return x ? 'Yes' : 'No'; + }, + onchange: x => save("lowalarm", x), + }, + "Low threshold": { + value: settings.min, + min: 600, + max: 1000, + step: 10, + onchange: x => save("min", x), + }, + "High alarm": { + value: settings.highalarm, + format: x => { + return x ? 'Yes' : 'No'; + }, + onchange: x => save("highalarm", x), + }, + "High threshold": { + value: settings.max, + min: 1000, + max: 1100, + step: 10, + onchange: x => save("max", x), + }, + "Change alarm": { + value: settings.changeIn3h, + min: 0, + max: 10, + step: 1, + format: x => { + return x != 0 ? x + ' hPa/3h' : 'off'; + }, + onchange: x => save("changeIn3h", x) + }, + "Show widget": { + value: settings.show, + format: x => { + return x ? 'Yes' : 'No'; + }, + onchange: x => save('show', x) + }, + "Buzz on alarm": { + value: settings.buzz, + format: x => { + return x ? 'Yes' : 'No'; + }, + onchange: x => save('buzz', x) + }, + }; + E.showMenu(menu); + } + + showMainMenu(); +}); diff --git a/apps/widbaroalarm/widget.js b/apps/widbaroalarm/widget.js new file mode 100644 index 000000000..b0a8f110c --- /dev/null +++ b/apps/widbaroalarm/widget.js @@ -0,0 +1,199 @@ +(function() { + let lastPressure; + let avrPressure; + let threeHourAvrPressure; + + const LOG_FILE = "widbaroalarm.log.json"; + const SETTINGS_FILE = "widbaroalarm.json"; + const storage = require('Storage'); + let settings = Object.assign( + storage.readJSON("widbaroalarm.default.json", true) || {}, + storage.readJSON(SETTINGS_FILE, true) || {} + ); + + function setting(key) { + return settings[key]; + } + const interval = setting("interval"); + + let history3 = storage.readJSON(LOG_FILE, true) || []; // history of recent 3 hours + + function showAlarm(text, value, value2, icon) { + if (text == undefined || value == undefined) return; + const Layout = require("Layout"); + layout = new Layout({ + type: "v", + c: [{ + type: "h", + c: [{ + type: "img", + pad: 0, + src: function() { + return icon || require("heatshrink").decompress(atob("jEY4cA///gH4/++mkK30kiWC4H8x3BGDmSGgYDCgmSoEAg3bsAIDpAIFkmSpMAm3btgIFDQwIGNQpTYkAIJwAHEgMoCA0JgMEyBnBCAW3KoQQDhu3oAIH5JnDBAW24IIBEYm2EYwACBCIACA")); + } + }, + { + type: "txt", + id: "title", + font: "6x8:2", + label: "Alarm", + pad: 2 + }, + ] + }, + { + type: "txt", + id: "text", + font: "6x8:2", + label: text, + pad: 3 + }, + { + type: "txt", + id: "value", + font: "6x8:2", + label: value, + halign: 0, + pad: 3 + }, + { + type: "txt", + id: "value2", + font: "6x8:1", + label: value2 || "", + halign: 0, + pad: 3 + }, + ] + }, { + btns: [{ + label: "OK", + cb: function() { + load(); // close + } + }], + lazy: false + }); + + g.clear(); + layout.render(); + + if (setting("buzz") && + !(storage.readJSON('setting.json', 1) || {}).quiet) { + Bangle.buzz(); + } + } + + let alreadyWarned = false; + + function baroHandler(data) { + if (data === undefined) { + setTimeout(() => Bangle.getPressure().then(baroHandler), 500); + } else { + lastPressure = Math.round(data.pressure); + if (lastPressure == undefined || lastPressure <= 0) return; + + const ts = Math.round(Date.now() / 1000); // seconds + const d = { + "ts": ts, + "p": lastPressure + }; + + // delete entries older than 3h + for (let i = 0; i < history3.length; i++) { + if (history3[i]["ts"] < ts - (3 * 60 * 60)) { + history3.shift(); + } + } + // delete oldest entries until we have max 50 + while (history3.length > 50) { + history3.shift(); + } + + history3.push(d); + // write data to storage + storage.writeJSON(LOG_FILE, history3); + + // we need at least three entries for reliable detection + if (history3.length >= 3) { + + // calculate average of recent three entries + avrPressure = (history3[history3.length - 1]["p"] + history3[history3.length - 2]["p"] + history3[history3.length - 3]["p"]) / 3; + + if (setting("lowalarm") && avrPressure <= setting("min")) { + showAlarm("Pressure low", Math.round(avrPressure) + " hPa"); + alreadyWarned = true; + } + if (setting("highalarm") && avrPressure >= setting("max")) { + showAlarm("Pressure high", Math.round(avrPressure) + " hPa"); + alreadyWarned = true; + } + + if (!alreadyWarned) { + // 3h change detection + const threeHourChange = setting("changeIn3h"); + if (threeHourChange > 0) { + // we need at least 30min of data for reliable detection + if (history3[0]["ts"] > ts - (30 * 60)) { + return; + } + + // Average of oldest three entries + const oldestAvgPressure = (history3[0]["p"] + history3[1]["p"] + history3[2]["p"]) / 3; + if (oldestAvgPressure != undefined && oldestAvgPressure > 0) { + const diff = oldestAvgPressure - avrPressure; + if (Math.abs(diff) > threeHourChange) { + showAlarm("Pressure " + (diff > 0 ? "drop" : "raise"), (Math.round(Math.abs(diff) * 10) / 10) + " hPa/3h", + Math.round(oldestAvgPressure) + " hPa -> " + Math.round(avrPressure) + " hPa"); + } + } + } + } + } + + // calculate 3h average for widget + let sum = 0; + for (let i = 0; i < history3.length; i++) { + sum += history3[i]["p"]; + } + threeHourAvrPressure = sum / history3.length; + } + } + + function check() { + Bangle.getPressure().then(baroHandler); + } + + function reload() { + check(); + } + + function draw() { + g.reset(); + if (setting("show") && lastPressure != undefined) { + g.setFont("6x8", 1).setFontAlign(1, 0); + g.drawString(Math.round(lastPressure), this.x + 24, this.y + 6); + if (threeHourAvrPressure != undefined && threeHourAvrPressure > 0) { + g.drawString(Math.round(threeHourAvrPressure), this.x + 24, this.y + 6 + 10); + } + } + } + + if (global.WIDGETS != undefined && typeof WIDGETS === "object") { + WIDGETS["baroalarm"] = { + width: setting("show") ? 24 : 0, + reload: reload, + area: "tr", + draw: draw + }; + } + + // Let's delay check a bit + setTimeout(function() { + check(); + if (interval > 0) { + setInterval(check, interval * 60000); + } + }, 10000); + +})(); diff --git a/apps/widbaroalarm/widget.png b/apps/widbaroalarm/widget.png new file mode 100644 index 0000000000000000000000000000000000000000..5be2921432b2d43167309be3ca50e52e3216dc71 GIT binary patch literal 9872 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 am%74g<61aCeHq`{01Xvg