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)
master
Erik Andresen 2025-05-04 09:50:27 +02:00
parent 04023e859b
commit 963fc4970e
8 changed files with 429 additions and 1 deletions

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA/4AB31H//A/hL/ABfABRMD+ALJh5kKn//BRCBCP4ILT/WrBZOq1W/BY/+BYOvBY0DBYZhGh/6BYOrMI0/BYf5qpGGBYWVqtQC4+qqtVqgvF1NVAIIABI4ogBAAdAL4gKEBZg8E/oLiBQpUEgILHJAUFBY4kCioLHQoQKHGAYL4I4RTLNZaDLGA78EAH4AR"))

247
apps/zambretti/app.js Normal file
View File

@ -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);

BIN
apps/zambretti/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

106
apps/zambretti/boot.js Normal file
View File

@ -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);
}

View File

@ -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"}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -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();
}
},
});
})

View File

@ -228,5 +228,34 @@
"quarter to *$2": "viertel vor *$2", "quarter to *$2": "viertel vor *$2",
"ten to *$2": "zehn vor *$2", "ten to *$2": "zehn vor *$2",
"five to *$2": "fünf 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"
} }
} }