sml_chart_eco_shelly.tc¶
sml_chart_eco_shelly.tc — charts + EcoTracker / Shelly Pro 3EM emulator.
// @name: SML Chart + EcoTracker / Shelly
// @info: https://ottelo9.github.io/tasmota-sml-script/eco_shelly/
// sml_chart_eco_shelly.tc — charts + EcoTracker / Shelly Pro 3EM emulator.
// Same charts and PV switch as sml_chart.tc, plus the meter emulation a PV
// battery polls. Without charts use sml_eco_shelly.tc.
// Docs: https://ottelo9.github.io/tasmota-sml-script/eco_shelly/
// Needs ESP32, TinyC in SLOT 0 (Tasmota dispatches every webOn to slot 0),
// USE_SML.
#include "sml_chart_common.tc"
#include "sml_modbus_common.tc"
#include "eco_shelly_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() {
esPoll();
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();
esTick();
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
// cpwr - what the emulator hands the battery - only exists here.
if (sml_sndpwr) {
sprintf(sml_ftopic, "stat/%s/script/cpwr", sml_mtopic);
sprintf(sml_buf, "%d", (int)es_cpwr);
smlPublish(sml_ftopic, sml_buf);
}
}
sml_t2 = sml_t2 - 1;
if (sml_t2 <= 0) {
sml_t2 = 60;
sml_chart_60s_tick();
sml_chart_pv_60s_tick();
}
}
void WebOn() {
int h = webHandler();
if (esWebOn(h)) return; // /v1/json, /rpc/*
if (h != 7) return; // /pwr
sml_cpwr = es_cpwr; sml_hascp = 1;
if (smlVars()) return; // /pwr?v=... single values
smlPwrJson(sml_power2);
sprintfAppend(sml_buf, ",\"cpwr\":%.0f}", es_cpwr);
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();
uiSep();
esRows();
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();
esPanel();
uiHead("☀️", "PV", "PV", "sml/#pv");
if (ui_de) webCheckbox(sml_e_pv, "Einspeisung");
else webCheckbox(sml_e_pv, "Feed-in");
uiHead("🔧", "Telemetry", "Telemetrie", "sml/#telemetry");
if (ui_de) webCheckbox(sml_e_sndpwr, "power2 + cpwr senden (MQTT)");
else webCheckbox(sml_e_sndpwr, "Send power2 + cpwr (MQTT)");
sprintf(sml_buf, "<div class='n'>power2: <b>%.0f W</b></div>", sml_power2);
webSend(sml_buf);
sprintf(sml_buf, "<div class='n'>cpwr: <b>%.0f W</b></div>", es_cpwr);
webSend(sml_buf);
webSend("<b>HTTP API</b>");
webSend("<div class='n'>HTTP: <a href='/pwr' target='_blank'>/pwr</a> <a href='/pwr?v=all' target='_blank'>/pwr?v=all</a> <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_eco_shelly.tc · Version %s<br>", sml_vers);
webSend(sml_buf);
webSend("by <a href='https://ottelo.jimdofree.com' target='_blank'>ottelo.jimdofree.com</a> & 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>");
}
void JsonCall() {
esJson();
}
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();
esInit(1, 2, 3, 5, 6); // /v1/json, EM, Shelly, EM1, EMData
mbInit();
webPageLabel(0, "⚙️ SML /🔋");
webPageLabel(1, "");
webPageLabel(2, ""); // charts page, reached by link
webPageLabel(5, "💻 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_eco_shelly: %s started mode=%d pv=%d modbus=%d",
sml_vers, es_mode, sml_pv, mb_on);
return 0;
}