sml_chart_common.tc¶
sml_chart_common.tc — shared chart + descriptor helpers for SML scripts
// ============================================================================
// sml_chart_common.tc — shared chart + descriptor helpers for SML scripts
// ============================================================================
//
// Helper library used by the sml_chart family (sml_chart.tc + later
// sml_chart_pv / sml_chart_bezug / sml_chart_modbus / sml_water /
// power_meter ports). Provides:
//
// * Persist state for descriptor management, baselines and the rolling
// chart arrays
// * `sml_descriptor_apply()` — change-detect for rx/tx/filter/activ,
// re-runs Sensor53 r + smlApplyPins on any flip (same logic as in
// sml_simple.tc)
// * Two-stage chart-array pump: filter ring at 5 s cadence → averaged
// value pushed to the visible chart at 30 s / 60 s cadence (matches
// ottelo's `s4hf/s24hf` moving-average idiom)
// * `sml_chart_60s_tick()` — daily/monthly/yearly counter updates +
// midnight rollover with month-wrap zero-fill
// * `sml_chart_render_*()` — WebChart-driven 4 h / 24 h power line
// charts + daily / monthly column charts
// * `sml_chart_save() / load()` — binary persistence of the four
// chart arrays to `/sml_chart.bin`
// * `sml_render_settings_page()` — same Stromzähler / Pins / Filter
// panel as sml_simple, kept central so all sml_chart_* variants
// share one source of truth
//
// **No callbacks / no main() in here** — the including script wires the
// helpers into its own EverySecond / WebUI / WebCall callbacks.
//
// Storage footprint (all in PSRAM on ESP32-S3-PSRAM via special_calloc):
// s4h: 481 ints ≈ 1.9 KB (4 h @ 30 s/sample)
// s24h: 1441 ints ≈ 5.6 KB (24 h @ 60 s/sample)
// dcon: 32 floats ≈ 128 B
// mcon: 13 floats ≈ 52 B
// filter rings, cursors, scratch ≈ 200 B
// ---------------------------------------------------------
// Total chart state ≈ 8 KB
// ============================================================================
// Descriptor management (persist watch vars + sml_descriptor_apply +
// sml_render_settings_panel) lives in sml_descriptor.tc — one source of
// truth shared with sml_simple.tc and any future SML-flavoured script.
#include "sml_descriptor.tc"
// ── Energy baselines (delta against current SML kWh counter) ────────────────
persist float sml_dval; // daily zero point
persist float sml_mval; // monthly zero point
persist float sml_yval; // yearly zero point
persist int sml_da; // last seen day-of-month (month-wrap detector)
// ── Rolling-power chart arrays — head index in a separate cursor ────────────
// MUST be float arrays — WebChart memcpy's each int32 slot into a float
// (vm.h SYS_WEB_CHART: `memcpy(&fval, &arr[idx], sizeof(float))`). Int
// arrays render as garbage values (bit-cast).
persist float sml_s4h[481]; // 4 h chart, 30 s per slot
persist float sml_s24h[1441]; // 24 h chart, 60 s per slot
persist int sml_s4h_pos;
persist int sml_s24h_pos;
// ── Daily / monthly columnar totals — 0-based indexing matching WebChart's
// ring-buffer walk (pos-count+i, mod count). Day 1 = index 0, Month 1 = 0.
persist float sml_dcon[31];
persist float sml_mcon[12];
// ── Runtime state — moving filter rings + tick counters ─────────────────────
int sml_s4hf[6]; // 6×5s = 30s average window for 4 h chart
int sml_s24hf[12]; // 12×5s = 60s average window for 24 h chart
int sml_f4_pos;
int sml_f24_pos;
int sml_t1; // 5s downcounter
int sml_t2; // 60s downcounter
int sml_hr_last; // last seen tasm_hour (midnight edge detect)
// sml_buf is declared in sml_descriptor.tc and shared by both modules
// ============================================================================
// Save / load chart arrays
// ----------------------------------------------------------------------------
// By default the arrays are stored as compact BINARY (/sml_chart*.bin).
// Define CHART_CSV (uncomment below, or `#define CHART_CSV` before the
// #include in your main script) to instead store human-readable TAB-separated
// CSV (/sml_chart*.csv) you can open / read / edit in a spreadsheet and
// restore. CHART_CSV_DEC sets the decimal places (trailing zeros stripped).
// Note: the two formats use different filenames, so toggling starts a fresh
// chart file rather than misreading the other format.
// ============================================================================
// #define CHART_CSV
#ifndef CHART_CSV_DEC
#define CHART_CSV_DEC 3
#endif
// Set whenever the meter modifies the chart arrays (5s/60s ticks). Gates the
// OnExit/CleanUp flush so a freshly-imported .bin is never clobbered — see the
// comment on CleanUp()/OnExit() below. NOT persisted (it is run-state only).
int sml_chart_dirty = 0;
void sml_chart_save() {
#ifdef CHART_CSV
int h = fileOpen("/sml_chart.csv", "w");
#else
int h = fileOpen("/sml_chart.bin", "w");
#endif
if (h < 0) {
addLog("sml_chart_save: fileOpen failed");
return;
}
#ifdef CHART_CSV
fileWriteArray(sml_s4h, h, 481, 0, CHART_CSV_DEC);
fileWriteArray(sml_s24h, h, 1441, 0, CHART_CSV_DEC);
fileWriteArray(sml_dcon, h, 31, 0, CHART_CSV_DEC);
fileWriteArray(sml_mcon, h, 12, 0, CHART_CSV_DEC);
#else
fileWriteBin(h, sml_s4h, 481);
fileWriteBin(h, sml_s24h, 1441);
fileWriteBin(h, sml_dcon, 31);
fileWriteBin(h, sml_mcon, 12);
#endif
fileClose(h);
sml_chart_dirty = 0; // file now matches the in-RAM arrays
}
void sml_chart_load() {
sml_chart_dirty = 0; // about to (re)sync arrays from the file
#ifdef CHART_CSV
int h = fileOpen("/sml_chart.csv", "r");
#else
int h = fileOpen("/sml_chart.bin", "r");
#endif
if (h < 0) return; // no file yet — arrays stay at persist defaults
#ifdef CHART_CSV
fileReadArray(sml_s4h, h, 481);
fileReadArray(sml_s24h, h, 1441);
fileReadArray(sml_dcon, h, 31);
fileReadArray(sml_mcon, h, 12);
#else
fileReadBin(h, sml_s4h, 481);
fileReadBin(h, sml_s24h, 1441);
fileReadBin(h, sml_dcon, 31);
fileReadBin(h, sml_mcon, 12);
#endif
fileClose(h);
}
// Flush chart data to file when the script is going away, so the post-boot
// sml_chart_load() never reads a stale .bin and shows a gap:
// CleanUp() — device Restart / OTA (FUNC_SAVE_BEFORE_RESTART). The .tas `>R`.
// OnExit() — this slot stopped / re-run / unlinked (per-slot teardown).
// GUARDED by sml_chart_dirty: only flush if the meter actually changed the
// arrays since the last load/save. This is essential for the "import new data,
// then restart" workflow — without the guard, the outgoing slot's teardown
// save overwrites a freshly-uploaded /sml_chart.bin *before* the new instance
// reads it (the clobber that made imports silently revert). With it, a
// stop → upload → run cycle preserves the import: the stop-save flushes and
// clears dirty, the upload overwrites the file, and the run-teardown sees
// dirty == 0 and skips. => Always upload the .bin while the slot is STOPPED.
void CleanUp() {
if (sml_chart_dirty) sml_chart_save();
}
void OnExit() {
if (sml_chart_dirty) sml_chart_save();
}
// ============================================================================
// Init helpers — reset baselines from current meter, reset chart arrays
// ============================================================================
void sml_chart_init_baselines() {
float ein = smlGet(2);
sml_dval = ein;
sml_mval = ein;
sml_yval = ein;
sml_s4h_pos = 0;
sml_s24h_pos = 0;
int i = 0;
while (i < 481) { sml_s4h[i] = 0.0; i = i + 1; }
i = 0;
while (i < 1441) { sml_s24h[i] = 0.0; i = i + 1; }
sml_chart_save();
addLog("sml_chart: baselines set from meter, power charts cleared");
}
void sml_chart_init_columns() {
int i = 0;
while (i < 31) { sml_dcon[i] = 0.0; i = i + 1; }
i = 0;
while (i < 12) { sml_mcon[i] = 0.0; i = i + 1; }
sml_chart_save();
addLog("sml_chart: daily + monthly column charts cleared");
}
// ============================================================================
// 5-s tick — feed the moving-average filters and push to the rolling
// chart arrays. Mirrors ottelo's >S "alle 5s" block exactly.
// ============================================================================
void sml_chart_5s_tick() {
int p = (int)smlGet(1); // current power in W (int filter ring)
sml_s4hf[sml_f4_pos] = p;
sml_f4_pos = (sml_f4_pos + 1) % 6;
sml_s24hf[sml_f24_pos] = p;
sml_f24_pos = (sml_f24_pos + 1) % 12;
int sum = 0;
int i = 0;
while (i < 6) { sum = sum + sml_s4hf[i]; i = i + 1; }
float s4h_avg = (float)sum / 6.0;
sum = 0;
i = 0;
while (i < 12) { sum = sum + sml_s24hf[i]; i = i + 1; }
float s24h_avg = (float)sum / 12.0;
// 240-slot ring at 1 min/slot = the last 4 h, sliding. Two fixes vs the old
// ((hour-4)%24)*120 mapping: (1) that only produced idx4 < 480 for hours
// 4..7, so outside 04:00-08:00 nothing was stored and the chart was empty;
// (2) 1-min slots match the WebChart x-axis interval=1, so 240 points span a
// real 4 h axis — the previous 480 30-s points were drawn over an 8 h axis.
int idx4 = (tasm_hour * 60 + tasm_minute) % 240;
int idx24 = tasm_hour * 60 + tasm_minute;
if (idx4 >= 0 && idx4 < 240) {
sml_s4h[idx4] = s4h_avg;
sml_s4h_pos = idx4;
}
if (idx24 >= 0 && idx24 < 1440) {
sml_s24h[idx24] = s24h_avg;
sml_s24h_pos = idx24;
}
sml_chart_dirty = 1; // meter modified the high-res arrays
}
// ============================================================================
// 60-s tick — daily / monthly counters + midnight rollover.
// Mirrors ottelo's >S "alle 60s" block (lines 244-278).
// ============================================================================
void sml_chart_60s_tick() {
float ein = smlGet(2);
int d = tasm_day; // 1..31
int m = tasm_month; // 1..12
if (d >= 1 && d <= 31) sml_dcon[d - 1] = ein - sml_dval;
if (m >= 1 && m <= 12) sml_mcon[m - 1] = ein - sml_mval;
sml_chart_dirty = 1; // meter modified the daily/monthly counters
int hr = tasm_hour;
if (sml_hr_last != hr && hr == 0) {
if (d > 1) {
sml_da = d;
} else {
// Month-wrap — zero out days past the previous month's length.
// sml_da still holds last-seen 1..31; zero indices [sml_da..30].
int i = sml_da;
while (i < 31) { sml_dcon[i] = 0.0; i = i + 1; }
sml_mval = ein;
sml_da = 1;
}
if (d == 1 && m == 1) sml_yval = ein;
sml_dval = ein;
sml_chart_save();
}
sml_hr_last = hr;
}
// ============================================================================
// Render helpers — chart + table blocks for the WebUI main page
// ============================================================================
void sml_chart_render_totals() {
// Datum + Uptime (Paritaet zum .tas-Original; auf der Hauptseite gewuenscht)
sprintf(sml_buf, "{s}Datum{m}%02d.%02d.%04d %02d:%02d{e}", tasm_day, tasm_month, tasm_year, tasm_hour, tasm_minute); webSend(sml_buf);
int up_d = tasm_uptime / 86400;
int up_h = (tasm_uptime / 3600) % 24;
int up_m = (tasm_uptime / 60) % 60;
sprintf(sml_buf, "{s}Uptime{m}%d d %d h %d min{e}", up_d, up_h, up_m); webSend(sml_buf);
float ein = smlGet(2);
sprintf(sml_buf, "{s}Tagesverbrauch{m}%.2f kWh{e}", ein - sml_dval); webSend(sml_buf);
sprintf(sml_buf, "{s}Monatsverbrauch{m}%.2f kWh{e}", ein - sml_mval); webSend(sml_buf);
sprintf(sml_buf, "{s}Jahresverbrauch{m}%.2f kWh{e}", ein - sml_yval); webSend(sml_buf);
}
// Script JS hook applied to both rolling charts: filled area (better
// readability) + DE date in the tooltip (dd.MM.yyyy), independent of the
// browser locale. WebChartJS runs in the chart's draw scope (dt/o/el) and
// o.done=1 takes over the draw so it renders as an AreaChart.
char sml_chart_js[224]; // must hold the full snippet incl. trailing o.done=1
void sml_chart_area_de() {
strcpy(sml_chart_js,
"var f=new google.visualization.DateFormat({pattern:'dd.MM.yyyy HH:mm'});f.format(dt,0);o.areaOpacity=0.3;new google.visualization.AreaChart(el).draw(dt,o);o.done=1");
WebChartJS(sml_chart_js);
}
void sml_chart_render_4h() {
WebChartSize(600, 280);
// 240 points at interval=1 min = a 4 h x-axis (matches the 240-slot 1-min
// ring). Filled area + DE date via the hook.
WebChart(0, "Verbrauch 4 Stunden [Watt]", "W", 0x3498db, sml_s4h_pos, 240, sml_s4h, 0, 1, 0.0, 0.0);
sml_chart_area_de();
}
void sml_chart_render_24h() {
WebChartSize(600, 280);
WebChart(0, "Verbrauch 24 Stunden [Watt]", "W", 0x3498db, sml_s24h_pos, 1440, sml_s24h, 0, 1, 0.0, 0.0);
sml_chart_area_de();
}
// ── Direct Google Charts emit — per-bar tri-color (past=green / today=red /
// future=blue) and day-of-month x-axis. WebChart's built-in axis
// formatter shows DD.MM. for >7 column points and doesn't support
// per-bar styles, so we bypass it for the column charts. Loader is
// already on the page from the WebChart line-chart calls above.
void sml_chart_render_days() {
int today = tasm_day;
if (today < 1) today = 1;
webSend("<div id='sml_dch' style='text-align:center;width:600px;height:280px'></div>");
webSend("<script>function _smlDD(){var d=google.visualization.arrayToDataTable([['Tag','Energie [kWh]',{role:'style'}]");
int i = 1;
while (i <= 31) {
if (i < today) sprintf(sml_buf, ",[%d,%.2f,'green']", i, sml_dcon[i - 1]);
else if (i == today) sprintf(sml_buf, ",[%d,%.2f,'red']", i, sml_dcon[i - 1]);
else sprintf(sml_buf, ",[%d,%.2f,'blue']", i, sml_dcon[i - 1]);
webSend(sml_buf);
i = i + 1;
}
webSend("]);new google.visualization.ColumnChart(document.getElementById('sml_dch')).draw(d,{chartArea:{left:50,right:20,height:'75%',backgroundColor:'#f0f2f5'},legend:'none',title:'Tagesverbraeuche Monatsansicht',vAxis:{format:'# kWh'},hAxis:{title:'Tag',ticks:[1,5,10,15,20,25,30]}});}google.charts.setOnLoadCallback(_smlDD);</script>");
}
void sml_chart_render_months() {
int thismon = tasm_month;
if (thismon < 1) thismon = 1;
// strToken needs the source as a resolvable char[] variable — string
// literals fall through tc_resolve_ref and return empty (the earlier
// version produced ['',12.3,...] with no month names on the x-axis).
char mn_names[64];
strcpy(mn_names, "Jan|Feb|Maer|Apr|Mai|Jun|Jul|Aug|Sep|Okt|Nov|Dez");
char mn[6];
webSend("<div id='sml_mch' style='text-align:center;width:600px;height:280px'></div>");
webSend("<script>function _smlDM(){var d=google.visualization.arrayToDataTable([['Monat','Verbrauch [kWh]',{role:'style'}]");
int i = 1;
while (i <= 12) {
strToken(mn, mn_names, '|', i);
if (i < thismon) sprintf(sml_buf, ",['%s',%.2f,'green']", mn, sml_mcon[i - 1]);
else if (i == thismon) sprintf(sml_buf, ",['%s',%.2f,'red']", mn, sml_mcon[i - 1]);
else sprintf(sml_buf, ",['%s',%.2f,'blue']", mn, sml_mcon[i - 1]);
webSend(sml_buf);
i = i + 1;
}
webSend("]);new google.visualization.ColumnChart(document.getElementById('sml_mch')).draw(d,{chartArea:{left:50,right:30,top:30,height:'70%',backgroundColor:'#f0f2f5'},legend:'none',title:'Monatsverbraeuche Jahresansicht',vAxis:{format:'# kWh'},hAxis:{slantedText:false,showTextEvery:1}});}google.charts.setOnLoadCallback(_smlDM);</script>");
}
// Settings panel renderer is in sml_descriptor.tc as
// `sml_render_settings_panel()` — including modules call that directly.