sml_chart_pv_common.tc¶
sml_chart_pv_common.tc — PV-production helpers shared by the PV variants
// ============================================================================
// sml_chart_pv_common.tc — PV-production helpers shared by the PV variants
// ============================================================================
//
// Factored out of sml_chart_pv.tc so both the plain PV variant and the
// Modbus-server variant (sml_chart_modbus.tc) share one source of truth
// for the production-side logic. Same role as sml_chart_common.tc plays
// for the consumption side.
//
// Builds on sml_chart_common.tc (which itself pulls in sml_descriptor.tc).
//
// Provides:
// * Production-side persist state (energy-out baselines + daily/monthly
// production arrays + the MQTT-publish toggle)
// * sml_chart_pv_save() / load() / init_baselines() / init_columns()
// * sml_chart_pv_5s_tick() — EMA smoothing + optional MQTT publish
// * sml_chart_pv_60s_tick() — production daily/monthly + midnight roll
// * sml_chart_pv_render_dayprod() — daily-production column chart
// * sml_chart_pv_render_months_dual() — dual-series month chart
// * sml_chart_pv_demo_fill() — synthetic data (SML_CHART_DEMO)
//
// **No callbacks / no main() / no do_* button flags here** — the
// including leaf script (sml_chart_pv.tc or sml_chart_modbus.tc) wires
// these into its own callbacks.
// ============================================================================
#include "sml_chart_common.tc"
// ── Production-side persist state ──────────────────────────────────────────
persist float sml_dval2; // daily zero point for energy-out (kWh)
persist float sml_mval2; // monthly zero point
persist float sml_yval2; // yearly zero point
persist float sml_dprod[31]; // 31 days of production totals
persist float sml_mprod[12]; // 12 months of production totals
persist watch int sml_sndpwr; // 1 = publish smoothed power via MQTT
// ── Runtime state ──────────────────────────────────────────────────────────
float sml_power2; // exponentially-smoothed power [W]
int sml_pv_hr_last; // PV-side midnight edge detector
char sml_pv_resp[64]; // tasmCmd response buffer
#ifdef SML_CHART_DEMO
void sml_chart_pv_demo_fill() {
// Match the consumption-side demo curves so the multi-chart page
// shows recognisable shapes before any live meter data lands.
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); // consumption 5..25 kWh
sml_dprod[i] = 9.0 + 7.0 * cos(ph + 1.57); // production phase-shifted
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); // anti-phase to consumption
i = i + 1;
}
addLog("sml_chart_pv: demo data loaded into all six chart blocks");
}
#endif
// ============================================================================
// Production-side helpers — companion to sml_chart_common's consumption set
// ============================================================================
void sml_chart_pv_save() {
#ifdef CHART_CSV
int h = fileOpen("/sml_chart_pv.csv", "w");
#else
int h = fileOpen("/sml_chart_pv.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_pv.csv", "r");
#else
int h = fileOpen("/sml_chart_pv.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() {
float eout = smlGet(3);
sml_dval2 = eout;
sml_mval2 = eout;
sml_yval2 = eout;
sml_chart_pv_save();
addLog("sml_chart_pv: production baselines set from meter");
}
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 smoothing — first-order EMA on smlGet(1) with α=0.1 (heavy smoothing).
// MQTT-publishes the smoothed value when sml_sndpwr is enabled.
void sml_chart_pv_5s_tick() {
sml_power2 = 0.9 * sml_power2 + 0.1 * smlGet(1);
if (sml_sndpwr) {
sprintf(sml_buf, "publish stat/%%topic%%/script/power2 %d", (int)sml_power2);
tasmCmd(sml_buf, sml_pv_resp);
}
}
// 60-s tick — production daily / monthly counters + midnight rollover for
// the production baselines. Mirrors the consumption-side logic in
// sml_chart_common's sml_chart_60s_tick.
void sml_chart_pv_60s_tick() {
float eout = smlGet(3);
int d = tasm_day;
int m = tasm_month;
if (d >= 1 && d <= 31) sml_dprod[d - 1] = eout - sml_dval2;
if (m >= 1 && m <= 12) sml_mprod[m - 1] = eout - sml_mval2;
int hr = tasm_hour;
if (sml_pv_hr_last != hr && hr == 0) {
if (d == 1) {
// Month-wrap — zero days past previous month's length.
int i = sml_da;
while (i < 31) { sml_dprod[i] = 0.0; i = i + 1; }
sml_mval2 = eout;
}
if (d == 1 && m == 1) sml_yval2 = eout;
sml_dval2 = eout;
sml_chart_pv_save();
}
sml_pv_hr_last = hr;
}
// ============================================================================
// Render helpers — production daily chart + dual-series month chart
// (consumption + feed-in). The 4h / 24h line charts and the consumption
// daily column chart come straight from sml_chart_common.
// ============================================================================
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%',backgroundColor:'#f0f2f5'},legend:'none',title:'Tageseinspeisungen Monatsansicht',vAxis:{format:'# kWh'},hAxis:{title:'Tag',ticks:[1,5,10,15,20,25,30]}});}google.charts.setOnLoadCallback(_smlDP);</script>");
}
// Dual-series month chart — overrides sml_chart_common's single-series
// version. ottelo's >W draws consumption AND feed-in side-by-side per
// month, with two independent y-axes so both ranges scale visibly.
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%',backgroundColor:'#f0f2f5'},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>");
}