Zum Inhalt

sml_chart.tc

sml_chart.tc — single-meter SML reader with 4h/24h power charts +

Source on GitHub

// ============================================================================
// sml_chart.tc — single-meter SML reader with 4h/24h power charts +
//                  daily / monthly column charts
//
// TinyC port of ottelo's 1_SML_Chart.tas. Same scaffold as sml_simple.tc
// (descriptor + pin management) plus four WebChart blocks rendering the
// rolling-power and daily/monthly-consumption history.
//
// All the heavy lifting (chart-array pump, midnight rollover, save/load,
// settings-panel rendering) is in `sml_chart_common.tc`. This file just
// wires the helpers into Tasmota's callbacks.
// ============================================================================

#include "sml_chart_common.tc"

// Uncomment to pre-fill all four chart arrays with synthetic data at boot —
// useful for visually validating chart rendering without waiting hours for
// real SML readings to accumulate. Comment out for production.
//#define SML_CHART_DEMO

// ── Transient WebButton flags (cleared after handler fires) ─────────────────
int do_init;     // re-baseline counters from current meter reading
int do_init2;    // reset the daily + monthly column charts
int do_save;     // manual save of chart arrays
int do_reset;    // Sensor53 r without changing any settings

#ifdef SML_CHART_DEMO
// ── Synthetic chart data — purely for rendering validation ────────────────
// 4h power: bell-shaped pulse centred halfway through, peaks ~5000 W.
// 24h power: solar-style daily curve (zero at night, peak at midday).
// Daily totals: cosine wave 5-30 kWh.
// Monthly totals: seasonal swing 200-450 kWh (heating winter).
void sml_chart_demo_fill() {
    int i = 0;
    while (i < 480) {
        float x = ((float)i - 240.0) / 60.0;       // -4..+4 over 480 samples
        float bell = cos(x * 0.5);                  // soft hump 0..1
        if (bell < 0.0) bell = 0.0;
        sml_s4h[i] = bell * bell * 5000.0 + 200.0;
        i = i + 1;
    }
    sml_s4h_pos = 240;

    i = 0;
    while (i < 1440) {
        float t = (float)i;
        float solar = 0.0;
        if (t > 360.0 && t < 1080.0) {
            float phase = (t - 360.0) / 720.0;
            solar = sin(phase * 3.14159);
            solar = solar * solar * 4500.0;
        }
        sml_s24h[i] = solar + 150.0;
        i = i + 1;
    }
    sml_s24h_pos = 720;

    i = 0;
    while (i < 31) {
        float ph = (float)i / 31.0 * 6.28318;
        sml_dcon[i] = 15.0 + 10.0 * cos(ph);       // 5..25 kWh wobble
        i = i + 1;
    }

    i = 0;
    while (i < 12) {
        float ph = (float)i / 12.0 * 6.28318;
        sml_mcon[i] = 325.0 + 125.0 * cos(ph);     // 200..450 kWh seasonal
        i = i + 1;
    }
    addLog("sml_chart: demo data loaded into all four charts");
}
#endif

// ============================================================================
// Callbacks
// ============================================================================

void EverySecond() {
    // Always: descriptor / pin / filter / activ change-detect
    sml_descriptor_apply();

    // Manual reset button — one-shot
    if (do_reset) {
        tasmCmd("Sensor53 r", sml_buf);
        do_reset = 0;
    }

    // Below: only run once NTP is up and the meter is actually reporting
    if (tasm_year < 2020) return;
    if (smlGet(2) == 0.0) return;

    // Reset buttons (act once the meter is live so baselines reflect reality)
    if (do_init)  { sml_chart_init_baselines(); do_init  = 0; }
    if (do_init2) { sml_chart_init_columns();   do_init2 = 0; }
    if (do_save)  { sml_chart_save();           do_save  = 0; }

    // 5 s / 60 s tick scheduler — first time `_t1`/`_t2` are 0 (zero-init),
    // tick runs and resets the counter.
    sml_t1 = sml_t1 - 1;
    if (sml_t1 <= 0) {
        sml_t1 = 5;
        sml_chart_5s_tick();
    }
    sml_t2 = sml_t2 - 1;
    if (sml_t2 <= 0) {
        sml_t2 = 60;
        sml_chart_60s_tick();
    }
}

