Zum Inhalt

sml_chart_bezug.tc

sml_chart_bezug.tc — SML reader for meters with no feed-in counter

Source on GitHub

// ============================================================================
// sml_chart_bezug.tc — SML reader for meters with no feed-in counter
// ============================================================================
//
// TinyC port of ottelo's 3_SML_Chart_PV_Bezugszaehler.tas.
//
// Same chart layout as sml_chart_pv.tc, but the production side comes
// from a **virtual** feed-in counter integrated from negative power
// readings — for installations where the Bezugszähler (grid-import
// meter) reports current power that can go below zero on export, but
// does NOT carry a separate energy-out kWh counter.
//
// Integration scheme (matches ottelo's >S logic):
//
//   * 5 s tick:  if smlGet(1) < 0 → accumulate −power into a Watt-sum
//                (sml_ptogrid_sum)
//   * 60 s tick: convert the per-minute Watt-sum into kWh and add to
//                the persistent virtual counter:
//                  sml_bezug_total += sml_ptogrid_sum / 720000
//                (12 samples × 60 s × 1000 W → kWh)
//
// All chart rendering / rollover / settings panel infrastructure is
// inherited from sml_chart_common.tc.
// ============================================================================

#include "sml_chart_common.tc"

// Uncomment to pre-fill charts with synthetic data for visual validation.
//#define SML_CHART_DEMO

// ── Virtual feed-in counter + accumulator ──────────────────────────────────
persist float sml_bezug_total;       // integrated kWh fed back to the grid
float sml_ptogrid_sum;               // 5 s-sample running Watt-sum

// ── Production-side persist state (same as sml_chart_pv, baselines now
//    track sml_bezug_total instead of smlGet(3)) ────────────────────────────
persist float sml_dval2;
persist float sml_mval2;
persist float sml_yval2;
persist float sml_dprod[31];
persist float sml_mprod[12];

persist watch int sml_sndpwr;

// ── Runtime state ──────────────────────────────────────────────────────────
float sml_power2;
int   sml_pv_hr_last;
char  sml_pv_resp[64];

// ── Transient WebButton flags ──────────────────────────────────────────────
int do_init;
int do_init2;
int do_save;
int do_reset;

#ifdef SML_CHART_DEMO
void sml_chart_pv_demo_fill() {
    int i = 0;
    while (i < 480) {
        float x = ((float)i - 240.0) / 60.0;
        float bell = cos(x * 0.5);
        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);
        sml_dprod[i] =  9.0 +  7.0 * cos(ph + 1.57);
        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);
        sml_mprod[i] = 180.0 + 100.0 * cos(ph + 3.14);
        i = i + 1;
    }
    sml_bezug_total = 1234.5;
    addLog("sml_chart_bezug: demo data loaded");
}
#endif

// ============================================================================
// Production-side helpers — baselines & arrays track the VIRTUAL feed-in
// total `sml_bezug_total` instead of a real meter counter.
// ============================================================================

void sml_chart_pv_save() {
#ifdef CHART_CSV
    int h = fileOpen("/sml_chart_bz.csv", "w");
#else
    int h = fileOpen("/sml_chart_bz.bin", "w");
#endif
    if (h < 0) return;
#ifdef CHART_CSV
    fileWriteArray(sml_dprod, h, 31, 0, CHART_CSV_DEC);
    fileWriteArray(sml_mprod, h, 12, 0, CHART_CSV_DEC);
#else
    fileWriteBin(h, sml_dprod, 31);
    fileWriteBin(h, sml_mprod, 12);
#endif
    fileClose(h);
}

void sml_chart_pv_load() {
#ifdef CHART_CSV
    int h = fileOpen("/sml_chart_bz.csv", "r");
#else
    int h = fileOpen("/sml_chart_bz.bin", "r");
#endif
    if (h < 0) return;
#ifdef CHART_CSV
    fileReadArray(sml_dprod, h, 31);
    fileReadArray(sml_mprod, h, 12);
#else
    fileReadBin(h, sml_dprod, 31);
    fileReadBin(h, sml_mprod, 12);
#endif
    fileClose(h);
}

void sml_chart_pv_init_baselines() {
    sml_dval2 = sml_bezug_total;
    sml_mval2 = sml_bezug_total;
    sml_yval2 = sml_bezug_total;
    sml_chart_pv_save();
    addLog("sml_chart_bezug: virtual production baselines set");
}

