↩ 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 6 KB.

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.
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”.
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.
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. After reboot wait for time sync (or pid(608) > 2 — uptime in minutes).

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.

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

11. Limits and common mistakes

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.