From 963fc4970e9874c8d75933671b2b14b709044807 Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Sun, 4 May 2025 09:50:27 +0200 Subject: [PATCH] Zambretti Weather Forecaster Zambretti Forecaster, uses the Barometer for empirical weather forecast in the Northern Hemisphere (see https://web.archive.org/web/20110610213848/http://www.meteormetrics.com/zambretti.htm), similar to weather stations. Watch must be stationary and its height above sea level set. Uses code from widbaroalarm for measurements (boot.js) --- apps/zambretti/app-icon.js | 1 + apps/zambretti/app.js | 247 ++++++++++++++++++++++++++++++++++ apps/zambretti/app.png | Bin 0 -> 716 bytes apps/zambretti/boot.js | 106 +++++++++++++++ apps/zambretti/metadata.json | 20 +++ apps/zambretti/screenshot.png | Bin 0 -> 3934 bytes apps/zambretti/settings.js | 25 ++++ lang/de_DE.json | 31 ++++- 8 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 apps/zambretti/app-icon.js create mode 100644 apps/zambretti/app.js create mode 100644 apps/zambretti/app.png create mode 100644 apps/zambretti/boot.js create mode 100644 apps/zambretti/metadata.json create mode 100644 apps/zambretti/screenshot.png create mode 100644 apps/zambretti/settings.js diff --git a/apps/zambretti/app-icon.js b/apps/zambretti/app-icon.js new file mode 100644 index 000000000..c76c5bca1 --- /dev/null +++ b/apps/zambretti/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA/4AB31H//A/hL/ABfABRMD+ALJh5kKn//BRCBCP4ILT/WrBZOq1W/BY/+BYOvBY0DBYZhGh/6BYOrMI0/BYf5qpGGBYWVqtQC4+qqtVqgvF1NVAIIABI4ogBAAdAL4gKEBZg8E/oLiBQpUEgILHJAUFBY4kCioLHQoQKHGAYL4I4RTLNZaDLGA78EAH4AR")) diff --git a/apps/zambretti/app.js b/apps/zambretti/app.js new file mode 100644 index 000000000..ef714e9d9 --- /dev/null +++ b/apps/zambretti/app.js @@ -0,0 +1,247 @@ +/** + * https://web.archive.org/web/20110610213848/http://www.meteormetrics.com/zambretti.htm + */ + +const storage = require('Storage'); +const Layout = require("Layout"); + +let height; +let mainScreen = false; + +const ZAMBRETTI_FORECAST = { +A: /*LANG*/'Settled Fine', +B: /*LANG*/'Fine Weather', +C: /*LANG*/'Becoming Fine', +D: /*LANG*/'Fine Becoming Less Settled', +E: /*LANG*/'Fine, Possibly showers', +F: /*LANG*/'Fairly Fine, Improving', +G: /*LANG*/'Fairly Fine, Possibly showers, early', +H: /*LANG*/'Fairly Fine Showery Later', +I: /*LANG*/'Showery Early, Improving', +J: /*LANG*/'Changeable Mending', +K: /*LANG*/'Fairly Fine, Showers likely', +L: /*LANG*/'Rather Unsettled Clearing Later', +M: /*LANG*/'Unsettled, Probably Improving', +N: /*LANG*/'Showery Bright Intervals', +O: /*LANG*/'Showery Becoming more unsettled', +P: /*LANG*/'Changeable some rain', +Q: /*LANG*/'Unsettled, short fine Intervals', +R: /*LANG*/'Unsettled, Rain later', +S: /*LANG*/'Unsettled, rain at times', +T: /*LANG*/'Very Unsettled, Finer at times', +U: /*LANG*/'Rain at times, worse later.', +V: /*LANG*/'Rain at times, becoming very unsettled', +W: /*LANG*/'Rain at Frequent Intervals', +X: /*LANG*/'Very Unsettled, Rain', +Y: /*LANG*/'Stormy, possibly improving', +Z: /*LANG*/'Stormy, much rain', +}; + +const ZAMBRETTI_FALLING = { + 1050: 'A', + 1040: 'B', + 1024: 'C', + 1018: 'H', + 1010: 'O', + 1004: 'R', + 998: 'U', + 991: 'V', + 985: 'X', +}; + +const ZAMBRETTI_STEADY = { + 1033: 'A', + 1023: 'B', + 1014: 'E', + 1008: 'K', + 1000: 'N', + 994: 'P', + 989: 'S', + 981: 'W', + 974: 'X', + 960: 'Z', +}; + +const ZAMBRETTI_RISING = { + 1030: 'A', + 1022: 'B', + 1012: 'C', + 1007: 'F', + 1000: 'G', + 995: 'I', + 990: 'J', + 984: 'L', + 978: 'M', + 970: 'Q', + 965: 'T', + 959: 'Y', + 947: 'Z', +}; + +function correct_season(letter, dir) { + const month = new Date().getMonth() + 1; + const location = require("Storage").readJSON("mylocation.json",1)||{"lat":51.5072,"lon":0.1276,"location":"London"}; + const northern_hemisphere = location.lat > 0; + const summer = northern_hemisphere ? (month >= 4 && month <= 9) : (month >= 10 || month <= 3); + + let corr = 0; + if (dir < 0 && !summer) { // Winter falling + corr = -1; + } else if (dir > 0 && summer) { // Summer rising + corr = 1; + } + return String.fromCharCode(letter.charCodeAt(0)+corr); +} + +function get_zambretti_letter(pressure, dir) { + let table = () => { + if (dir < 0) { // Barometer Falling + return ZAMBRETTI_FALLING; + } else if (dir == 0) { // Barometer Steady + return ZAMBRETTI_STEADY; + } else { // Barometer Rising + return ZAMBRETTI_RISING; + } + }(); + + const closest = Object.keys(table).reduce(function(prev, curr) { + return (Math.abs(curr - pressure) < Math.abs(prev - pressure) ? curr : prev); + }); + + return correct_season(table[closest], dir); +} + +function loadSettings() { + const settings = require('Storage').readJSON("zambretti.json", true) || {}; + height = settings.height; +} + +function showMenu() { + const menu = { + "" : { + title : "Zambretti Forecast", + }, + "< Back": () => { + E.showMenu(); + layout.forgetLazyState(); + show(); + }, + /*LANG*/"Exit": () => load(), + /*LANG*/"Settings": () => + eval(require('Storage').read('zambretti.settings.js'))(() => { + loadSettings(); + showMenu(); + }), + 'Plot history': () => {E.showMenu(); history();}, + }; + E.showMenu(menu); +} + +const layout = new Layout({ + type:"v", c: [ + {type:"txt", font:"9%", label:/*LANG*/"Zambretti Forecast", bgCol:g.theme.bgH, fillx: true, pad: 1}, + {type:"txt", font:"12%", id:"forecast", filly: 1, wrap: 1, width: Bangle.appRect.w, pad: 1}, + {type:"h", c:[ + {type: 'v', c:[ + {type:"txt", font:"9%", label:/*LANG*/"Pressure ", pad: 3, halign: -1}, + {type:"txt", font:"9%", label:/*LANG*/"Difference", pad: 3, halign: -1}, + {type:"txt", font:"9%", label:/*LANG*/"Temperature", pad: 3, halign: -1}, + ]}, + {type: 'v', c:[ + {type:"txt", font:"9%", id:"pressure", pad: 3, halign: -1}, + {type:"txt", font:"9%", id:"diff", pad: 3, halign: -1}, + {type:"txt", font:"9%", id:"temp", pad: 3, halign: -1}, + ]} + ]}, + ] +}, {lazy:true}); + +function draw(temperature) { + const history3 = storage.readJSON("zambretti.log.json", true) || []; // history of recent 3 hours + const pressure_cur = history3[history3.length-1].p; + const pressure_last = history3[0].p; + const diff = pressure_cur - pressure_last; + const pressure_sea = Math.round(pressure_cur * Math.pow(1 - (0.0065 * height) / (temperature + (0.0065 * height) + 273.15),-5.257)); + + layout.forecast.label = ZAMBRETTI_FORECAST[get_zambretti_letter(pressure_sea, diff)]; + layout.pressure.label = pressure_sea; + layout.diff.label = diff; + layout.temp.label = require("locale").number(temperature,1); + layout.render(); + //layout.debug(); + + mainScreen = true; + Bangle.setUI({ + mode: "custom", + btn: (n) => {mainScreen = false; showMenu();}, + }); +} + +function show() { + Bangle.getPressure().then(p =>{if (p) draw(p.temperature);}); +} + +function history() { + const interval = 15; // minutes + const history3 = require('Storage').readJSON("zambretti.log.json", true) || []; // history of recent 3 hours + + const now = new Date()/(1000); + let curtime = now-3*60*60; // 3h ago + const data = []; + while (curtime <= now) { + // find closest value in history for this timestamp + const closest = history3.reduce((prev, curr) => { + return (Math.abs(curr.ts - curtime) < Math.abs(prev.ts - curtime) ? curr : prev); + }); + data.push(closest.p); + curtime += interval*60; + } + + Bangle.setUI({ + mode: "custom", + back: () => showMenu(), + }); + + g.reset().setFont("6x8",1); + require("graph").drawLine(g, data, { + axes: true, + x: 4, + y: Bangle.appRect.y+8, + height: Bangle.appRect.h-20, + gridx: 1, + gridy: 1, + miny: Math.min.apply(null, data)-1, + maxy: Math.max.apply(null, data)+1, + title: /*LANG*/"Barometer history (mBar)", + ylabel: y => y, + xlabel: i => { + const t = -3*60 + interval*i; + if (t % 60 === 0) { + return "-" + t/60 + "h"; + } + return ""; + }, + }); +} + +g.reset().clear(); +loadSettings(); +Bangle.loadWidgets(); + +if (height === undefined) { + // setting of height required + eval(require('Storage').read('zambretti.settings.js'))(() => { + loadSettings(); + show(); + }); +} else { + show(); +} +Bangle.drawWidgets(); + +// Update every 15 minutes +setInterval(() => { + if (mainScreen) { + show(); + } +}, 15*60*1000); diff --git a/apps/zambretti/app.png b/apps/zambretti/app.png new file mode 100644 index 0000000000000000000000000000000000000000..8db4fb8f80025d946d1532929de170ad46422a65 GIT binary patch literal 716 zcmV;-0yF)IP)UmR65Re-IO6P#eh30Q|1V{`8Jb}l@fVZcCtx@+{j5n_f0sxu=dtj!Wc{_yt z+#m)UhqsN@HeSrP%HB?XZznjdN~`BP0mz;i%!-BEp0@_I69DV_KWA%CVkk{d;9YjE zno`w@$s(j=gILnUa$|N0Cwd29z1ui#9&6il0#G^MI9Gmk74sk0L9AWNWGS0myArqr zIC&eya-?}`K`5z{k~{r_u=c#o;7)lQ`BD;@!Y0y%pU4z8kT1<(r+mHffBC~r&*sSn z(Z3`Tv4*kwmocoYy*Sdy^4d$xe>%Et+)FC2R}kwZ#6b#+@wVoBQKpiKo&Yo;YyjJd zvA&z9=1@~#0P=61`2s*n*82jGSuXkmz_QmDfVBGIQv$j-2_#o>idf$xrE{cY*Hrs; ze-hBv9BdKmr!$4Uvt5}txpvuP`R12latG1Y>qh71ttaDv&C@?^CF7pkCJ1+)4kw|c zN(#em0UA9Y0K?>%g1+A#KVh~;aFYeX#9W(zppmH9}KB1MX1c+hY3>;8rh58J!|0000HbA literal 0 HcmV?d00001 diff --git a/apps/zambretti/boot.js b/apps/zambretti/boot.js new file mode 100644 index 000000000..3d23e2a44 --- /dev/null +++ b/apps/zambretti/boot.js @@ -0,0 +1,106 @@ +{ + // Copied from widbaroalarm + const LOG_FILE = "zambretti.log.json"; + const history3 = require('Storage').readJSON(LOG_FILE, true) || []; // history of recent 3 hours + let currentPressures = []; + + isValidPressureValue = (pressure) => { + return !(pressure == undefined || pressure <= 0); + }; + + calculcate3hAveragePressure = () => { + if (history3 != undefined && history3.length > 0) { + let sum = 0; + for (let i = 0; i < history3.length; i++) { + sum += history3[i].p; + } + threeHourAvrPressure = sum / history3.length; + } else { + threeHourAvrPressure = undefined; + } + }; + + handlePressureValue = (pressure) => { + if (pressure == undefined || pressure <= 0) { + return; + } + + const ts = Math.round(Date.now() / 1000); // seconds + const d = {"ts" : ts, "p" : pressure}; + + history3.push(d); + + // delete oldest entries until we have max 50 + while (history3.length > 50) { + history3.shift(); + } + + // delete entries older than 3h + for (let i = 0; i < history3.length; i++) { + if (history3[i].ts < ts - (3 * 60 * 60)) { + history3.shift(); + } else { + break; + } + } + + // write data to storage + require('Storage').writeJSON(LOG_FILE, history3); + + calculcate3hAveragePressure(); + }; + + barometerPressureHandler = (e) => { + const MEDIANLENGTH = 20; + while (currentPressures.length > MEDIANLENGTH) + currentPressures.pop(); + + const pressure = e.pressure; + if (isValidPressureValue(pressure)) { + currentPressures.unshift(pressure); + let median = currentPressures.slice().sort(); + + if (median.length > 10) { + var mid = median.length >> 1; + medianPressure = Math.round(E.sum(median.slice(mid - 4, mid + 5)) / 9); + if (medianPressure > 0) { + turnOff(); + handlePressureValue(medianPressure); + } + } + } + }; + + /* + turn on barometer power + take multiple measurements + sort the results + take the middle one (median) + turn off barometer power + */ + getPressureValue = () => { + Bangle.setBarometerPower(true, "zambretti"); + Bangle.on('pressure', barometerPressureHandler); + setTimeout(turnOff, 30000); + }; + + turnOff = () => { + Bangle.removeListener('pressure', barometerPressureHandler); + Bangle.setBarometerPower(false, "zambretti"); + }; + + // delay pressure measurement by interval-lastrun + const interval = 15; // minutes + const lastRun = history3.length > 0 ? history3[history3.length-1].ts : 0; + const lastRunAgo = Math.round(Date.now() / 1000) - lastRun; + let diffNextRun = interval*60-lastRunAgo; + if (diffNextRun < 0) { + diffNextRun = 0; // run asap + } + setTimeout(() => { + if (interval > 0) { + setInterval(getPressureValue, interval * 60000); + } + getPressureValue(); + }, diffNextRun*1000); +} diff --git a/apps/zambretti/metadata.json b/apps/zambretti/metadata.json new file mode 100644 index 000000000..7feb5e3f1 --- /dev/null +++ b/apps/zambretti/metadata.json @@ -0,0 +1,20 @@ +{ + "id": "zambretti", + "name": "Zambretti Weather Forecaster", + "shortName": "Zb. Weather", + "version": "0.01", + "description": "Zambretti Forecaster, uses the Barometer for empirical weather forecast in the Northern Hemisphere (see https://web.archive.org/web/20110610213848/http://www.meteormetrics.com/zambretti.htm), similar to weather stations. Watch must be stationary and its height above sea level set.", + "icon": "app.png", + "tags": "outdoors,weather", + "supports": ["BANGLEJS2"], + "dependencies": {"mylocation":"app"}, + "screenshots": [ {"url":"screenshot.png"} ], + "storage": [ + {"name":"zambretti.app.js","url":"app.js"}, + {"name":"zambretti.settings.js","url":"settings.js"}, + {"name":"zambretti.boot.js","url":"boot.js"}, + {"name":"zambretti.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"zambretti.json"}, {"name":"zambretti.log.json"}] +} + diff --git a/apps/zambretti/screenshot.png b/apps/zambretti/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..1f01fd3d4d5f65190faef5fdf68b6b15ed390a29 GIT binary patch literal 3934 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 back(), + 'Height above sea level (m)': { + value: settings.height, + min: 0, max: 1000, + onchange: v => { + settings.height = v; + writeSettings(); + } + }, + }); +}) diff --git a/lang/de_DE.json b/lang/de_DE.json index 9f9af6b18..5c21cb995 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -228,5 +228,34 @@ "quarter to *$2": "viertel vor *$2", "ten to *$2": "zehn vor *$2", "five to *$2": "fünf vor *$2" + }, + "zambretti": { + "//": "App-specific overrides", + "Settled Fine": "Beständig sonnig", + "Fine Weather": "Sonniges Wetter", + "Becoming Fine": "Es wird schöner", + "Fine Becoming Less Settled": "Sonnig, Tendenz unbeständiger", + "Fine, Possibly showers": "Sonnig, eventuell Schauer", + "Fairly Fine, Improving": "Heiter bis wolkig, Besserung zu erwarten", + "Fairly Fine, Possibly showers, early": "Heiter bis wolkig, anfangs evtl. Schauer", + "Fairly Fine Showery Later": "Heiter bis wolkig, später Regen", + "Showery Early, Improving": "Anfangs noch Schauer, dann Besserung", + "Changeable Mending": "Wechselhaft mit Schauern", + "Fairly Fine, Showers likely": "Heiter bis wolkig, vereinzelt Regen", + "Rather Unsettled Clearing Later": "Unbeständig, spaeter Aufklarung", + "Unsettled, Probably Improving": "Unbeständig, evtl. Besserung.", + "Showery Bright Intervals": "Regnerisch mit heiteren Phasen", + "Showery Becoming more unsettled": "Regnerisch, wird unbeständiger", + "Changeable some rain": "Wechselhaft mit etwas Regen", + "Unsettled, short fine Intervals": "Unbeständig mit heiteren Phasen", + "Unsettled, Rain later": "Unbeständig, später Regen", + "Unsettled, rain at times": "Unbeständig mit etwas Regen", + "Very Unsettled, Finer at times": "Wechselhaft und regnerisch", + "Rain at times, worse later.": "Gelegentlich Regen, Verschlechterung", + "Rain at times, becoming very unsettled": "Zuweilen Regen, sehr unbeständig", + "Rain at Frequent Intervals": "Häufiger Regen", + "Very Unsettled, Rain": "Regen, sehr unbeständig", + "Stormy, possibly improving": "Stürmisch, evtl. Besserung", + "Stormy, much rain": "Stürmisch mit viel Regen" } -} \ No newline at end of file +}