void sml_chart_pv_init_columns() {
    int i = 0;
    while (i < 31) { sml_dprod[i] = 0.0; i = i + 1; }
    i = 0;
    while (i < 12) { sml_mprod[i] = 0.0; i = i + 1; }
    sml_chart_pv_save();
}

// 5 s tick — accumulate negative power (export) into Watt-sum and run
// the same EMA smoothing as sml_chart_pv.
void sml_chart_pv_5s_tick() {
    float p = smlGet(1);
    if (p < 0.0) sml_ptogrid_sum = sml_ptogrid_sum - p;   // accumulate |export|
    sml_power2 = 0.9 * sml_power2 + 0.1 * p;
    if (sml_sndpwr) {
        sprintf(sml_buf, "publish stat/%%topic%%/script/power2 %d", (int)sml_power2);
        tasmCmd(sml_buf, sml_pv_resp);
    }
}

// 60 s tick — finalise the virtual feed-in increment, run the production
// daily/monthly rollover, handle midnight transitions for the virtual
// baselines.
void sml_chart_pv_60s_tick() {
    // Integrate the per-minute Watt-sum into the kWh total. Magic constant
    // 720 000 = 12 samples/min × 60 s × 1000 W (per ottelo's formula).
    if (sml_ptogrid_sum > 0.0) {
        sml_bezug_total = sml_bezug_total + sml_ptogrid_sum / 720000.0;
        sml_ptogrid_sum = 0.0;
    }

    int d = tasm_day;
    int m = tasm_month;
    if (d >= 1 && d <= 31) sml_dprod[d - 1] = sml_bezug_total - sml_dval2;
    if (m >= 1 && m <= 12) sml_mprod[m - 1] = sml_bezug_total - sml_mval2;

    int hr = tasm_hour;
    if (sml_pv_hr_last != hr && hr == 0) {
        if (d == 1) {
            int i = sml_da;
            while (i < 31) { sml_dprod[i] = 0.0; i = i + 1; }
            sml_mval2 = sml_bezug_total;
        }
        if (d == 1 && m == 1) sml_yval2 = sml_bezug_total;
        sml_dval2 = sml_bezug_total;
        sml_chart_pv_save();
    }
    sml_pv_hr_last = hr;
}

// ============================================================================
// Render helpers — same chart layout as sml_chart_pv.tc
// ============================================================================

void sml_chart_pv_render_dayprod() {
    int today = tasm_day;
    if (today < 1) today = 1;
    webSend("<div id='sml_dpch' style='text-align:center;width:600px;height:280px'></div>");
    webSend("<script>function _smlDP(){var d=google.visualization.arrayToDataTable([['Tag','Einspeisung [kWh]',{role:'style'}]");
    int i = 1;
    while (i <= 31) {
        if (i < today)        sprintf(sml_buf, ",[%d,%.2f,'green']", i, sml_dprod[i - 1]);
        else if (i == today)  sprintf(sml_buf, ",[%d,%.2f,'red']",   i, sml_dprod[i - 1]);
        else                  sprintf(sml_buf, ",[%d,%.2f,'blue']",  i, sml_dprod[i - 1]);
        webSend(sml_buf);
        i = i + 1;
    }
    webSend("]);new google.visualization.ColumnChart(document.getElementById('sml_dpch')).draw(d,{chartArea:{left:50,right:20,height:'75%'},legend:'none',title:'Tageseinspeisungen Monatsansicht (virtuell)',vAxis:{format:'# kWh'},hAxis:{title:'Tag',ticks:[1,5,10,15,20,25,30]}});}google.charts.setOnLoadCallback(_smlDP);</script>");
}

void sml_chart_pv_render_months_dual() {
    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_mch2' style='text-align:center;width:600px;height:280px'></div>");
    webSend("<script>function _smlDM2(){var d=google.visualization.arrayToDataTable([['Monat','Verbrauch [kWh]','Einspeisung [kWh]']");
    int i = 1;
    while (i <= 12) {
        strToken(mn, mn_names, '|', i);
        sprintf(sml_buf, ",['%s',%.2f,%.2f]", mn, sml_mcon[i - 1], sml_mprod[i - 1]);
        webSend(sml_buf);
        i = i + 1;
    }
    webSend("]);new google.visualization.ColumnChart(document.getElementById('sml_mch2')).draw(d,{chartArea:{left:60,right:60,top:30,height:'70%'},legend:{position:'top'},title:'Monatsverbraeuche / -einspeisungen Jahresansicht',series:{0:{color:'#27ae60',targetAxisIndex:0},1:{color:'#f39c12',targetAxisIndex:1}},vAxes:{0:{format:'# kWh'},1:{format:'# kWh'}},hAxis:{slantedText:false,showTextEvery:1}});}google.charts.setOnLoadCallback(_smlDM2);</script>");
}

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

