↩ Back

Українська

Inverter automation

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 6144 bytes (non-Latin letters take 2 bytes each; a counter sits under the editor).

Rules run every ~1.5 s, starting 20 seconds after boot.

1. Getting started

  1. Open the Automation page.
  2. Script is the full text. Visual is simple cards.
  3. Press Check — the device checks syntax with no Telegram, HTTP or inverter writes (edge/at state is not changed either).
  4. If OK — Save.
Is the script running right now? The status line above the buttons: ▶ running — rules are executing; ⛔ … — the script is stopped, with the reason and line number (same text as in the device log, code 16). Save works even with an error (your edits are kept), but the rules do not run until the error is fixed. Save also resets variables and the edge/every/held/at state.
Smallest working script. Copy, Check, Save. When the grid drops, you get a message.
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.

2. Two tabs: Visual and Script

They edit the same file. Change one tab — you see it in the other after Save or switching tabs.

Visual — simple cards

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:

Visual shows only blocks wrapped like this:
  // @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.

Script — full language

Variables, your own functions, if / else, night-tariff with two branches live here. Visual does not erase this code.

3. Script skeleton

// 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]%");
  }
}

There is no for / while on purpose: the script always finishes and cannot hang.

4. When a rule fires

The most common beginner mistake: if (pid(110) < 30) { tg("..."); } — Telegram floods every 1.5 s. Pick a trigger type:

I wantWriteIn 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.
Minimum — grid just dropped:
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"); }

5. Conditions: if, else, nesting

Comparisons: > < >= <= == !=. Logic: && (and), || (or), ! (not). Parentheses ( ) control order.

You can mix PIDs: pid(106) - pid(117) > 1500 (PV surplus).

A bare 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.

One level

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.

Several levels (else if)

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);
}

Nested if inside if

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]");
  }
}
For 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.

Short form without if

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"); }
}

6. Actions

Several lines inside braces run in order.

CallWhat it does
set(132, 2)Write a parameter (inverter mode, GPIO, Tasmota, web button). For dropdowns — the option index, not amps: see “PID list”. If the parameter already holds that value the write is skipped (buttons always fire). After a failed inverter write the remaining set calls of that tick are skipped so data reading is not stalled.
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, ~1.5 s timeout, the response is not read. One per tick. Space in URL → +. [pid110] in the URL is substituted like in tg. A request to the device’s own IP is ignored.
Minimum — one action:
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).

7. Time of day

Device clock (NTP). now() is an integer HHMM: 7:30 → 730, 23:00 → 2300.

FunctionExampleWhen true
between("07:00", "21:00")dayFrom 07:00 to 21:00 inclusive
between("22:00", "06:00")nightAcross midnight: 22:00…00:00…06:00
at("23:00")momentOnce a day in that minute (after a reboot in the same minute — again)
hm("07:30")→ 730For now() >= hm("07:00") && now() < hm("21:00")
dow()0…60 = 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. Until the clock is synced (right after a reboot without internet) now() = 9999 and between / at are false, so night rules do not fire by mistake.

8. Variables, expose, your functions

Variables

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; }
}

Home-page value — expose

PID number 800–807 (or a free one). Not stored in daily charts. A DS18B20 sensor may already use 800 and up — pick the next free number. An expose value can be edited from the home page, the inverter page, or Telegram: that write stays in RAM, no Modbus. The next set in the script overwrites it.

fn setup() {
  expose(800, "Peak PV", "W");
}
fn tick() {
  set(800, peak);
}

In Visual the same block sits under the cards.

Your functions

Up to 8 functions including setup and tick (so up to 6 of your own). No parameters, name up to 11 characters, calls nest up to 4 deep (Check: depth). 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.

9. Function reference

FunctionPurpose
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.
changed(x)1 when the number changes (0.001). The first cycle after Save is 0, so boot does not fire.
cooldown(sec)1 immediately, then 0 until N seconds pass. Unlike every, the first tick is not silent.
hyst(val, on, off)Hysteresis. If on ≥ off: turn on when val ≥ on, off when val ≤ off, keep the previous state in the gap.
sunrise() sunset()Sunrise/sunset as HHMM from weather. No forecast → 9999 (will not fire at 00:00).
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).

10. Ready-made recipes

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.

Low SOC without spam

if (pid(110) < 25 && cooldown(1800)) {
  tg("Battery [pid110]%");
}

SOC hysteresis (Script tab, because of else)

if (hyst(pid(110), 25, 35)) { set(132, 1); }
else { set(132, 2); }

Here on < off: turn on when SOC ≤ 25, off when SOC ≥ 35. Between 25 and 35 the previous state stays.

Light from sunrise to sunset

if (between(sunrise(), sunset())) { set(704, 1); }

Until weather is loaded, sunrise() and sunset() return 9999 and the condition stays false.

11. Limits and common mistakes

Check / status words

WordWhat is wrong
{ } ( ) ; , = :That character was expected on the given line (unclosed bracket, missing ;).
undefUnknown variable: no let for it (or a typo in the name).
unknown fn, need (Unknown function / function used without ().
strQuoted text is required here (at("23:00"), expose(800, "Name", "unit")).
name>11Variable/function name longer than 11 characters.
stmt, top, prim, bad charUnexpected text: only let and fn at top level; inside functions — if, assignment, call, return.
nested let/fn, no loopslet/fn inside a function; for/while are not supported.
lets, fns, slots, avg, depthLimit exceeded: 16 variables, 8 functions, 24 edge/every/held/at, 8 avg, 4 nested calls.
too long, too big, nomem, load failedScript over 6144 bytes / out of memory / file could not be read.
busy, retryThe device is in the middle of a tick (e.g. waiting for the inverter) — press again.
stopped: 3 crashesThe device crash-rebooted three times in a row — automation was disabled as a safeguard. Review the script and press Save (or restart the device from Settings).

Runtime errors (e.g. undef inside a rule) show up in the same status line above the buttons and are written to the device log (Inverter page, code 16) once — until the next Save. Rules above the failing line keep working.

Common mistakes

Full parameter list — the PID list button on the automation page. GPIO 7xx, Tasmota 1000+, BMS 9xx — as in your table.

12. Migrating from old JSON

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.

Rolling back to firmware ≤ 4.23 reads only the JSON at conversion time. Edits already in the script will not go back. Flash beta OTA first, confirm, then stable.

Firmware 4.24+. Check → Save. Complex rules — in Script without cards; simple ones — in Visual with @vis.