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 000000000..8db4fb8f8 Binary files /dev/null and b/apps/zambretti/app.png differ 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 000000000..1f01fd3d4 Binary files /dev/null and b/apps/zambretti/screenshot.png differ diff --git a/apps/zambretti/settings.js b/apps/zambretti/settings.js new file mode 100644 index 000000000..ca8394abe --- /dev/null +++ b/apps/zambretti/settings.js @@ -0,0 +1,25 @@ +(function(back) { + const FILE = "zambretti.json"; + // Load settings + const settings = Object.assign({ + height: 0, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Show the menu + E.showMenu({ + "" : { "title" : "Zambretti Forecast" }, + "< Back" : () => 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 +}