sml_chart_bezug.tc¶
sml_chart_bezug.tc — SML reader for meters with no feed-in counter
// ============================================================================
// 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>🔧 Optionen</b>");
webCheckbox(sml_sndpwr, "MQTT: Gemittelte Leistung senden");
webSend("<hr><b>💾 Daten</b>");
webButton(do_init, "Zaehlerwerte initialisieren|initialisiert");
webButton(do_init2, "Balkendiagramme zuruecksetzen|zurueckgesetzt");
webButton(do_save, "Diagrammdaten speichern|gespeichert");
webSend("<hr><b>🔄 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;
}