Skip to content

Commit 4f9aceb

Browse files
committed
Refactor automation rule table to nested two-level grid (Solar Assistant parity)
- Replace flat conditions/action_value model with nested groups->ranges structure: setting (top-level write) + group_kind + groups[] (each with condition + ranges[] where each range carries its own action_value). - Both group_kind and range_kind are free-choice condition kinds (nothing hardcoded to time/voltage). - Evaluation: find matching group -> find matching range -> write that range's action_value to setting. Restore value fires on no-match. - Rewrite rule_table UI form as nested grid with +/- buttons on both levels, free-choice dropdowns, and per-range action value input. - Fix fetchJson to pass through options (was silently dropping POST/DELETE bodies, causing saves to fire as GETs and wipe local state). - Fix automation _log: correct rule_id/rule_name variable names and use 'ts' column instead of 'created_at'. - Rewrite tests for nested model; 27 tests pass.
1 parent 70cb258 commit 4f9aceb

9 files changed

Lines changed: 2225 additions & 1816 deletions

File tree

‎README.md‎

Lines changed: 54 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ Works with EG4, LuxPower, and any rebranded inverter using the LuxPower WiFi don
1111
- **Stores** time-series data in **MariaDB/MySQL** (InfluxDB optional, both can run together)
1212
- **Exposes** a **REST API** for scripting, morning briefings, Home Assistant, etc.
1313
- **Streams live snapshots** over a **WebSocket** (`/ws`) so the web dashboard updates instantly
14-
- **Writes inverter settings** via a safe automation rule engine with dry-run, clamping, and action logging
14+
- **Writes inverter settings** via a safe holding-register write path with clamping and verification
15+
- **SolarAssistant-style automations** — rule table, battery SOC control, battery protection, and notifications, with global dry-run for safety (38/57 settings mapped)
1516
- **Quick Charge / Generator Charge** — one-shot timed grid charge with automatic restore
1617
- **Solar PV forecast** — weather-based (Open-Meteo) with historical calibration, persisted to MariaDB
1718
- **Alerts** — SOC/temperature/grid-loss thresholds with SMTP + webhook notifications
@@ -29,7 +30,7 @@ This project is actively running on private hardware monitoring an EG4 6000XP in
2930
| Collector | ✅ Live | 111 input registers decoded, writes to MariaDB + InfluxDB + MQTT |
3031
| REST API | ✅ Live | FastAPI on port 80, systemd-managed |
3132
| Storage | ✅ Live | MariaDB, InfluxDB v2, hourly energy rollups |
32-
| Dashboard | ✅ Live | Web UI with gauges, charts, battery, totals, settings, automations |
33+
| Dashboard | ✅ Live | Web UI with gauges, charts, battery, totals, settings, automations, schedule editor |
3334
| Active polling | ✅ Built | Fallback for non-broadcasting dongles |
3435
| Runtime settings | ✅ Live | DB-backed, editable from dashboard ⚙️ tab |
3536
| Docker image | ✅ Published | Stable: `jmewing/lux-mon:v1.0.1`; Beta: `jmewing/lux-mon:v1.1.0-beta.1` (amd64 + arm64) on Docker Hub and GHCR |
@@ -38,10 +39,10 @@ This project is actively running on private hardware monitoring an EG4 6000XP in
3839
| Solar forecast | ✅ Live | Open-Meteo weather forecast + historical calibration (v1.2.1) |
3940
| Quick charge | ✅ Live | Timed grid charge with restore-on-expiry |
4041
| Home Assistant | ✅ Integrated | Native REST integration + MQTT auto-discovery + energy sensors |
41-
| Schedule editor | ✅ Fixed | Reads/writes actual inverter holding registers (Grid charge, Charge priority, AC first, Forced discharge) |
42+
| Schedule editor | ✅ Fixed | Reads/writes actual inverter holding registers (Grid charge + AC first) |
43+
| Automations | ✅ Live | SolarAssistant-style rule table, SOC control, battery protection, notifications (global dry-run; 38/57 settings mapped) |
4244
| Backup/restore | ✅ Built | Nightly systemd timer + one-command restore |
4345
| Grafana | ✅ Built | Pre-loaded dashboards and data source |
44-
| Automation rules | ✅ Built | Rule-table model (time + sensor conditions) that writes holding registers |
4546

4647
### Supported inverter models
4748

@@ -68,13 +69,12 @@ families:
6869
### In progress / near-term
6970

