Zum Inhalt

sml_chart.tc

sml_chart.tc — SML meter with charts and tables, no emulator.

Source on GitHub

// @name: SML Chart
// @info: https://ottelo9.github.io/tasmota-sml-script/sml/
// sml_chart.tc — SML meter with charts and tables, no emulator.
// PV (feed-in) is a switch, not a separate build: the extra rings cost about
// 180 bytes, so they are always compiled in and only the display is gated.
// With a battery emulator use sml_chart_ct002.tc or sml_chart_eco_shelly.tc.
// Docs: https://ottelo9.github.io/tasmota-sml-script/sml/
// Needs ESP32, TinyC in slot 0 (for /pwr), USE_SML.

#include "sml_chart_common.tc"
#include "sml_modbus_common.tc"

char sml_vers[] = "28.09.2026";

// ─── Settings ──────────────────────────────────────────────────────────────

int sml_e_pv; int sml_e_sndpwr;   // edit buffer, applied by Save

// ─── Transient WebButton flags ─────────────────────────────────────────────
int do_base;      // reset consumption baselines
int do_power;     // clear the 4h/24h charts
int do_bars;      // clear the day/month bars
int do_reset;
int do_csv;      // write the chart arrays out as readable .csv files

// ─── Callbacks ─────────────────────────────────────────────────────────────

void Every100ms() {
    mbPoll();
}

void EverySecond() {
    uiApplyBegin();
    if (tc_opt_first == 1) {
        sml_pv     = tcOptGet(9,  sml_pv);
        sml_sndpwr = tcOptGet(10, sml_sndpwr);
        tcOptSet(9,  sml_pv);
        tcOptSet(10, sml_sndpwr);
        // Baselines from the shared file, so the day/month/year totals survive
        // a program change. sml_da comes along: it is the last-seen day, and
        // without it the month-wrap would zero the wrong span of days.
        sml_dval  = tcOptGetF(0, sml_dval);
        sml_mval  = tcOptGetF(1, sml_mval);
        sml_yval  = tcOptGetF(2, sml_yval);
        sml_dval2 = tcOptGetF(3, sml_dval2);
        sml_mval2 = tcOptGetF(4, sml_mval2);
        sml_yval2 = tcOptGetF(5, sml_yval2);
        sml_da    = tcOptGet(25, sml_da);
        sml_stmin  = tcOptGet(30, sml_stmin);
        sml_chmain = tcOptGet(31, sml_chmain);
        if (sml_stmin == 0) sml_stmin = 240;
        tcOptSet(30, sml_stmin);
        tcOptSet(31, sml_chmain);
        sml_chart_opt_load();
    }
    // Pushed every second rather than at the rollover: the baselines are
    // written in several places (midnight, month wrap, new year, the manual
    // reset button, the .bin import) and a missed one would silently keep an
    // old figure in the file. tcOptSetF only marks the file dirty on a real
    // change, so this costs six comparisons.
    tcOptSetF(0, sml_dval);
    tcOptSetF(1, sml_mval);
    tcOptSetF(2, sml_yval);
    tcOptSetF(3, sml_dval2);
    tcOptSetF(4, sml_mval2);
    tcOptSetF(5, sml_yval2);
    tcOptSet(25, sml_da);
    sml_chart_week_opt();
    if (ui_reloading) { sml_e_pv = sml_pv; sml_e_sndpwr = sml_sndpwr; sml_chart_opt_load(); }
    if (ui_applying)  { sml_pv = sml_e_pv; sml_sndpwr = sml_e_sndpwr;
                        tcOptSet(9, sml_pv); tcOptSet(10, sml_sndpwr);
                        sml_chart_opt_apply(); }
    sml_descriptor_apply();
    mbTick();

    if (do_reset) {
        tasmCmd("Sensor53 r", sml_pv_resp);
        do_reset = 0;
    }

    // CSV export deliberately BEFORE the NTP/meter returns below: getting the
    // stored data out has to work on a device whose meter is silent, which is
    // exactly when someone wants it (mi-hol, #119).
    if (do_csv) {
        sml_chart_export_csv();
        if (sml_pv > 0) sml_chart_pv_export_csv();
        do_csv = 0;
    }

    if (tasm_year < 2020) return;
    if (smlGet(2) == 0.0) return;

    if (do_base) {
        sml_chart_init_baselines();
        sml_chart_pv_init_baselines();
        do_base = 0;
    }
    if (do_power) {
        sml_chart_clear_power();
        do_power = 0;
    }
    if (do_bars) {
        sml_chart_init_columns();
        sml_chart_pv_init_columns();
        do_bars = 0;
    }
    sml_t1 = sml_t1 - 1;
    if (sml_t1 <= 0) {
        sml_t1 = 5;
        sml_chart_5s_tick();
        sml_chart_pv_5s_tick();   // smooths power2 and publishes it
    }
    sml_t2 = sml_t2 - 1;
    if (sml_t2 <= 0) {
        sml_t2 = 60;
        sml_chart_60s_tick();
        // The PV tick keeps running with the display off, so switching PV on
        // later shows continuous history instead of a gap.
        sml_chart_pv_60s_tick();
    }
}

