energy_charts.tc¶
energy_charts.tc — die fuenf Webseiten von core2_energy.tc (.76) auf dem P4
// @name: Energie-Grafiken (Seiten des Core2, Daten von der SD-Karte)
// ─────────────────────────────────────────────────────────────────────
// energy_charts.tc — die fuenf Webseiten von core2_energy.tc (.76) auf dem P4
// (.118): Tages Verlauf, Letzte Woche, 52 Wochen, Andere Geraete, Heizung
// 31 Tage. Seiten, Auszuege und Diagramme sind wortgleich uebernommen.
//
// Anders ist nur die QUELLE. Der Core2 holt jeden Bereich auf Abruf von .61,
// ueber Port 80 -- genau der Weg, der .61 blockiert. Hier kommt alles von der
// SD-Karte, die energy_backup.tc (Slot 1) jede Nacht um 00:20 fuellt:
// /energy_m_log.txt Tagesdatenbank, ganz
// /e15_JJJJ-MM.txt 15-Minuten-Werte eines Monats, ROH (Ortszeit wie .61,
// mit einer Stunde Ueberhang in den Folgemonat)
// Ohne Netz, ohne .61. Fehlt ein Monat (Geschichte noch nicht nachgeholt:
// EBAKALL), sagt die Seite das.
//
// Gleiche Zeilen wie auf dem Core2: .61 liefert einen Bereich OHNE das Ende,
// fileExtract nimmt das Ende MIT. Darum endet jeder Bereich, der auf dem Core2
// "heute 00:00" hiess, hier um 23:59 des Vortags.
//
// Das Live-Diagramm der vier Wechselrichter steht in energy_dashboard.tc
// (Slot 0): die Firmware leitet jeden webOn-Endpunkt an Slot 0 weiter, egal
// wer ihn anmeldet -- /tc_array aus diesem Slot bekam nie eine Antwort.
//
// Nicht uebernommen: Hauptseiten-Tabelle (hat energy_dashboard.tc in Slot 0),
// Core2-Bildschirm, Ton, Daikin-Abfrage.
//
// ⚠️ RINGFORMEL: WebChart liest seit Firmware 24.06.2026 arr[(pos+i) % count].
// Der Core2 legt Tageswerte an [1..count] ab und liest mit pos = count+1 -- das
// letzte Feld [count] ist damit unerreichbar, statt ihm kommt [0]. Darum steht
// nach jedem Auszug der letzte Wert zusaetzlich in [0] (Wochentabelle, Tages-
// bilanz, 52 Wochen, 31 Tage), und die 168-Stunden-Linien lesen ab pos 1.
//
// HEIZUNG: Seit Oktober 2023 heizt die Waermepumpe (HWP), Gas steht auf 0. Die
// Warmwasser macht eine eigene Brauchwasser-WP (BWWP); Gas deckte frueher
// beides. Die Wochenseite zeigt darum neben Gas die SUMME beider als "Waerme"
// (Leistungslinie, Tagesbalken, Tabelle mit "Waerme E" zum Strompreis, das
// Gegenstueck zu "Gas E"). Der HWP-Zaehler meldet im Sommer 0, weil die HWP aus
// ist; sein Stand bleibt erhalten. fileExtract rechnete bei der Rueckkehr den
// ganzen Zaehlerstand als Tagesverbrauch (3498 kWh am 1.10.24). hwpDeltas()
// behandelt 0 als fehlend und rechnet vom letzten gueltigen Stand weiter.
//
// Zwei Fehler des Originals sind behoben: Woche, 52 Wochen und 31 Tage teilen
// sich die w_*-Felder, Tagesbilanz und Woche die wt_*-Felder; wer zuletzt lud,
// stand ungeprueft auf der anderen Seite. Jetzt merkt sich wFor/wtFor, wessen
// Daten gerade darin liegen. Und die Tagesbilanz las /daylog.txt, die es auf
// dem Core2 nie gab.
// ─────────────────────────────────────────────────────────────────────
#define PRICE_GAS_KWH 12 // cents per kWh (gas: cbm * 9.9 = kwh)
#define PRICE_STROM 40 // cents per kWh electricity (wie core2_energy.tc)
#define MLOG "/energy_m_log.txt"
#define TMP15 "/ch15.txt"
// Schaltuhr Garage + Schlafzimmer G kommen per UDP (Seite "Andere Geraete")
global char tv2_ip[16];
global char sug_ip[16];
// ═══════════════════════════════════════════════════════════════════
// Felder -- wortgleich aus core2_energy.tc
// ═══════════════════════════════════════════════════════════════════
float t_wbx[97];
float t_wr1[97];
float t_wr2[97];
float t_wr3[97];
float t_wr4[97];
float t_hv[97];
float t_atmp[97];
float t_ktmp[97];
float t_ss[97];
float t_pw[97];
float t_hzwp[97];
float t_bwwp[97];
float t_ni[97];
float t_gas[97];
float t_wa[97];
float t_wpx[97];
float t_rain[97];
// ═══════════════════════════════════════════════════════════════════
// TIME-SERIES ARRAYS for weekly charts (168 = hours per week)
// ═══════════════════════════════════════════════════════════════════
float w_atmp[169];
float w_bwp[169];
float w_hwp[169];
float w_gwp[169];
float w_rain[169];
float w_pwl[169];
float w_sin[169];
float w_swa[169];
float w_hv[169];
float w_at[169];
float w_sk[169];
float w_wr1[169];
float w_wr2[169];
float w_wr3[169];
float w_wr4[169];
float w_gas[169];
float w_pgas[169];
float w_wa[169];
float w_avgt[169];
float w_bwwp[169];
// ─── Dummy array for unused fxto columns ───
float a_du[169];
// ═══════════════════════════════════════════════════════════════════
// WEEKLY SUMMARY ARRAYS (7 days)
// ═══════════════════════════════════════════════════════════════════
float wt_hv[8];
float wt_sin1[8];
float wt_sin2[8];
float wt_sin3[8];
float wt_sin4[8];
float wt_he[8];
float wt_wb[8];
float wt_wp[8];
float wt_bwwp[8];
float wt_hwp[8];
float wt_rain[8];
float wt_tmp[8];
float wt_gas[8];
float wt_pgas[8];
float wt_wa[8];
// ═══════════════════════════════════════════════════════════════════
// Zustand
// ═══════════════════════════════════════════════════════════════════
int dotask; // 1 Woche, 2 Tag, 3 52 Wochen, 4 31 Tage
int dy_loaded; // Tagesseite geladen
char d_day[32]; // gewaehlter Tag (ISO)
char dy_tag[32]; // fuer diesen Tag geladen
int dy_heute; // ... an diesem Tag (heuteNr)
int dy_fehlt; // Monatsdatei fehlt
char dy_datei[40];
int wk_fehlt;
char wk_datei[40];
int wFor; // w_* enthaelt: 0 nichts, 1 Woche, 2 52 Wochen, 3 31 Tage
int wtFor; // wt_* enthaelt: 0 nichts, 1 Woche, 2 Tagesbilanz
int w_heute; // w_* geladen an diesem Tag
int d52;
int wk_drows;
int d_pos;
int d_cnt;
int db_loaded;
char db_min[32];
char db_max[32];
int db_rows;
int tmp;
int res;
int dres;
char buf[256];
char str[128];
char s1[32];
char s2[32];
char sx1[32];
char sx2[32];
char fnam[48];
char lwstr[80];
char tmin[32];
char tmax[32];
char tval[32];
char dx1[32];
// HWP-Zaehler roh und als Tagesdifferenz (hwpDeltas)
float hraw[400];
float hdel[400];
float wt_phwp[8];
float wt_warm[8]; // HWP + BWWP je Tag (kWh)
// Statuszeile auf der Hauptseite, alle 10 Minuten im TaskLoop neu bestimmt
char stZeile[160] = "Stand wird ermittelt";
int stNaechste = 0; // Uptime der naechsten Ermittlung
// Zeilenleser (zum Zusammenfuegen zweier Monate)
char rb[1024]; int rbLen = 0; int rbPos = 0;
char line[800];
// ═══════════════════════════════════════════════════════════════════
// Hilfen
// ═══════════════════════════════════════════════════════════════════
int heuteNr() {
char t[32];
timeStamp(t);
return timeToSecs(t) / 86400;
}
// ISO-Mitternacht -> 23:59 desselben Tages (in place)
void aufTagesende(char t[]) {
t[11] = '2'; t[12] = '3'; t[14] = '5'; t[15] = '9';
}
// "/e15_JJJJ-MM.txt" zum ISO-Stempel
void monatsDatei(char dst[], char iso[]) {
char ym[8];
strSub(ym, iso, 0, 7);
strcpy(dst, "/e15_");
strcat(dst, ym);
strcat(dst, ".txt");
}
// 1, wenn line mit p beginnt (strStartsWith will ein Literal)
int beginntMit(char p[]) {
int i = 0;
while (p[i] != 0) { if (line[i] != p[i]) { return 0; } i = i + 1; }
return 1;
}
int zeileLesen(int h) {
int n = 0;
while (1) {
if (rbPos >= rbLen) {
rbLen = fileRead(h, rb, 1024); rbPos = 0;
if (rbLen <= 0) { line[n] = 0; if (n > 0) { return n; } return -1; }
}
char c = rb[rbPos]; rbPos = rbPos + 1;
if (c == '\n') { line[n] = 0; return n; }
if (c != '\r' && n < 798) { line[n] = c; n = n + 1; }
}
return -1;
}
// 15-Minuten-Quelle fuer den Bereich von..bis (ISO) nach fnam. Liegt er in
// einem Monat, ist es dessen Datei. Sonst werden beide Monate nach TMP15
// gefuegt; der Ueberhang des ersten Monats (der 1. des zweiten ab 00:00)
// bleibt dabei weg, sonst kaeme diese Stunde doppelt. 1 = ok, 0 = fehlt
// (fehlende Datei steht dann in fnam).
int quelle15(char von[], char bis[]) {
char fa[40]; char fb[40];
monatsDatei(fa, von);
monatsDatei(fb, bis);
if (strcmp(fa, fb) == 0) {
strcpy(fnam, fa);
return fileExists(fa);
}
if (!fileExists(fa)) { strcpy(fnam, fa); return 0; }
if (!fileExists(fb)) { strcpy(fnam, fb); return 0; }
// "1.M.JJ " -- so beginnt jede Zeile vom 1. des zweiten Monats
char mo[4]; char jj[4]; char pref[16];
strSub(mo, bis, 5, 2);
strSub(jj, bis, 2, 2);
sprintf(pref, "1.%d.%s ", strToInt(mo), jj);
char nl[2] = "\n";
fileDelete(TMP15);
int ho = fileOpen(TMP15, "w");
if (ho < 0) { strcpy(fnam, TMP15); return 0; }
int hi = fileOpen(fa, "r");
rbLen = 0; rbPos = 0;
while (hi >= 0 && zeileLesen(hi) >= 0) {
if (beginntMit(pref)) { break; }
fileWrite(ho, line, strlen(line)); fileWrite(ho, nl, 1);
}
if (hi >= 0) { fileClose(hi); }
hi = fileOpen(fb, "r");
rbLen = 0; rbPos = 0;
while (hi >= 0 && zeileLesen(hi) >= 0) {
if (strStartsWith(line, "TSTAMP")) { continue; }
fileWrite(ho, line, strlen(line)); fileWrite(ho, nl, 1);
}
if (hi >= 0) { fileClose(hi); }
fileClose(ho);
strcpy(fnam, TMP15);
return 1;
}
// HWP-Zaehler (Spalte 28 der Tagesdatenbank) roh holen und daraus die
// Differenzen bilden wie fileExtract (hdel[k] = Zeile k - Zeile k-1), aber
// eine 0 ist "HWP aus, kein Wert": sie liefert 0, und der naechste gueltige
// Stand zaehlt ab dem letzten gueltigen. Liefert die Zeilenzahl.
int hwpDeltas(char von[], char bis[]) {
int fr = fileOpen(MLOG, "r");
if (fr < 0) { return 0; }
int rows = fileExtract(fr, von, bis, 28, 1, hraw);
fileClose(fr);
float last = 0.0;
int i = 0;
while (i < rows) {
float v = hraw[i];
float d = 0.0;
if (v > 0.0) {
if (last > 0.0 && v >= last) { d = v - last; }
last = v;
}
hdel[i] = d;
i = i + 1;
}
return rows;
}
// Spanne der Tagesdatenbank fuer "Datenbank vom ... bis ..."
void dbSpanne() {
int fr = fileOpen(MLOG, "r");
if (fr < 0) { db_loaded = 0; return; }
db_rows = fileRange(fr, db_min, db_max);
fileClose(fr);
timeConvert(db_min, 1);
strToken(str, db_min, ' ', 1);
strcpy(db_min, str);
timeConvert(db_max, 1);
strToken(str, db_max, ' ', 1);
strcpy(db_max, str);
db_loaded = 1;
}
// ═══════════════════════════════════════════════════════════════════
// Laden -- Auszuege wortgleich aus core2_energy.tc, Quelle SD-Karte
// ═══════════════════════════════════════════════════════════════════
void loadDayData() {
int fr;
int rows;
int i;
if (strlen(d_day) > 0) {
strcpy(s1, d_day);
} else {
timeStamp(s1);
timeOffset(s1, -1, 1); // yesterday midnight
}
strcpy(s2, s1);
aufTagesende(s2); // Core2: .61 bis Folgetag 00:00 ohne Ende
d_cnt = 0; d_pos = 0;
dy_fehlt = 0;
if (quelle15(s1, s2)) {
fr = fileOpen(fnam, "r");
if (fr >= 0) {
// col_offs=1, accum=-1 — starts at WB (first data col in 15-min file)
rows = fileExtract(fr, s1, s2, 1, -1,
t_wbx, t_wr1, t_wr2, t_wr3, t_atmp,
a_du, a_du, a_du, a_du,
t_ss, t_wr4, t_ni,
a_du,
t_wa, t_gas, t_pw,
a_du, a_du, a_du,
t_hv,
a_du, a_du, a_du, t_wpx,
t_bwwp,
t_rain, a_du,
t_hzwp, t_ktmp, a_du);
fileClose(fr);
sprintf(buf, "ECH: day rows=%d", rows);
addLog(buf);
// Scale counters to watts: value * 4000 (15min → hourly rate)
d_cnt = rows;
d_pos = rows;
i = 1;
while (i <= rows) {
t_wbx[i] = t_wbx[i] * 4000.0;
t_wr1[i] = t_wr1[i] * 4000.0;
t_wr2[i] = t_wr2[i] * 4000.0;
t_wr3[i] = t_wr3[i] * 4000.0;
t_wr4[i] = t_wr4[i] * 4000.0;
t_ni[i] = t_ni[i] * 4000.0;
t_wpx[i] = t_wpx[i] * 4000.0;
t_bwwp[i] = t_bwwp[i] * 4000.0;
t_hzwp[i] = t_hzwp[i] * 4000.0;
i = i + 1;
}
}
} else {
dy_fehlt = 1;
strcpy(dy_datei, fnam);
}
// Energiebilanz des Tages aus der Tagesdatenbank: Zeile des Tages bis
// Zeile des Folgetags (die Zeile "D 0:00" schreibt .61 um Mitternacht)
strcpy(sx1, s1);
strcpy(sx2, sx1);
timeOffset(sx2, 1, 1);
fr = fileOpen(MLOG, "r");
if (fr >= 0) {
rows = fileExtract(fr, sx1, sx2, 1, -1,
wt_wb, wt_sin1, wt_sin2, wt_sin3, wt_tmp,
a_du, a_du, a_du, a_du, a_du,
wt_sin4, wt_hv, a_du, wt_wa, wt_gas,
a_du, a_du, a_du, a_du, a_du,
a_du, a_du, a_du, wt_wp, wt_bwwp, wt_rain, a_du, wt_hwp, a_du, a_du);
fileClose(fr);
sprintf(buf, "ECH: bilanz rows=%d", rows);
addLog(buf);
if (hwpDeltas(sx1, sx2) > 1) { wt_hwp[1] = hdel[1]; } else { wt_hwp[1] = 0.0; }
// Ringformel: WebChart(pos 2, count 1) liest [0], der Tag steht in [1]
wt_hv[0] = wt_hv[1]; wt_wb[0] = wt_wb[1]; wt_sin1[0] = wt_sin1[1];
wt_sin2[0] = wt_sin2[1]; wt_sin3[0] = wt_sin3[1]; wt_sin4[0] = wt_sin4[1];
wt_gas[0] = wt_gas[1]; wt_wa[0] = wt_wa[1]; wt_wp[0] = wt_wp[1];
wt_bwwp[0] = wt_bwwp[1]; wt_hwp[0] = wt_hwp[1]; wt_rain[0] = wt_rain[1];
}
wtFor = 2;
dbSpanne();
}
void loadWeekData() {
int fr;
int rows;
// ── 15-Minuten-Werte, 7 Tage, stuendlich gemittelt ──
timeStamp(s1);
timeOffset(s1, -7, 1); // 7 days ago midnight
timeStamp(s2);
timeOffset(s2, -1, 1);
aufTagesende(s2); // gestern 23:59
wk_fehlt = 0;
dres = 0;
if (quelle15(s1, s2)) {
fr = fileOpen(fnam, "r");
if (fr >= 0) {
// col_offs=2, accum=-4 (average 4 rows -> hourly)
rows = fileExtract(fr, s1, s2, 2, -4,
w_wr1, w_wr2, w_wr3, w_at,
a_du, a_du, a_du,
w_sk, w_swa, w_wr4,
a_du, a_du,
w_wa, w_gas, w_pwl,
a_du,
w_sin,
a_du,
w_hv,
a_du, a_du, a_du, a_du, // RCAP_a KLI_a PWP_a WP
w_bwwp, // BWWP-Zaehler (x4000 -> W)
a_du, // RAIN
w_hwp); // HWP_a: Leistung der HWP (W)
fileClose(fr);
sprintf(buf, "ECH: 15min rows=%d", rows);
addLog(buf);
// Scale solar inverter values: counter increments * 4000 = watts
int i;
int maxr;
maxr = rows;
if (maxr > 168) { maxr = 168; }
i = 1;
while (i <= maxr) {
w_wr1[i] = w_wr1[i] * 4000.0;
w_wr2[i] = w_wr2[i] * 4000.0;
w_wr3[i] = w_wr3[i] * 4000.0;
w_wr4[i] = w_wr4[i] * 4000.0;
// Waerme: HWP-Leistung + BWWP (Zaehlerdifferenz -> W) in w_hwp
w_hwp[i] = w_hwp[i] + w_bwwp[i] * 4000.0;
i = i + 1;
}
// Ringformel: die Linien lesen ab pos 1 -> [1..167], dann [0]. Die
// 168. Stunde steht in [168]; sie kommt nach [0]. (Der Core2 setzte
// [0] auf einen Scripter-Zeitversatz und verlor die letzte Stunde.)
int le;
le = maxr;
w_at[0] = w_at[le]; w_sk[0] = w_sk[le]; w_swa[0] = w_swa[le];
w_pwl[0] = w_pwl[le]; w_hv[0] = w_hv[le]; w_sin[0] = w_sin[le];
w_wr1[0] = w_wr1[le]; w_wr2[0] = w_wr2[le]; w_wr3[0] = w_wr3[le];
w_wr4[0] = w_wr4[le]; w_wa[0] = w_wa[le]; w_gas[0] = w_gas[le];
w_hwp[0] = w_hwp[le];
}
} else {
wk_fehlt = 1;
strcpy(wk_datei, fnam);
}
dbSpanne();
// Extract weekly data (8 days, delta → 7 rows)
timeStamp(s1);
timeOffset(s1, -8, 1); // 8 days ago midnight
timeStamp(s2);
timeOffset(s2, -1, 1);
aufTagesende(s2); // gestern 23:59 (Core2: bis heute 00:00 ohne Ende)
fr = fileOpen(MLOG, "r");
if (fr >= 0) {
rows = fileExtract(fr, s1, s2, 1, -1,
wt_wb, wt_sin1, wt_sin2, wt_sin3, wt_tmp,
a_du, a_du, a_du, a_du, a_du,
wt_sin4, wt_hv, a_du, wt_wa, wt_gas,
a_du, a_du, a_du, a_du, a_du,
a_du, a_du, a_du, wt_wp, wt_bwwp);
fileClose(fr);
wk_drows = rows;
sprintf(buf, "ECH: daily rows=%d", rows);
addLog(buf);
int i;
i = 1;
while (i <= rows) {
wt_pgas[i] = wt_gas[i] * 10.0 * PRICE_GAS_KWH / 100.0;
i = i + 1;
}
// HWP je Tag (Sommer-0 als fehlend) und in Euro zum Strompreis
int hn;
hn = hwpDeltas(s1, s2);
i = 1;
while (i < 8) {
wt_hwp[i] = 0.0;
if (i < hn) { wt_hwp[i] = hdel[i]; }
wt_warm[i] = wt_hwp[i] + wt_bwwp[i];
wt_phwp[i] = wt_warm[i] * PRICE_STROM / 100.0;
i = i + 1;
}
// Ringformel: pos 8, count 7 liest 1..6, dann 0 -- der 7. Tag steht in
// [7] und kommt nach [0] (der Core2 schrieb dort den Wochentag hin)
if (rows > 1) {
int le;
le = rows - 1;
wt_hv[0] = wt_hv[le]; wt_wb[0] = wt_wb[le]; wt_sin1[0] = wt_sin1[le];
wt_sin2[0] = wt_sin2[le]; wt_sin3[0] = wt_sin3[le]; wt_sin4[0] = wt_sin4[le];
wt_wa[0] = wt_wa[le]; wt_gas[0] = wt_gas[le]; wt_wp[0] = wt_wp[le];
wt_pgas[0] = wt_pgas[le];
wt_hwp[0] = wt_hwp[le]; wt_phwp[0] = wt_phwp[le];
wt_bwwp[0] = wt_bwwp[le]; wt_warm[0] = wt_warm[le];
}
}
wFor = 1;
wtFor = 1;
}
void load52WeekData() {
int fr;
int rows;
fr = fileOpen(MLOG, "r");
if (fr < 0) { return; }
fileRange(fr, tmin, tmax);
fileClose(fr);
int tmin_s;
int tmax_s;
tmin_s = timeToSecs(tmin);
tmax_s = timeToSecs(tmax);
int days;
days = (tmax_s - tmin_s) / 86400;
days = days - (days % 7);
d52 = days / 7;
if (d52 > 52) { d52 = 52; }
int ndays;
ndays = d52 * 7 + 1;
timeStamp(s1);
timeOffset(s1, -ndays, 1);
timeStamp(s2);
timeOffset(s2, -1, 1);
aufTagesende(s2);
// Title
strcpy(dx1, s1);
timeConvert(dx1, 1);
sprintf(lwstr, "%d Wochen seit ", d52);
strcat(lwstr, dx1);
fr = fileOpen(MLOG, "r");
if (fr >= 0) {
rows = fileExtract(fr, s1, s2, 1, -7,
w_pwl, w_wr1, w_wr2, w_wr3,
a_du, a_du, a_du, a_du, a_du, a_du,
w_wr4, w_sin, w_swa, w_wa, w_gas,
a_du, a_du, a_du, a_du, a_du,
a_du, a_du, a_du,
w_at, w_bwwp, w_rain,
a_du, w_hwp, a_du, w_avgt);
fileClose(fr);
d52 = rows;
// HWP neu, Sommer-0 als fehlend: Wochenmittel wie fileExtract (-7)
{
int hn;
hn = hwpDeltas(s1, s2);
int j; int k; int c; float sum;
j = 1; k = 1; c = 0; sum = 0.0;
while (k < hn) {
sum = sum + hdel[k]; c = c + 1;
if (c == 7) { w_hwp[j] = sum / 7.0; j = j + 1; sum = 0.0; c = 0; }
k = k + 1;
}
if (c > 0) { w_hwp[j] = sum / c; }
}
int i;
i = 1;
while (i < rows) {
w_pgas[i] = w_gas[i] * 10.0 * PRICE_GAS_KWH / 100.0;
i = i + 1;
}
// Ringformel: pos d52, count d52-1 liest 1..d52-2, dann 0 -- die
// letzte Woche [d52-1] kommt nach [0] (der Core2 loeschte [0])
if (d52 > 1) {
int le;
le = d52 - 1;
w_wr1[0] = w_wr1[le]; w_wr2[0] = w_wr2[le]; w_wr3[0] = w_wr3[le]; w_wr4[0] = w_wr4[le];
w_pwl[0] = w_pwl[le]; w_at[0] = w_at[le]; w_sin[0] = w_sin[le]; w_swa[0] = w_swa[le];
w_wa[0] = w_wa[le]; w_gas[0] = w_gas[le]; w_bwwp[0] = w_bwwp[le]; w_rain[0] = w_rain[le];
w_hwp[0] = w_hwp[le]; w_avgt[0] = w_avgt[le]; w_pgas[0] = w_pgas[le];
}
}
wFor = 2;
}
void load31DayData() {
int fr;
int rows;
fr = fileOpen(MLOG, "r");
if (fr < 0) { return; }
fileRange(fr, tmin, tmax);
fileClose(fr);
int tmin_s;
int tmax_s;
tmin_s = timeToSecs(tmin);
tmax_s = timeToSecs(tmax);
int days;
days = (tmax_s - tmin_s) / 86400;
if (days > 31) { days = 31; }
d52 = days;
int ndays;
ndays = days + 2;
timeStamp(s1);
timeOffset(s1, -ndays, 1);
timeStamp(s2);
timeOffset(s2, -1, 1);
aufTagesende(s2);
strcpy(dx1, s1);
timeConvert(dx1, 1);
sprintf(lwstr, "%d Tage seit ", days);
strcat(lwstr, dx1);
fr = fileOpen(MLOG, "r");
if (fr >= 0) {
rows = fileExtract(fr, s1, s2, 1, -1,
w_pwl, w_wr1, w_wr2, w_wr3,
a_du, a_du, a_du, a_du, a_du, a_du,
w_wr4, w_sin, w_swa, w_wa, w_gas,
a_du, a_du, a_du, a_du, a_du,
a_du, a_du, a_du,
w_at, w_bwwp, w_rain,
a_du, w_hwp, a_du, w_atmp);
fileClose(fr);
d52 = rows;
if (d52 > days + 1) { d52 = days + 1; }
// HWP neu, Sommer-0 als fehlend (vor w_gwp = BWWP + HWP)
{
int hn;
hn = hwpDeltas(s1, s2);
int k;
k = 1;
while (k < rows) {
w_hwp[k] = 0.0;
if (k < hn) { w_hwp[k] = hdel[k]; }
k = k + 1;
}
}
w_wr1[0] = 0.0; w_wr2[0] = 0.0; w_wr3[0] = 0.0; w_wr4[0] = 0.0;
w_pwl[0] = 0.0; w_at[0] = 0.0; w_sin[0] = 0.0; w_swa[0] = 0.0;
w_wa[0] = 0.0; w_gas[0] = 0.0; w_bwwp[0] = 0.0; w_rain[0] = 0.0;
w_hwp[0] = 0.0; w_atmp[0] = 0.0; w_gwp[0] = 0.0; w_sk[0] = 0.0;
int i;
i = 1;
while (i < rows) {
w_gwp[i] = w_bwwp[i] + w_hwp[i];
w_sk[i] = w_wr1[i] + w_wr2[i] + w_wr3[i];
i = i + 1;
}
// Die JUENGSTEN Tage zeigen: der Auszug liefert einen Tag mehr, als
// die Seite zeigt, und die Kappung oben schnitt das Ende ab -- auf dem
// Core2 fehlte so immer der Vortag. Das Fenster rueckt ans Ende.
int off;
off = rows - d52;
if (off > 0) {
int k;
k = 1;
while (k < d52) {
int q;
q = k + off;
w_wr1[k] = w_wr1[q]; w_wr2[k] = w_wr2[q]; w_wr3[k] = w_wr3[q]; w_wr4[k] = w_wr4[q];
w_pwl[k] = w_pwl[q]; w_at[k] = w_at[q]; w_sin[k] = w_sin[q]; w_swa[k] = w_swa[q];
w_wa[k] = w_wa[q]; w_gas[k] = w_gas[q]; w_bwwp[k] = w_bwwp[q]; w_rain[k] = w_rain[q];
w_hwp[k] = w_hwp[q]; w_atmp[k] = w_atmp[q]; w_gwp[k] = w_gwp[q]; w_sk[k] = w_sk[q];
k = k + 1;
}
strcpy(dx1, s1);
timeOffset(dx1, off, 1);
timeConvert(dx1, 1);
sprintf(lwstr, "%d Tage seit ", days);
strcat(lwstr, dx1);
}
// Ringformel: letzter Tag [d52-1] zusaetzlich nach [0]
if (d52 > 1) {
int le;
le = d52 - 1;
w_wr1[0] = w_wr1[le]; w_wr2[0] = w_wr2[le]; w_wr3[0] = w_wr3[le]; w_wr4[0] = w_wr4[le];
w_pwl[0] = w_pwl[le]; w_at[0] = w_at[le]; w_sin[0] = w_sin[le]; w_swa[0] = w_swa[le];
w_wa[0] = w_wa[le]; w_gas[0] = w_gas[le]; w_bwwp[0] = w_bwwp[le]; w_rain[0] = w_rain[le];
w_hwp[0] = w_hwp[le]; w_atmp[0] = w_atmp[le]; w_gwp[0] = w_gwp[le]; w_sk[0] = w_sk[le];
}
}
wFor = 3;
}
// ═══════════════════════════════════════════════════════════════════
// TaskLoop -- Dateiarbeit nicht im Webaufruf
// ═══════════════════════════════════════════════════════════════════
// Wie weit reicht die Tagesdatenbank, wie viele Monate liegen lokal?
// (auch der Fortschritt von EBAKALL). Liest nur Verzeichniseintraege und
// einmal die Spanne der Tagesdatenbank.
void statusErmitteln() {
char ts[24]; timeStamp(ts);
int yj = (ts[0]-'0')*1000 + (ts[1]-'0')*100 + (ts[2]-'0')*10 + (ts[3]-'0');
int ym = (ts[5]-'0')*10 + (ts[6]-'0');
int j = 2021; int m = 12; int da = 0; int alle = 0;
char f[40];
while (j < yj || (j == yj && m <= ym)) {
sprintf(f, "/e15_%04d-%02d.txt", j, m);
if (fileExists(f)) { da = da + 1; }
alle = alle + 1;
m = m + 1; if (m > 12) { m = 1; j = j + 1; }
}
dbSpanne();
if (db_loaded == 1) {
sprintf(stZeile, "Tagesdatenbank %s bis %s · 15-Minuten-Monate lokal %d von %d", db_min, db_max, da, alle);
} else {
sprintf(stZeile, "Tagesdatenbank fehlt · 15-Minuten-Monate lokal %d von %d", da, alle);
}
}
void TaskLoop() {
while (1) {
if (dotask == 0 && tasm_uptime >= stNaechste) {
statusErmitteln();
stNaechste = tasm_uptime + 600;
}
if (dotask == 1) {
loadWeekData();
w_heute = heuteNr();
dotask = 0;
}
if (dotask == 2) {
loadDayData();
strcpy(dy_tag, d_day);
dy_heute = heuteNr();
dy_loaded = 1;
dotask = 0;
}
if (dotask == 3) {
load52WeekData();
w_heute = heuteNr();
dotask = 0;
}
if (dotask == 4) {
load31DayData();
w_heute = heuteNr();
dotask = 0;
}
delay(500);
}
}
// ═══════════════════════════════════════════════════════════════════
// WEB — Chart pages (WebUI callback, rendered at /tc_ui?p=N)
// ═══════════════════════════════════════════════════════════════════
void WebUI() {
int pg;
pg = webPage();
// Guard: if TaskLoop is loading data, don't touch shared buffers
if (dotask != 0) {
webSend("<script>var tt=document.querySelector(\"div[style*='c_ttl']\");if(tt)tt.style.display='none';</script>");
webSend("<h3>Daten werden geladen...</h3>");
return;
}
if (pg == 0) {
// ─── Page 0: Daily charts (data from collector DB) ───
// Check for date parameter from date picker
res = webArg("d", tval);
if (res > 0) {
strToken(str, tval, 'T', 1);
strcpy(buf, str);
buf += "T00:00:00";
if (strcmp(buf, d_day) != 0) {
strcpy(d_day, buf);
dy_loaded = 0;
dotask = 2;
webSend("<script>var tt=document.querySelector(\"div[style*='c_ttl']\");if(tt)tt.style.display='none';</script>");
webSend("<h3>Daten werden geladen...</h3>");
return;
}
}
// If no day selected yet, default to yesterday
if (strlen(d_day) == 0) {
timeStamp(d_day);
timeOffset(d_day, -1, 1);
}
// Veraltet? (anderer Tag, Bilanzfelder von der Woche belegt, oder
// seit dem Laden ist Mitternacht vorbei und das Backup brachte Neues)
if (dy_loaded == 1 && (strcmp(d_day, dy_tag) != 0 || wtFor != 2 || dy_heute != heuteNr())) {
dy_loaded = 0;
}
webSend("<script>var tt=document.querySelector(\"div[style*='c_ttl']\");if(tt)tt.style.display='none';</script>");
webSend("<h3>Tages Verlauf</h3>");
webSend("<center>");
// Date picker first
strToken(buf, d_day, 'T', 1);
webSend("<label><b>Tag: </b><input type='date' value='");
webSend(buf);
webSend("' style='width:200px' onchange='window.location=\"/tc_ui?p=0&d=\"+this.value+\"T00:00\"'>");
webSend("</label><br><br>");
// Database range
if (db_loaded == 1) {
webSend("Datenbank vom ");
webSend(db_min);
webSend(" bis ");
webSend(db_max);
webSend("<br><br>");
}
// Selected date
strcpy(str, d_day);
timeConvert(str, 1);
strToken(buf, str, ' ', 1);
webSend("<b>Daten vom ");
webSend(buf);
webSend("</b>");
webSend("</center><br>");
if (dy_loaded == 1) {
// Stop AJAX polling — charts are ready
webSend("<script>rfsh=0;clearTimeout(lt);</script>");
if (dy_fehlt == 1) {
webSend("<center><b style='color:orange'>Für diesen Tag liegen auf der SD-Karte noch keine 15-Minuten-Werte (");
webSend(dy_datei);
webSend(") – der Monat ist noch nicht gesichert.</b></center><br>");
}
// Set time base so x-axis shows 0:00-24:00 of the selected day
// time_base = (d_day_midnight - now_epoch) / 60 + (count-1)*15
timeStamp(sx1); // now
int now_s;
int day_s;
now_s = timeToSecs(sx1);
day_s = timeToSecs(d_day);
int tbase;
tbase = (day_s - now_s) / 60 + (d_cnt - 1) * 15;
WebChartTimeBase(tbase);
// Chart 1: PV power — decimals bit3=smooth (0+8=8)
WebChart('l', "Photovoltaik - kWh", "Dach|W", 0x0000FF, d_pos, d_cnt, t_wr4, 8, 15, 0.0, 4000.0);
WebChart('l', "", "Garage|W", 0xFF0000, d_pos, d_cnt, t_wr1, 8, 15, 0.0, 4000.0);
WebChart('l', "", "Garten Haus|W", 0xFF8800, d_pos, d_cnt, t_wr2, 8, 15, 0.0, 4000.0);
WebChart('l', "", "Garten|W", 0x00AA00, d_pos, d_cnt, t_wr3, 8, 15, 0.0, 4000.0);
// Chart 2: Grid + House
WebChart('l', "Verbrauch - kWh", "Netz|W", 0x0088FF, d_pos, d_cnt, t_ni, 8, 15, 0.0, 5000.0);
WebChart('l', "", "Haus|W", 0xFF0000, d_pos, d_cnt, t_hv, 8, 15, 0.0, 5000.0);
// Chart 3: Powerwall charge
WebChart('l', "Powerwall - %", "Powerwall|%", 0x00CC00, d_pos, d_cnt, t_pw, 8, 15, 0.0, 100.0);
// Chart 4: Temperatures (1 decimal + smooth = 9)
WebChart('l', "Solarthermie - C", "Aussen Temperatur|C", 0x0088FF, d_pos, d_cnt, t_atmp, 9, 15, 0.0, 60.0);
WebChart('l', "", "Solar Speicher|C", 0xCC0000, d_pos, d_cnt, t_ss, 9, 15, 0.0, 60.0);
WebChart('l', "", "Keller Temperatur|C", 0xFF8800, d_pos, d_cnt, t_ktmp, 9, 15, 0.0, 60.0);
// Chart 5: Gas / Wasser (2 decimals + smooth = 10)
WebChart('l', "Gas - Wasser - m3", "Gas|m3", 0xFF0000, d_pos, d_cnt, t_gas, 10, 15, 0.0, 0.3);
WebChart('l', "", "Wasser|m3", 0x0088FF, d_pos, d_cnt, t_wa, 10, 15, 0.0, 0.3);
// Chart 6: Heizung / Brauchwasser WP (smooth)
WebChart('l', "Heizung - Brauchwasser - WP - W", "HWP|W", 0x0088FF, d_pos, d_cnt, t_hzwp, 8, 15, 0.0, 3000.0);
WebChart('l', "", "BWWP|W", 0xFF0000, d_pos, d_cnt, t_bwwp, 8, 15, 0.0, 3000.0);
WebChartTimeBase(0); // reset for non-time charts
// Energiebilanz — from daily log (wt_* arrays, matching Scripter #gload)
// wt_hv[1]=Haus(ZWZI), wt_wb[1]=Wallbox, wt_sin1[1]=Garage,
// wt_sin2[1]=G-Haus, wt_sin3[1]=Garten (missing, use 0),
// wt_sin4[1]=Dach, wt_gas[1]=Gas, wt_wa[1]=Wasser,
// wt_wp[1]=Klima, wt_bwwp[1]=BWWP, wt_hwp[1]=HWP, wt_rain[1]=Regen
// Gas € = Gas * 10 * PRICE_GAS_KWH / 100
// Column chart — uses wt_* arrays directly, pos=2 count=1 reads [1]
WebChart('c', "Energiebilanz", "Haus", 0x0000FF, 2, 1, wt_hv, 2, 0, 0.0, 0.0);
WebChart('c', "", "Wallbox", 0x8800FF, 2, 1, wt_wb, 2, 0, 0.0, 0.0);
WebChart('c', "", "Garage", 0xFF0000, 2, 1, wt_sin1, 2, 0, 0.0, 0.0);
WebChart('c', "", "G-Haus", 0xFF8800, 2, 1, wt_sin2, 2, 0, 0.0, 0.0);
WebChart('c', "", "Garten", 0x00AA00, 2, 1, wt_sin3, 2, 0, 0.0, 0.0);
WebChart('c', "", "Dach", 0x00CCFF, 2, 1, wt_sin4, 2, 0, 0.0, 0.0);
WebChart('c', "", "Gas", 0xFF00FF, 2, 1, wt_gas, 2, 0, 0.0, 0.0);
WebChart('c', "", "Wasser", 0x0088FF, 2, 1, wt_wa, 2, 0, 0.0, 0.0);
WebChart('c', "", "Klima", 0x888800, 2, 1, wt_wp, 2, 0, 0.0, 0.0);
WebChart('c', "", "BWWP", 0xCC0000, 2, 1, wt_bwwp, 2, 0, 0.0, 0.0);
WebChart('c', "", "HWP", 0x0088AA, 2, 1, wt_hwp, 2, 0, 0.0, 0.0);
WebChart('c', "", "Regen", 0x00AA88, 2, 1, wt_rain, 2, 0, 0.0, 0.0);
// Table — use a_du for transposed layout (copy wt values + Gas €)
a_du[1] = wt_hv[1];
a_du[2] = wt_wb[1];
a_du[3] = wt_sin1[1];
a_du[4] = wt_sin2[1];
a_du[5] = wt_sin3[1];
a_du[6] = wt_sin4[1];
a_du[7] = wt_gas[1];
a_du[8] = wt_gas[1] * 10.0 * PRICE_GAS_KWH / 100.0;
a_du[9] = wt_wa[1];
a_du[10] = wt_wp[1];
a_du[11] = wt_bwwp[1];
a_du[12] = wt_hwp[1];
a_du[13] = wt_rain[1];
a_du[0] = a_du[13]; // Ringformel: pos 14, count 13 liest 1..12, dann 0
WebChart('t', "Energiebilanz|Haus|Wallbox|Garage|G-Haus|Garten|Dach|Gas|Gas E|Wasser|Klima|BWWP|HWP|Regen", "Bilanz", 0, 14, 13, a_du, 2, 0, 0.0, 0.0);
} else {
dotask = 2;
webSend("<p>Daten werden geladen...</p>");
}
}
if (pg == 1) {
// ─── Page 1: Letzte Woche (7-day hourly line charts) ───
webSend("<script>var tt=document.querySelector(\"div[style*='c_ttl']\");if(tt)tt.style.display='none';rfsh=0;clearTimeout(lt);</script>");
webSend("<h3>Letzte Woche</h3>");
if (db_loaded == 1) {
webSend("<center>Datenbank vom ");
webSend(db_min);
webSend(" bis ");
webSend(db_max);
webSend("</center><br>");
}
if (wFor == 1 && wtFor == 1 && w_heute == heuteNr() && dotask == 0) {
// Chart 1: Energy balance column chart
if (wk_fehlt == 1) {
webSend("<center><b style='color:orange'>15-Minuten-Werte der Woche fehlen auf der SD-Karte (");
webSend(wk_datei);
webSend(").</b></center><br>");
}
// pos=wk_drows reads last 7 deltas, tbase=-1440 shifts labels to yesterday
WebChartTimeBase(-1440);
WebChart('c', "Energie Bilanz", "Netz", 0xFF0000, wk_drows, 7, wt_hv, 1, 1440, 0.0, 0.0);
WebChart('c', "", "Wallbox", 0x0088FF, wk_drows, 7, wt_wb, 1, 1440, 0.0, 0.0);
WebChart('c', "", "Garage", 0x00AA00, wk_drows, 7, wt_sin1, 1, 1440, 0.0, 0.0);
WebChart('c', "", "G-Haus", 0x008800, wk_drows, 7, wt_sin2, 1, 1440, 0.0, 0.0);
WebChart('c', "", "Garten", 0x888800, wk_drows, 7, wt_sin3, 1, 1440, 0.0, 0.0);
WebChart('c', "", "Dach", 0xFF8800, wk_drows, 7, wt_sin4, 1, 1440, 0.0, 0.0);
WebChart('c', "", "WP", 0x8800FF, wk_drows, 7, wt_wp, 1, 1440, 0.0, 0.0);
WebChart('c', "", "Wärme", 0xCC0000, wk_drows, 7, wt_warm, 1, 1440, 0.0, 0.0);
WebChartTimeBase(0);
// Chart 2: Powerwall & Temperatures (dual y-axis: 0-100% left, 0-60°C right)
// decimals: 0+8=8 (smooth), 1+8=9 (1 decimal + smooth)
WebChart('l', "Powerwall / Temperaturen", "Powerwall|%", 0x00CC00, 1, 168, w_pwl, 8, 60, 0.0, 100.0);
WebChart('l', "", "Aussentemperatur|C", 0xFF6600, 1, 168, w_at, 9, 60, 0.0, 60.0);
WebChart('l', "", "Kollektortemperatur|C", 0x0000FF, 1, 168, w_sk, 9, 60, 0.0, 60.0);
WebChart('l', "", "Solarspeicher|C", 0xCC0000, 1, 168, w_swa, 9, 60, 0.0, 60.0);
// Chart 3: Power consumption & solar yield (smooth)
WebChart('l', "Netzverbrauch / Solarertrag", "Netz|W", 0xFF0000, 1, 168, w_hv, 8, 60, 0.0, 8000.0);
WebChart('l', "", "Solar Input|W", 0x00AA00, 1, 168, w_sin, 8, 60, 0.0, 8000.0);
WebChart('l', "", "Garage|W", 0xFF8800, 1, 168, w_wr1, 8, 60, 0.0, 8000.0);
WebChart('l', "", "GHaus|W", 0x0000FF, 1, 168, w_wr2, 8, 60, 0.0, 8000.0);
WebChart('l', "", "Garten|W", 0x888800, 1, 168, w_wr3, 8, 60, 0.0, 8000.0);
WebChart('l', "", "Dach|W", 0x8800FF, 1, 168, w_wr4, 8, 60, 0.0, 8000.0);
// Chart 4: Gas & Water (2 decimals + smooth = 10), dazu die HWP-Leistung
// auf eigener Achse (anderer Bereich = zweite y-Achse)
WebChart('l', "Gas / Wasser / Wärmepumpen", "Gas|m3", 0xFF0000, 1, 168, w_gas, 10, 60, 0.0, 0.3);
WebChart('l', "", "Wasser|m3", 0x0088FF, 1, 168, w_wa, 10, 60, 0.0, 0.3);
WebChart('l', "", "Wärmepumpen|W", 0xFF8800, 1, 168, w_hwp, 8, 60, 0.0, 3000.0);
// Chart 5: Energy balance table (dynamic weekday labels via JS rotation)
WebChart('t', "Tag|So|Mo|Di|Mi|Do|Fr|Sa", "Haus", 0x000000, wk_drows, 7, wt_hv, 2, 0, 0.0, 0.0);
WebChart('t', "", "Wallbox", 0x000000, wk_drows, 7, wt_wb, 2, 0, 0.0, 0.0);
WebChart('t', "", "Garage", 0x000000, wk_drows, 7, wt_sin1, 2, 0, 0.0, 0.0);
WebChart('t', "", "Gartenhaus", 0x000000, wk_drows, 7, wt_sin2, 2, 0, 0.0, 0.0);
WebChart('t', "", "Garten", 0x000000, wk_drows, 7, wt_sin3, 2, 0, 0.0, 0.0);
WebChart('t', "", "Dach", 0x000000, wk_drows, 7, wt_sin4, 2, 0, 0.0, 0.0);
WebChart('t', "", "Gas", 0x000000, wk_drows, 7, wt_gas, 2, 0, 0.0, 0.0);
WebChart('t', "", "Gas E", 0x000000, wk_drows, 7, wt_pgas, 2, 0, 0.0, 0.0);
WebChart('t', "", "Wasser", 0x000000, wk_drows, 7, wt_wa, 2, 0, 0.0, 0.0);
WebChart('t', "", "WP", 0x000000, wk_drows, 7, wt_wp, 2, 0, 0.0, 0.0);
WebChart('t', "", "Wärme", 0x000000, wk_drows, 7, wt_warm, 2, 0, 0.0, 0.0);
WebChart('t', "", "Wärme E", 0x000000, wk_drows, 7, wt_phwp, 2, 0, 0.0, 0.0);
// Rotate table weekday labels to start from today's weekday
// c.lb = [So,Mo,Di,Mi,Do,Fr,Sa] → rotate by getDay() positions
webSend("<script>(function(){var c=_tcC[_tcC.length-1];if(c&&c.lb){var w=new Date().getDay();var o=c.lb.slice();for(var i=0;i<7;i++)c.lb[i]=o[(w+i)%7];}})()</script>");
} else {
dotask = 1;
// wie 52 Wochen / 31 Tage: neu laden, sonst bleibt die Seite hier
// stehen (oben wurde das Nachladen schon abgeschaltet)
webSend("<meta http-equiv='refresh' content='3'>");
webSend("<p>Daten werden geladen...</p>");
}
}
if (pg == 2) {
// ─── Page 2: 52 Wochen (yearly overview, weekly aggregated) ───
webSend("<script>var tt=document.querySelector(\"div[style*='c_ttl']\");if(tt)tt.style.display='none';rfsh=0;clearTimeout(lt);</script>");
webSend("<h3>52 Wochen</h3>");
if (wFor == 2 && w_heute == heuteNr() && dotask == 0) {
// Title with date range
webSend("<center>");
webSend(lwstr);
webSend("</center><br>");
// Chart 1: Solar inverters (stacked column) — Garage, G-Haus, Garten, Dach
// pos=d52, count=d52-1: skip [0] (accumulated start value), show [1..d52-1]
WebChart('s', "Solar Ertrag kWh", "Garage", 0x4285F4, d52, d52-1, w_wr1, 1, 10080, 0.0, 80.0);
WebChart('s', "", "G-Haus", 0xEA4335, d52, d52-1, w_wr2, 1, 10080, 0.0, 80.0);
WebChart('s', "", "Garten", 0xFBBC04, d52, d52-1, w_wr3, 1, 10080, 0.0, 80.0);
WebChart('s', "", "Dach", 0x34A853, d52, d52-1, w_wr4, 1, 10080, 0.0, 80.0);
// Chart 2: Wallbox, WPumpen, HWP
WebChart('s', "Wallbox / WPumpen kWh", "WBox", 0x4285F4, d52, d52-1, w_pwl, 1, 10080, 0.0, 30.0);
WebChart('s', "", "Wpumpe", 0xEA4335, d52, d52-1, w_at, 1, 10080, 0.0, 30.0);
WebChart('s', "", "HWP", 0xFBBC04, d52, d52-1, w_hwp, 1, 10080, 0.0, 30.0);
// Chart 3: Verbrauch / Einspeisung
WebChart('s', "Verbrauch / Einspeisung kWh", "Verbrauch", 0x4285F4, d52, d52-1, w_sin, 1, 10080, 0.0, 60.0);
WebChart('s', "", "Einsp", 0x34A853, d52, d52-1, w_swa, 1, 10080, 0.0, 60.0);
// Chart 4: Gas, Wasser, HWP (line chart)
WebChart('l', "Gas / Wasser / HWP", "Gas", 0x4285F4, d52, d52-1, w_gas, 8, 10080, 0.0, 40.0);
WebChart('l', "", "Wasser", 0xEA4335, d52, d52-1, w_wa, 8, 10080, 0.0, 40.0);
WebChart('l', "", "HWP", 0xFBBC04, d52, d52-1, w_hwp, 8, 10080, 0.0, 40.0);
// Table: all weekly data (skip [0])
WebChart('t', "Woche", "Garage", 0x000000, d52, d52-1, w_wr1, 2, 0, 0.0, 0.0);
WebChart('t', "", "Gartenhaus", 0x000000, d52, d52-1, w_wr2, 2, 0, 0.0, 0.0);
WebChart('t', "", "Garten", 0x000000, d52, d52-1, w_wr3, 2, 0, 0.0, 0.0);
WebChart('t', "", "Dach", 0x000000, d52, d52-1, w_wr4, 2, 0, 0.0, 0.0);
WebChart('t', "", "Wallbox", 0x000000, d52, d52-1, w_pwl, 2, 0, 0.0, 0.0);
WebChart('t', "", "WPumpen", 0x000000, d52, d52-1, w_at, 2, 0, 0.0, 0.0);
WebChart('t', "", "Verbrauch", 0x000000, d52, d52-1, w_sin, 2, 0, 0.0, 0.0);
WebChart('t', "", "Einspeisung", 0x000000, d52, d52-1, w_swa, 2, 0, 0.0, 0.0);
WebChart('t', "", "Gas", 0x000000, d52, d52-1, w_gas, 2, 0, 0.0, 0.0);
WebChart('t', "", "Gas E", 0x000000, d52, d52-1, w_pgas, 2, 0, 0.0, 0.0);
WebChart('t', "", "Wasser", 0x000000, d52, d52-1, w_wa, 2, 0, 0.0, 0.0);
WebChart('t', "", "D-temp", 0x000000, d52, d52-1, w_avgt, 2, 0, 0.0, 0.0);
WebChart('t', "", "Regen", 0x000000, d52, d52-1, w_rain, 2, 0, 0.0, 0.0);
WebChart('t', "", "HWP", 0x000000, d52, d52-1, w_hwp, 2, 0, 0.0, 0.0);
WebChart('t', "", "BWWP", 0x000000, d52, d52-1, w_bwwp, 2, 0, 0.0, 0.0);
// Replace table first-column labels with week numbers (0,1,2...)
webSend("<script>(function(){var c=_tcC[_tcC.length-1];if(c&&c.lb){for(var i=0;i<c.lb.length;i++)c.lb[i]=i;}})()</script>");
} else {
dotask = 3;
webSend("<meta http-equiv='refresh' content='3'>");
webSend("<p>Daten werden geladen...</p>");
}
}
if (pg == 3) {
// ─── Page 3: Andere Geräte ───
webSend("<style>button{border:0;border-radius:0.3rem;background:#1fa3ec;color:white;line-height:1.4rem;font-size:1.2rem;width:400px;transition-duration:0.4s;cursor:pointer;height:45px;}</style>");
webSend("<div style='text-align:center'>");
webSend("<h3>Andere Geräte</h3>");
// Rolladen (hardcoded static IPs)
webSend("<p><form action='http://192.168.188.22' method='get'><button>Rolladen Wohnzimmer Veranda</button></form></p>");
webSend("<p><form action='http://192.168.188.29' method='get'><button>Rolladen Wohnzimmer vorne</button></form></p>");
webSend("<p><form action='http://192.168.188.35' method='get'><button>Rolladen AZ Heidrun</button></form></p>");
webSend("<p><form action='http://192.168.188.40' method='get'><button>Rolladen Küche</button></form></p>");
webSend("<p><form action='http://192.168.188.41' method='get'><button>Rolladen Flur</button></form></p>");
// Dynamic IPs from UDP
webSend("<p><form action='http://");
webSend(sug_ip);
webSend("' method='get'><button>Schaltuhr Garage</button></form></p>");
webSend("<p><form action='http://192.168.188.33' method='get'><button>Netstream Power</button></form></p>");
// Dynamic IP from UDP
webSend("<p><form action='http://");
webSend(tv2_ip);
webSend("' method='get'><button>Schlafzimmer G</button></form></p>");
webSend("</div>");
}
if (pg == 4) {
// ─── Page 4: Heizung Verlauf letzte 31 Tage ───
webSend("<script>var tt=document.querySelector(\"div[style*='c_ttl']\");if(tt)tt.style.display='none';rfsh=0;clearTimeout(lt);</script>");
webSend("<h3>Heizung Verlauf</h3>");
if (wFor == 3 && w_heute == heuteNr() && dotask == 0) {
// Title with date range
webSend("<center>");
webSend(lwstr);
webSend("</center><br>");
// Wrap chart+table in consistent-width container
webSend("<div style='max-width:960px;margin:0 auto'>");
// Chart: dual-axis line — D-Temp (left) + HWP (right)
// Different min/max triggers dual y-axis in WebChart
WebChart('l', "D-Temp / Heizung WP", "D-TMP", 0x4285F4, d52, d52-1, w_atmp, 8, 1440, -10.0, 35.0);
WebChart('l', "", "HWP", 0xFF0000, d52, d52-1, w_gwp, 8, 1440, 0.0, 50.0);
// Table: 14 columns, skip [0]
WebChart('t', "Tag", "Garage", 0x000000, d52, d52-1, w_wr1, 2, 0, 0.0, 0.0);
WebChart('t', "", "Gartenhaus", 0x000000, d52, d52-1, w_wr2, 2, 0, 0.0, 0.0);
WebChart('t', "", "Garten", 0x000000, d52, d52-1, w_wr3, 2, 0, 0.0, 0.0);
WebChart('t', "", "Dach", 0x000000, d52, d52-1, w_wr4, 2, 0, 0.0, 0.0);
WebChart('t', "", "Wallbox", 0x000000, d52, d52-1, w_pwl, 2, 0, 0.0, 0.0);
WebChart('t', "", "WPumpen", 0x000000, d52, d52-1, w_at, 2, 0, 0.0, 0.0);
WebChart('t', "", "Summe WR", 0x000000, d52, d52-1, w_sk, 2, 0, 0.0, 0.0);
WebChart('t', "", "Verbrauch", 0x000000, d52, d52-1, w_sin, 2, 0, 0.0, 0.0);
WebChart('t', "", "Einspeisung", 0x000000, d52, d52-1, w_swa, 2, 0, 0.0, 0.0);
WebChart('t', "", "Wasser", 0x000000, d52, d52-1, w_wa, 2, 0, 0.0, 0.0);
WebChart('t', "", "Gas", 0x000000, d52, d52-1, w_gas, 2, 0, 0.0, 0.0);
WebChart('t', "", "D-Temp", 0x000000, d52, d52-1, w_atmp, 2, 0, 0.0, 0.0);
WebChart('t', "", "HWP", 0x000000, d52, d52-1, w_gwp, 2, 0, 0.0, 0.0);
WebChart('t', "", "Regen", 0x000000, d52, d52-1, w_rain, 2, 0, 0.0, 0.0);
// Counter-based row labels (0, 1, 2, ...)
webSend("<script>(function(){var c=_tcC[_tcC.length-1];if(c&&c.lb){for(var i=0;i<c.lb.length;i++)c.lb[i]=i;}})()</script>");
webSend("</div>");
} else {
dotask = 4;
webSend("<meta http-equiv='refresh' content='3'>");
webSend("<p>Daten werden geladen...</p>");
}
}
}
// Statuszeile auf der Hauptseite
void WebCall() {
webSend("{s}Energie-Grafiken{m}");
webSend(stZeile);
webSend("{e}");
}
int main() {
dotask = 0;
dy_loaded = 0;
wFor = 0;
wtFor = 0;
d52 = 0;
d_pos = 0;
d_cnt = 0;
db_loaded = 0;
webPageLabel(0, "Tages Verlauf");
webPageLabel(1, "Letzte Woche");
webPageLabel(2, "52 Wochen");
webPageLabel(3, "Andere Geräte");
webPageLabel(4, "Heizung 31 Tage");
return 0;
}