void EverySecond() {
    sml_descriptor_apply();

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

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

    if (do_init) {
        sml_chart_init_baselines();
        sml_chart_pv_init_baselines();
        do_init = 0;
    }
    if (do_init2) {
        sml_chart_init_columns();
        sml_chart_pv_init_columns();
        do_init2 = 0;
    }
    if (do_save) {
        sml_chart_save();
        sml_chart_pv_save();
        do_save = 0;
    }

    sml_t1 = sml_t1 - 1;
    if (sml_t1 <= 0) {
        sml_t1 = 5;
        sml_chart_5s_tick();
        sml_chart_pv_5s_tick();
    }
    sml_t2 = sml_t2 - 1;
    if (sml_t2 <= 0) {
        sml_t2 = 60;
        sml_chart_60s_tick();
        sml_chart_pv_60s_tick();
    }
}

void WebCall() {
    if (!sml_activ) {
        webSend("{s}SML{m}disabled (Rule1 off){e}");
        return;
    }
    sml_chart_render_totals();
    sprintf(sml_buf, "{s}Leistung (gefiltert){m}%.0f W{e}", sml_power2);
    webSend(sml_buf);
    sprintf(sml_buf, "{s}Tageseinspeisung{m}%.2f kWh{e}",   sml_bezug_total - sml_dval2); webSend(sml_buf);
    sprintf(sml_buf, "{s}Monatseinspeisung{m}%.2f kWh{e}",  sml_bezug_total - sml_mval2); webSend(sml_buf);
    sprintf(sml_buf, "{s}Gesamteinspeisung{m}%.2f kWh{e}",  sml_bezug_total);             webSend(sml_buf);
    sprintf(sml_buf, "{s}Jahreseinspeisung{m}%.2f kWh{e}",  sml_bezug_total - sml_yval2); webSend(sml_buf);
}

void WebPage() {
    // ottelo's chart-centering compensation (margin-left:-30px wrapper) —
    // counters the ~30 px right-shift of the Tasmota main page.
    webSend("<div style='margin-left:-30px'>");
    sml_chart_render_4h();
    sml_chart_render_24h();
    sml_chart_render_days();
    sml_chart_pv_render_dayprod();
    sml_chart_pv_render_months_dual();
    webSend("</div>");
}

void WebUI() {
    sml_render_settings_panel();
    webSend("<hr><b>&#x1F527; Optionen</b>");
    webCheckbox(sml_sndpwr, "MQTT: Gemittelte Leistung senden");
    webSend("<hr><b>&#x1F4BE; Daten</b>");
    webButton(do_init,  "Zaehlerwerte initialisieren|initialisiert");
    webButton(do_init2, "Balkendiagramme zuruecksetzen|zurueckgesetzt");
    webButton(do_save,  "Diagrammdaten speichern|gespeichert");
    webSend("<hr><b>&#x1F504; SML neu initialisieren</b>");
    webButton(do_reset, "Sensor53 r|SML neu geladen");
    webSend("<div style='text-align:center;font-size:10px;color:#777;margin-top:10px'><b>sml_chart_bezug.tc</b><br>TinyC port of ottelo's 3_SML_Chart_PV_Bezugszaehler.tas</div></div>");
}

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_chart_load();
    sml_chart_pv_load();

#ifdef SML_CHART_DEMO
    sml_chart_pv_demo_fill();
#endif

    webPageLabel(0, "Einstellungen / Daten");
    webPageLabel(1, "");

    smlScripterLoad("/sml_meter.def");

    addLog("sml_chart_bezug: started rx=%d tx=%d meter=%d activ=%d virt_total=%.3f",
           sml_rx_pin, sml_tx_pin, sml_meter_sel, sml_activ, sml_bezug_total);
    return 0;
}