// /pwr — JSON for a second battery polling over HTTP (opendtu-onbattery DPL)
void WebOn() {
    int h = webHandler();
    if (h != 7) return;               // /pwr
    if (smlVars()) return;           // /pwr?v=... single values
    smlPwrJson(sml_power2);
    strcat(sml_buf, "}");
    smlSendJson(sml_buf);
}

// Tasmota's own root page, rendered once per load (FUNC_WEB_ADD_MAIN_BUTTON).
// The charts go here rather than into WebCall(), which is re-rendered on every
// refresh - five Google charts on that path would be re-sent every two seconds.
void WebPage() {
    if (sml_chmain > 0) sml_chart_render_all();
}

void WebCall() {
    if (!sml_activ) {
        uiText("{s}SML{m}off (Rule1){e}", "{s}SML{m}aus (Rule1){e}");
        return;
    }
    sml_chart_render_meter();
    sml_phase_rows();
    sml_chart_render_totals();
    sml_chart_pv_render_totals();
    mbRows();
    uiSep();
    if (sml_chmain == 0) sml_chart_link();   // no link when they are on the page itself
    sml_chart_datetime_rows();
}

void WebUI() {
    if (webPage() == 2) { sml_chart_page_render(); return; }
    if (webPage() == 5) { webSend("<script>location.replace('/tc')</script>"); return; }
    if (webPage() != 0) return;
    sml_render_settings_panel();

    uiHead("&#x2600;&#xFE0F;", "PV", "PV", "sml/#pv");
    if (ui_de) webCheckbox(sml_e_pv, "Einspeisung");
    else       webCheckbox(sml_e_pv, "Feed-in");

    uiHead("&#x1F527;", "Telemetry", "Telemetrie", "sml/#telemetry");
    if (ui_de) webCheckbox(sml_e_sndpwr, "power2 senden (MQTT)");
    else       webCheckbox(sml_e_sndpwr, "Send power2 (MQTT)");
    sprintf(sml_buf, "<div class='n'>power2: <b>%.0f W</b></div>", sml_power2);
    webSend(sml_buf);
    webSend("<b>HTTP API</b>");
    webSend("<div class='n'>HTTP: <a href='/pwr' target='_blank'>/pwr</a> &nbsp; <a href='/pwr?v=all' target='_blank'>/pwr?v=all</a> &nbsp; <a href='/pwr?v=sml[1]' target='_blank'>/pwr?v=sml[1]</a></div>");
    mbPanel();

    sml_chart_options_panel();
    sml_chart_data_panel(varIdx(do_base), varIdx(do_power), varIdx(do_bars));
    webSend("<hr>");
    uiSaveButton();
    if (ui_de) webButton(do_reset, "SML Neustart|OK");
    else       webButton(do_reset, "Restart SML|OK");
    sml_chart_csv_button(varIdx(do_csv));

    uiUptime();
    sprintf(sml_buf, "sml_chart.tc &middot; Version %s<br>", sml_vers);
    webSend(sml_buf);
    webSend("by <a href='https://ottelo.jimdofree.com' target='_blank'>ottelo.jimdofree.com</a> &amp; gemu2015 (TinyC <span id='tcv'>...</span>)</div></div>");
    webSend("<script>if(window.__tcv){document.getElementById('tcv').textContent=window.__tcv;}else{fetch('/tc_api?cmd=status').then(function(r){return r.json();}).then(function(j){window.__tcv=j.release;var e=document.getElementById('tcv');if(e)e.textContent=j.release;}).catch(function(){});}</script>");
}

int main() {
    if (sml_rx_pin == 0 && sml_tx_pin == 0) {
        sml_rx_pin = -1;
        sml_tx_pin = -1;
    }
    if (sml_da == 0) sml_da = 1;
    if (sml_activ != tasm_rule) tasm_rule = sml_activ;

    sml_e_pv = sml_pv;
    sml_e_sndpwr = sml_sndpwr;
    sml_chart_load();
    if (sml_stmin == 0) sml_stmin = 240;   // never set -> 4 h, as before
    sml_chart_opt_load();
    sml_chart_pv_load();

    mbInit();
    webPageLabel(0, "&#x2699;&#xFE0F; SML");
    webPageLabel(1, "");
    webPageLabel(2, "");                // charts page, reached by link
    webPageLabel(5, "&#x1F4BB; TinyC");   // hands over to /tc - kept last so it stays
                                          // last when a program adds a page
    webOn(7, "/pwr");
    smlScripterLoad("/sml_meter.def");

    addLog("sml_chart: %s started rx=%d meter=%d pv=%d modbus=%d",
           sml_vers, sml_rx_pin, sml_meter_sel, sml_pv, mb_on);
    return 0;
}