// ── Tasmota main page, sensor-table row block (Scripter >W text part) ──
void WebCall() {
    if (!sml_activ) {
        webSend("{s}SML{m}disabled (Rule1 off){e}");
        return;
    }
    sml_chart_render_totals();
}

// ── Tasmota main page, chart block below the sensor table ──────────────
// This is where ottelo's >W $gc(...) chart sections land in TinyC.
void WebPage() {
    // ottelo's chart-centering compensation: the Tasmota main page shifts
    // chart blocks ~30 px right (sensor-table left structure). ottelo's
    // Scripter scripts wrap the whole chart section in
    // `<div style="margin-left:-30px">` (1_SML_Chart.tas line 301/345) to
    // pull it back to centered. Same fix, same value, here.
    webSend("<div style='margin-left:-30px'>");
    sml_chart_render_4h();
    sml_chart_render_24h();
    sml_chart_render_days();
    sml_chart_render_months();
    webSend("</div>");
}

// ── Dedicated settings sub-page (Scripter >w "Stromzähler / Daten") ────
void WebUI() {
    sml_render_settings_panel();
    webSend("<hr><b>&#x1F4BE; Daten</b>");
    // Destruktive Aktionen mit Sicherheitsabfrage (confirm) + Klartext-Label.
    webSend("<div style='font-size:11px;color:#a00;margin:4px 0'>&#9888; Die folgenden zwei Aktionen ueberschreiben Verlaufsdaten.</div>");
    webSend("<div><button style='width:100%' data-a='erledigt' onclick='if(confirm(\"Setzt Tages-/Monats-/Jahres-Nullpunkt NEU vom aktuellen Zaehlerstand und loescht die 4h/24h-Charts. Fortfahren?\"))tcbtn(this,1,");
    sprintf(sml_buf, "%d)'>Zaehler-Nullpunkte setzen (Tag/Monat/Jahr) + Power-Charts leeren</button></div>", varIdx(do_init));
    webSend(sml_buf);
    webSend("<div><button style='width:100%' data-a='erledigt' onclick='if(confirm(\"Loescht ALLE Tages- und Monats-Balken (Verbrauchsverlauf). Fortfahren?\"))tcbtn(this,1,");
    sprintf(sml_buf, "%d)'>Balkendiagramme (Tage/Monate) zuruecksetzen</button></div>", varIdx(do_init2));
    webSend(sml_buf);
    webButton(do_save,  "Diagrammdaten jetzt speichern|gespeichert");
    webSend("<hr><b>&#x1F504; SML neu initialisieren</b>");
    webButton(do_reset, "SML-Treiber neu laden (Sensor53 r)|SML neu geladen");
    webSend("<div style='font-size:11px;color:#777;margin:4px 0'>Laedt nur den SML-Treiber neu (Pins/Descriptor) - aendert KEINE Zaehler- oder Chart-Daten.</div>");
    webSend("<div style='text-align:center;font-size:10px;color:#777;margin-top:10px'><b>sml_chart.tc</b><br>TinyC port of ottelo's 1_SML_Chart.tas</div></div>");
}

int main() {
    // First-run pin defaults: -1 = "leave the descriptor placeholder
    // alone until user picks". Same trick as sml_simple.tc.
    if (sml_rx_pin == 0 && sml_tx_pin == 0) {
        sml_rx_pin = -1;
        sml_tx_pin = -1;
    }
    if (sml_da == 0) sml_da = 1;

    // tasm_rule must match the persisted checkbox before SML_Init runs.
    if (sml_activ != tasm_rule) {
        tasm_rule = sml_activ;
    }

    sml_chart_load();

#ifdef SML_CHART_DEMO
    sml_chart_demo_fill();
#endif

    // Single button on Tasmota's menu → opens the settings sub-page (WebUI).
    // Label kept short — the panel header inside already shows "SML Zaehler",
    // so prefixing "SML" here just stacks redundantly with it.
    webPageLabel(0, "Einstellungen / Daten");
    // Clear any stale label left in slot 1 from a previous version that
    // registered two pages — Tinyc->page_label[] persists across slot
    // restarts, an empty string suppresses the button at render time.
    webPageLabel(1, "");

    smlScripterLoad("/sml_meter.def");

    addLog("sml_chart: started rx=%d tx=%d meter=%d activ=%d",
           sml_rx_pin, sml_tx_pin, sml_meter_sel, sml_activ);
    return 0;
}