The device watches the battery, grid and weather, can send Telegram and change inverter mode.
Rules are a short script (simplified JavaScript). File on the device: /automation.js, up to 6 KB.
Rules run every ~1.5 s, starting 20 seconds after boot.
edge/at state is not changed either).fn setup() {
}
fn tick() {
if (edge(pid(100) < 50)) {
tg("Grid lost, battery [pid110]%");
}
}
pid(100) is grid voltage. [pid110] in the text is replaced with the live battery %. Any PID works the same way.
They edit the same file. Change one tab — you see it in the other after Save or switching tabs.
A card is one simple rule: “when … then do …”. The list shows the name and on/off. Click the name to open one card.
On a card you can:
// @vis Grid lost
if (edge(pid(100) < 50)) { tg("Grid lost"); }
// @endvis
A new Visual card writes these markers for you. Complex code (else, nested if, your own formulas) belongs in Script without @vis — then cards will not touch or break it.
Below the cards: “Show on home” (expose). Templates at the bottom are typical scenarios.
Variables, your own functions, if / else, night-tariff with two branches live here. Visual does not erase this code.
// Comment to end of line. The device ignores it.
let min_soc = 25; // global variable (kept between ticks)
fn setup() { // once after Save or reboot
expose(800, "Threshold", "%");
set(800, min_soc);
}
fn tick() { // every ~1.5 seconds
set(800, min_soc);
if (pid(110) < min_soc && every(600)) {
tg("Battery [pid110]%");
}
}
fn setup() — startup: show a value on the home page, initial set.fn tick() — the working heart. All “if battery…” rules go here.let — only at the top, not inside a function. Name up to 11 Latin characters: min_soc is OK, night_min_soc is too long.; after every call and assignment.if and else always need braces { } (in else if the if has its own). Without them Check fails.// is dropped from the working copy (@vis markers too). A comment at the end of a code line stays and is skipped by the parser.There is no for / while on purpose: the script always finishes and cannot hang.
The most common beginner mistake: if (pid(110) < 30) { tg("..."); } — Telegram floods every 1.5 s. Pick a trigger type:
| I want | Write | In plain words |
|---|---|---|
| Once, when it happens | if (edge(COND)) { ... } |
Fires on the false→true transition. After reboot, already-true does not fire again. |
| Remind while the condition holds | if (COND && every(600)) { ... } |
Every 600 s (10 min) while COND is true. Put every on the right. The first call only arms the timer (still false) — the first fire is after N seconds. |
| Wait until it has held for N seconds | if (held(COND, 300)) { ... } |
Becomes true and stays true every tick while COND holds. For a one-shot action: edge(held(…, 300)). Resets as soon as COND is false. |
| Exactly at 23:00, once a day | if (at("23:00")) { ... } |
Once per calendar day in that minute. After a reboot in the same minute it can fire again. |
| All the time while true | if (COND) { ... } |
Every tick. Almost never what you want for Telegram / inverter writes. |
if (edge(pid(100) < 50)) { tg("Grid lost"); }
Maximum — several “when”s in one tick:
if (edge(pid(100) < 50)) { tg("Grid lost, SOC [pid110]%"); }
if (edge(pid(100) > 50)) { tg("Grid [pid100]V"); }
if (pid(110) < 25 && every(600)) { tg("Low SOC [pid110]%"); }
if (pid(509) > 40 && every(1800)) { tg("Hot [pid509]°C"); }
Comparisons: > < >= <= == !=. Logic: && (and), || (or), ! (not). Parentheses ( ) control order.
You can mix PIDs: pid(106) - pid(117) > 1500 (PV surplus).
if without edge/every runs every ~1.5 s. Telegram in that branch would flood the chat (and you still only get 1 message per tick). Below is branch syntax; in real rules add edge or every.
if (pid(110) < 20) {
set(800, 1);
} else {
set(800, 0);
}
set(800, …) on a home-page expose value is safe every tick (RAM, not an inverter command). Do not do that to pid132.
The language understands else if. Branches are checked top to bottom; the first true one runs.
if (pid(110) < 15) {
set(800, 1);
} else if (pid(110) < 30) {
set(800, 2);
} else if (pid(110) > 90) {
set(800, 3);
} else {
set(800, 0);
}
if (at("23:00")) {
if (pid(513) >= 3000 && pid(110) >= 25) {
set(132, 2);
} else {
set(132, 1);
set(131, 2);
tg("Night charge, tomorrow [pid513]");
}
}
at it is safer to ask “is it 23:00?” first, then weather / SOC inside.
Do not write at("23:00") && pid(513) < 1000 the other way around: if the forecast is not yet “that”, today’s shot is already spent.
cond ? if_true : if_false
let flag = 0; // at the top of the file, not inside fn
fn tick() {
flag = pid(509) > 40 ? 1 : 0;
if (edge(flag == 1)) { tg("Heat [pid509]°C"); }
}
Several lines inside braces run in order.
| Call | What it does |
|---|---|
set(132, 2) | Write a parameter (inverter mode, GPIO, Tasmota, web button). For dropdowns — the option index, not amps: see “PID list”. |
tg("text [pid110]%") | Telegram. One per tick. If the subscription expired — not sent. |
log("...") | Device log line (code 12). Up to 3 per tick, text up to 64 characters. |
http("http://192.168.0.4/cm?cmnd=Power+1") | LAN GET, ~0.6 s timeout, no response wait. One per tick. Space in URL → +. A request to the device’s own IP is ignored. |
if (at("07:00")) { set(132, 2); }
Maximum — several actions together:
if (pid(513) < 1000 && at("23:00")) {
set(132, 1);
set(138, 4);
tg("Night charge, tomorrow [pid513] Wh/m², SOC [pid110]%");
log("night charge");
}
set on inverter PIDs (priorities, currents) really changes settings. Check first, then Save. For a dry run use the buzzer PID (often 123) or a home-page variable (800–807).
Device clock (NTP). now() is an integer HHMM: 7:30 → 730, 23:00 → 2300.
| Function | Example | When true |
|---|---|---|
between("07:00", "21:00") | day | From 07:00 to 21:00 inclusive |
between("22:00", "06:00") | night | Across midnight: 22:00…00:00…06:00 |
at("23:00") | moment | Once a day in that minute (after a reboot in the same minute — again) |
hm("07:30") | → 730 | For now() >= hm("07:00") && now() < hm("21:00") |
dow() | 0…6 | 0 = Sunday, 6 = Saturday |
if ((dow() == 0 || dow() == 6) && at("10:00")) {
tg("Weekend, SOC [pid110]%");
}
Do not parse pid611 in the script — use now / at / between. After reboot wait for time sync (or pid(608) > 2 — uptime in minutes).
Up to 16. They live between ticks and reset on reboot and when you save the script. They are not written to flash.
let peak = 0;
fn tick() {
peak = max(peak, pid(106));
if (at("23:50")) { peak = 0; }
}
PID number 800–807 (or a free one). Not stored in daily charts.
fn setup() {
expose(800, "Peak PV", "W");
}
fn tick() {
set(800, peak);
}
In Visual the same block sits under the cards.
Up to 8 functions including setup and tick (so up to 6 of your own). No parameters, name up to 11 characters. return is the result.
fn excess() {
return pid(106) - pid(117);
}
fn tick() {
if (edge(held(excess() > 1500 && pid(110) > 80, 300))) {
set(1000, 1);
}
}
return; inside tick skips the rest of the rules for that cycle.
| Function | Purpose |
|---|---|
pid(n) | Current value. Empty → -1. |
set(n, v) | Write a parameter. |
tg("...") log("...") http("...") | Telegram / log / HTTP GET. |
now() hm("HH:MM") | Time as HHMM. |
between("A","B") | In range; if A > B — across midnight. |
at("HH:MM") | Once per calendar day in that minute. After a reboot in the same minute it can fire again. |
dow() | Weekday 0…6. |
edge(x) every(sec) held(x, sec) | Rising edge / period / hold. State is tied to the place of the call in the text. |
avg(pid, sec) | Smoothed PID (fewer false triggers). |
min(a,b) max(a,b) abs(x) | Math. |
expose(pid,"name","unit") | Home-page value. Up to 8. |
Also: + - * / %, comparisons, && || !, ?:. == has a 0.001 tolerance (25 and 25.0005 count as equal).
Grid lost / restored
if (edge(pid(100) < 50)) {
tg("Grid lost! Battery [pid110]% ([pid109]V)");
}
if (edge(pid(100) > 50)) {
tg("Grid [pid100]V");
}
Two separate ifs, not else if: otherwise the second edge does not update while the first fired, and “grid restored” can be missed.
Night on battery if SOC is high
if (edge(between("22:00", "06:00") && pid(110) > 60)) {
set(132, 2);
tg("Night, battery [pid110]%");
}
PV surplus for 5 min → Tasmota socket (pid1000)
if (edge(held(pid(106) - pid(117) > 1500 && pid(110) > 80, 300))) {
set(1000, 1);
}
if (edge(pid(110) < 40 || pid(106) < 500)) {
set(1000, 0);
}
On its own held stays true every tick — without edge, set would hammer the relay every 1.5 s.
GPIO: light at 18:00 / 22:00 (pin mode is set in the parameter settings)
if (at("18:00")) { set(704, 1); }
if (at("22:00")) { set(704, 0); }
More complex: night charge from forecast + morning SBU
Write this in Script without @vis. Thresholds and pid131/132/138 indexes depend on your inverter — see “PID list”.
let sun_ok = 3000;
let min_soc = 25;
let grid_a = 4;
fn tick() {
if (at("23:00")) {
if (pid(513) >= sun_ok && pid(110) >= min_soc) {
set(131, 3);
set(132, 2);
} else {
set(132, 1);
set(131, 2);
set(138, grid_a);
tg("Night charge. Tomorrow [pid513], SOC [pid110]%");
}
}
if (at("07:00")) {
set(132, 2);
set(131, 1);
}
}
pid513 is tomorrow’s solar forecast (Wh/m²). pid506 is “is it sunny now” — not for tomorrow.
let, 8 × fn (including setup/tick), 8 × expose, 8 × avg, 24 slots for edge/every/held/at._, up to 11 characters.tg/log/http/at/…tg run in one cycle — only the first is sent.edge/at call is a separate place in the text. Do not copy “the same” call twice thinking it is one counter.if, use edge or every.held / avg.pid(n) can be -1 (not read yet).}, undef, need (, no loops).@vis/@endvis around it, leave it in Script only.Full parameter list — the PID list button on the automation page. GPIO 7xx, Tasmota 1000+, BMS 9xx — as in your table.
If /automation_PARAMS.json exists and /automation.js does not yet — the device converts the rules itself (offline). JSON stays as a copy. Old rules become cards with @vis.
Firmware 4.24+. Check → Save. Complex rules — in Script without cards; simple ones — in Visual with @vis.