7071
- **Inverter Edit Mode page** — manual Read/Set for every editable EG4 6000XP holding register, mirroring the EG4 Monitor Maintenance tab.
71-
- **Automation page overhaul** — rebuild around a premade option list + wizard (like SolarAssistant's Power Management), with restore-value support and time/day conditions.
7272
- **Holding register map corrections** — align `collector/protocol.py` with the `LXP_REGISTERS.txt` reference, add missing registers, fix ranges, and correct AC charge current to register 168.
7373

7474
### Roadmap
7575

7676
- Generator and AC-coupled charge support (generator-charge register path is stubbed)
77-
- Battery protection automations (SOC threshold + restore) — rule-table model supports this, needs UI
77+
- A general automation/rule engine (condition → setting write) — see `docs/solarassistant-automation-map.md` for the SolarAssistant reference field map
7878
- More inverter models via pluggable Modbus RTU drivers (FlexBOSS/GridBOSS need a dedicated register map)
7979
- Forecast.Solar provider (listed in settings, not yet wired)
8080

@@ -225,14 +225,18 @@ The API server runs on port 80 and provides:
225225
| `GET /api/holding` | Read actual holding-register values directly from the inverter |
226226
| `GET /api/holding/{name}` | Read a single named holding register from the inverter |
227227
| `PUT /api/holding/{name}` | Write a single named holding register to the inverter |
228-
| `GET /api/automation/registers` | List writable holding registers for automation actions |
229228
| `GET /api/automation/types` | List automation types (rule_table, battery_soc, battery_protection, notify) |
230-
| `GET /api/automation/rules` | List automation rules and global enable flag |
231-
| `POST /api/automation/rules` | Replace the full rule set (JSON array) |
232-
| `DELETE /api/automation/rules/{rule_id}` | Delete a single rule |
229+
| `GET /api/automation/conditions` | List condition dimensions for automation rules |
230+
| `GET /api/automation/settings` | List the 50 SolarAssistant-style writable settings with register mapping |
231+
| `GET /api/automation/rules` | List automations + global enable + dry-run |
232+
| `POST /api/automation/rules` | Replace the full automation configuration |
233+
| `DELETE /api/automation/rules/{id}` | Delete one automation |
233234
| `POST /api/automation/enable` | Globally enable/disable the automation engine |
234-
| `POST /api/automation/test` | Dry-run evaluate rules against the latest snapshot |
235+
| `POST /api/automation/dry-run` | Globally enable/disable dry-run mode |
236+
| `POST /api/automation/rules/{id}/disable` | Temporarily disable an automation for N minutes |
237+
| `POST /api/automation/test` | Dry-run evaluate automations against the latest snapshot |
235238
| `GET /api/automation/log` | Recent automation actions / dry-runs |
239+
| `GET /api/automation/registers` | List writable holding registers (used by the schedule editor) |
236240
| `GET /api/quick-charge/status` | Current quick-charge state and defaults |
237241
| `POST /api/quick-charge/start` | Start a timed quick charge (JSON: `{"minutes": 60}`) |
238242
| `POST /api/quick-charge/stop` | Stop an active quick charge, restoring the prior value |
@@ -242,50 +246,44 @@ The API server runs on port 80 and provides:
242246
| `GET /api/storage` | Show DB table sizes and disk usage |
243247
| `WS /ws` | Live snapshot WebSocket stream |
244248

245-
The built-in dashboard (`/`) includes an **Automations** page where you can enable the engine, add rules with time windows and sensor conditions, and test them in dry-run mode before allowing live inverter writes.
246-
247-
### Automation rules
248-
249-
Rules are stored as JSON in the `automation_rules` setting and evaluated after every snapshot. The engine uses a **rule-table model** (target-first, nested subset columns, restore-on-exit) that mirrors the EG4/Luxpower "Power Management" portal. Four automation types are supported:
250-
251-
- `rule_table` — a multi-dimensional rule table (e.g. grid charge current as a function of time-of-day and battery voltage)
252-
- `battery_soc` — battery state-of-charge control (time + SOC thresholds → grid/battery output source)
253-
- `battery_protection` — "if SOC ≤ X%, shutdown output; restore to Y when recovered" (restore-on-exit)
254-
- `notify` — send an email/webhook notification when a condition is met
255-
256-
Only one automation may be active per target type. A rule can set a holding register to a static value or choose a value from a sensor-range table. Each rule supports:
257-
258-
- `time_window`: `{"start": "21:00", "end": "06:00"}` (wraps past midnight)
259-
- `conditions`: list of `{"sensor": "battery_voltage", "min": 0, "max": 54}` checks
260-
- `action`: `{"register": "ac_charge_battery_current", "value": 85}` or a `ranges` table
261-
- `dry_run`: when `true`, the rule logs what it *would* write but never sends a Modbus command
262-
- `restore`: a value written back when conditions stop matching (or the automation is disabled)
263-
264-
Only registers listed in `collector/protocol.py:HOLDING_REGISTERS` can be written, and every value is clamped to the register's documented min/max. Example rule matching SolarAssistant's night grid-charge behavior:
265-
266-
```json
267-
{
268-
"id": "night_grid_charge",
269-
"name": "Grid charge current (night)",
270-
"enabled": true,
271-
"dry_run": true,
272-
"time_window": {"start": "21:00", "end": "06:00"},
273-
"conditions": [],
274-
"action": {
275-
"register": "ac_charge_battery_current",
276-
"range_sensor": "battery_voltage",
277-
"ranges": [
278-
{"min": 0, "max": 54.0, "value": 85},
279-
{"min": 55.0, "max": 56.0, "value": 45},
280-
{"min": 57.0, "max": 58.0, "value": 5}
281-
]
282-
}
283-
}
284-
```
249+
The built-in dashboard (`/`) includes a **Power Management** page with three tools:
250+
251+
- **Automations** — SolarAssistant-style condition → action rules with a global dry-run toggle.
252+
- **Timer Schedule** — Grid charge + AC first time slots.
253+
- **Quick Charge** — one-shot timed grid charge.
254+
255+
### Automations
256+
257+
Click **Add Automation** to create a rule. Choose from four types:
258+
259+
- **Rule table** — pick a setting, add up to two conditions, set the action value, and optionally a restore value.
260+
- **Battery state of charge control** — time-of-day + SOC thresholds to choose grid vs battery source.
261+
- **Battery protection** — if SOC drops below a threshold, write a shutdown voltage; restore when SOC recovers.
262+
- **Send notification** — when a condition is met, send an email/webhook via the existing alert notifier.
263+
264+
Conditions include time-of-day, day-of-week, month-of-year, battery SOC/voltage/current, grid voltage/frequency, PV progress, load power, and inverter/battery temperatures.
265+
266+
**Safety:** the automation engine has a **global dry-run toggle** (`automation_global_dry_run`) that is ON by default. While dry-run is enabled, the engine evaluates every rule and logs what it *would* write, but it never sends a Modbus command. Turn dry-run OFF only when you are confident the rules behave as intended.
267+
268+
Individual automations can be enabled/disabled and temporarily disabled for 30 minutes up to 24 hours from the dashboard card. Saved automations are stored as JSON in the `automations_v2` setting.
269+
270+
### Schedule editor
271+
272+
The schedule editor writes directly to the inverter's holding registers via
273+
`PUT /api/holding/{name}`. It covers two groups:
274+
275+
- **Grid charge** — `ac_charge_period_*` (3 slots) plus charge power and SOC limit
276+
- **AC first** — `ac_first_period_*` (3 slots)
285277

286-
Enable the engine globally with the `automation_enabled` setting (or the dashboard toggle). Set `dry_run: false` on a rule only when you are ready for live inverter writes.
278+
Time slots are encoded as `(minute << 8) | hour` (minute in the high byte,
279+
hour in the low byte), matching the LuxPower register format. Writes are sent
280+
sequentially (not in parallel) because the dongle rejects bursts of
281+
simultaneous write connections.
287282

288-
**Design note:** Readable values (SOC, voltage, current, power) are telemetry and are included automatically. Set values (charge current, SOC limits, time slots, modes) are only written when explicitly changed via the website or when an automation rule says "this is what we want it to be." lux-mon never writes a setting just because it happens to be readable.
283+
**Design note:** Readable values (SOC, voltage, current, power) are telemetry
284+
and are included automatically. Set values (charge current, SOC limits, time
285+
slots, modes) are only written when explicitly changed via the website. lux-mon
286+
never writes a setting just because it happens to be readable.
289287

290288
### Quick Charge
291289

@@ -312,8 +310,8 @@ not the quick-charge toggle.
312310
The quick-charge control lives on the **main dashboard** (a button with a
313311
▼-toggle for a custom duration, 1–240 minutes, default 60). State is persisted
314312
in the `lux_settings` table so it survives collector restarts, and all actions
315-
are logged to the automation log. Writes reuse the automation engine's safe
316-
write helpers (fresh socket, echo verification, clamping).
313+
are logged. Writes use the shared holding-register write helpers (fresh socket,
314+
echo verification, clamping).
317315

318316
### Solar PV forecast
319317

@@ -446,7 +444,7 @@ Tables are auto-created on first run:
446444
- `lux_settings` — runtime settings (key/value)
447445
- `lux_alerts` — alert events
448446
- `lux_solar_forecast` — solar PV forecast time series
449-
- `lux_automation_log` — automation actions / dry-runs
447+
- `lux_automation_log` — quick-charge action log
450448

451449
Set `LUX_MARIADB_TABLE_PREFIX` to change the table prefix from `lux_` if needed.
452450

‎TODO.md‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
- `collector/settings.py` lists `forecast_provider` options `open-meteo` and `forecast.solar`, but `collector/forecast.py` only implements the Open-Meteo path.
2525
- **To do:** implement the Forecast.Solar provider (or remove the option from settings until it's real).
2626

27-
### 3. Battery protection automations — 🚧 (engine supports it, no UI)
28-
- The automation engine supports the `battery_protection` type (SOC threshold + restore-on-exit), but there is no dashboard UI to configure it.
29-
- **To do:** add a battery-protection wizard/panel to the Automations page.
27+
### 3. Battery protection automations — ✅ (implemented in v2 automation engine)
28+
- Implemented 2026-08-28 as part of the SolarAssistant-style automation engine.
29+
- The `battery_protection` type triggers on SOC ≤ threshold, writes the shutdown battery voltage, and restores when SOC recovers.
3030

3131
### 4. Inverter Edit Mode page — ⬜ (explicitly "in progress")
3232
- Manual Read/Set for every editable EG4 6000XP holding register, mirroring the EG4 Monitor Maintenance tab.
@@ -38,9 +38,10 @@
3838
- Also fixed: charge-priority prefix mismatch (`charge_priority_period_*` → `forced_charge_period_*`) and added the missing **AC First Mode** group.
3939
- Verified against the EG4 Monitor portal Maintenance tab (read + write round-trip on `ac_charge_period_1_start`).
4040

41-
### 6. Automation page overhaul — ⬜ (explicitly "in progress")
42-
- Rebuild around a premade option list + wizard (like SolarAssistant's Power Management), with restore-value support and time/day conditions.
43-
- The rule-table engine is done; the UI is still the older flat form.
41+
### 6. Automation page overhaul — ✅ (SolarAssistant-style v2 implemented)
42+
- Implemented 2026-08-28 with option-first field generation, four automation types, two-level conditions, restore-on-exit, disable-for timer, global dry-run, and top cards.
43+
- **Mapping status:** 38 of 57 settings mapped to known holding registers. 19 remain unmapped (enum/toggle modes and generator/smart-load threshold families) pending confirmed register addresses.
44+
- **To do:** discover + map the remaining 19 settings, or confirm they are controlled through combined/undocumented registers.
4445

4546
### 7. Holding register map corrections — ⬜ (explicitly "in progress")
4647
- Align `collector/protocol.py` with `LXP_REGISTERS.txt`, add missing registers, fix ranges, correct AC charge current to register 168.
@@ -89,7 +90,7 @@
8990
## Future direction (from README roadmap)
9091

9192
- ⬜ Generator and AC-coupled charge support (see #1)
92-
- ⬜ Battery protection automations UI (see #3)
93+
- 🚧 Complete SolarAssistant setting → register mapping for automations (generator-charge + smart-load families)
9394
- ⬜ More inverter models via pluggable Modbus RTU drivers (see #7)
9495
- ⬜ Forecast.Solar provider (see #2)
9596

@@ -103,4 +104,4 @@
103104
- ✅ RS-485 BMS daemon (EG4 A5/5A, JK BMS, Modbus RTU)
104105
- ✅ Native Home Assistant integration (`jmewing/ha_luxmon`) + HAOS add-on (`jmewing/ha_luxmon_addons`)
105106
- ✅ API port default changed 8080 → 80 repo-wide
106-
- ✅ Automation rule-table engine (target-first, nested subsets, restore-on-exit)
107+
- ✅ Automation v2 engine — SolarAssistant-style rule table, SOC control, battery protection, notifications, global dry-run, multilevel conditions, disable-for timer

0 commit comments

Comments
 (0)