Record each forecast change (Zambretti letter transition) with a timestamp into a 24-entry ring buffer, persisted to LittleFS and restored on boot. New /api/forecasts endpoint and a 'Forecast log' panel in the web UI (newest first, category colour, timestamp). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
106 lines
4.0 KiB
C++
106 lines
4.0 KiB
C++
#include <math.h>
|
|
#include "config.h"
|
|
#include "forecast.h"
|
|
|
|
// Full Zambretti phrases, index 0='A' .. 25='Z'.
|
|
static const char* const ZTEXT[26] = {
|
|
"Settled fine weather", // A
|
|
"Fine weather", // B
|
|
"Becoming fine", // C
|
|
"Fine, becoming less settled", // D
|
|
"Fine, possibly showers", // E
|
|
"Fairly fine, improving", // F
|
|
"Fairly fine, possibly showers early", // G
|
|
"Fairly fine, showers later", // H
|
|
"Showery early, improving", // I
|
|
"Changeable, improving", // J
|
|
"Fairly fine, showers likely", // K
|
|
"Rather unsettled, clearing later", // L
|
|
"Unsettled, probably improving", // M
|
|
"Showery, bright intervals", // N
|
|
"Showery, becoming unsettled", // O
|
|
"Changeable, some rain", // P
|
|
"Unsettled, short fine intervals", // Q
|
|
"Unsettled, rain later", // R
|
|
"Unsettled, rain at times", // S
|
|
"Very unsettled, finer at times", // T
|
|
"Rain at times, worse later", // U
|
|
"Rain at times, becoming very unsettled", // V
|
|
"Rain at frequent intervals", // W
|
|
"Very unsettled, rain", // X
|
|
"Stormy, possibly improving", // Y
|
|
"Stormy, much rain" // Z
|
|
};
|
|
|
|
// Map a constrained Zambretti number to a letter, per trend (G6EJD tables).
|
|
static char letterRising(int z) { const char* m = "ABCFGIJLMQTYZ"; return m[z - 1]; } // z 1..13
|
|
static char letterFalling(int z) { const char* m = "ABDHORUXZ"; return m[z - 1]; } // z 1..9
|
|
static char letterSteady(int z) { const char* m = "ABEKNPSWXZ"; return m[z - 1]; } // z 1..10
|
|
|
|
static int clampi(int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); }
|
|
|
|
static WxCategory categoryOf(char letter) {
|
|
switch (letter) {
|
|
case 'A': case 'B': case 'C': case 'F': return WX_FINE;
|
|
case 'E': case 'G': case 'I': case 'J': case 'K':
|
|
case 'M': case 'N': case 'Q': return WX_FAIR;
|
|
case 'D': case 'H': case 'L': case 'O': case 'P': return WX_CHANGEABLE;
|
|
case 'R': case 'S': case 'T': case 'U': case 'V': case 'W':
|
|
case 'X': return WX_RAIN;
|
|
case 'Y': case 'Z': return WX_STORM;
|
|
default: return WX_UNKNOWN;
|
|
}
|
|
}
|
|
|
|
Trend classifyTrend(float d) {
|
|
if (d > TREND_THRESHOLD_HPA) return TREND_RISING;
|
|
if (d < -TREND_THRESHOLD_HPA) return TREND_FALLING;
|
|
return TREND_STEADY;
|
|
}
|
|
|
|
Forecast computeForecast(float p, Trend trend, int month) {
|
|
bool winter = (month < 4 || month > 9); // northern hemisphere
|
|
int z;
|
|
char letter;
|
|
|
|
if (trend == TREND_RISING) {
|
|
z = (int)lround(-0.1449 * p + 150.18);
|
|
if (winter) z += 1;
|
|
z = clampi(z, 1, 13);
|
|
letter = letterRising(z);
|
|
} else if (trend == TREND_FALLING) {
|
|
z = (int)lround(0.0000257935 * p * p * p
|
|
- 0.078482105 * p * p
|
|
+ 79.4582219457 * p
|
|
- 26762.7164899421);
|
|
if (winter) z -= 1;
|
|
z = clampi(z, 1, 9);
|
|
letter = letterFalling(z);
|
|
} else {
|
|
z = (int)lround(0.0000258964 * p * p * p
|
|
- 0.07753778137 * p * p
|
|
+ 77.2287820569 * p
|
|
- 25582.130426005);
|
|
z = clampi(z, 1, 10);
|
|
letter = letterSteady(z);
|
|
}
|
|
|
|
return Forecast{ letter, ZTEXT[letter - 'A'], categoryOf(letter) };
|
|
}
|
|
|
|
const char* forecastTextForLetter(char letter) {
|
|
if (letter < 'A' || letter > 'Z') return "";
|
|
return ZTEXT[letter - 'A'];
|
|
}
|
|
|
|
const char* categoryShort(WxCategory c) {
|
|
switch (c) {
|
|
case WX_FINE: return "Fine";
|
|
case WX_FAIR: return "Fair";
|
|
case WX_CHANGEABLE: return "Changeable";
|
|
case WX_RAIN: return "Rain";
|
|
case WX_STORM: return "Storm";
|
|
default: return "...";
|
|
}
|
|
}
|