Skip to content

Commit 4f002ca

Browse files
committed
docs: rewrite README, add TODO, drop RS-485 captures from repo
- README: document solar forecast (v1.2.1), quick/generator charge, alerts, RS-485 BMS, native HA integration + add-on, full API surface, rule-table automation model, multi-backend storage, and forecast/quick-charge settings. - TODO.md: new project status/direction doc (replaces lost PROJECT.md/PLAN.md). - Remove captures/ from tracking (4.6 MB RS-485 BMS reference data); add to .gitignore so future captures stay untracked.
1 parent 4a1197b commit 4f002ca

11 files changed

Lines changed: 205 additions & 60132 deletions

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,6 @@ collector/diff_sampler.py
5252
collector/qc_force_test.py
5353
collector/qc_sampler.py
5454
collector/qc_trigger_test.py
55+
56+
# RS-485 BMS reference captures (large, not needed in-repo)
57+
captures/

‎README.md‎

Lines changed: 105 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,23 @@
22

33
**Local monitoring for LuxPower-based inverters — no cloud required.**
44

5-
Works with EG4, LuxPower, and any rebranded inverter using the LuxPower WiFi dongle protocol (TCP port 8000).
5+
Works with EG4, LuxPower, and any rebranded inverter using the LuxPower WiFi dongle protocol (TCP port 8000). Also supports RS-485 battery BMS monitoring (EG4 A5/5A, JK BMS, generic Modbus RTU).
66

77
## What It Does
88

99
- **Passively listens** to your inverter's WiFi dongle — zero bus contention
1010
- **Actively polls** as a fallback for non-broadcasting dongles
11-
- **Stores** time-series data in **MariaDB/MySQL** (InfluxDB optional)
11+
- **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
1414
- **Writes inverter settings** via a safe automation rule engine with dry-run, clamping, and action logging
15-
- **Home Assistant energy dashboard** ready — publishes MQTT sensors with `state_class: total_increasing` for solar, grid, and battery flows
15+
- **Quick Charge / Generator Charge** — one-shot timed grid charge with automatic restore
16+
- **Solar PV forecast** — weather-based (Open-Meteo) with historical calibration, persisted to MariaDB
17+
- **Alerts** — SOC/temperature/grid-loss thresholds with SMTP + webhook notifications
18+
- **RS-485 BMS monitoring** — EG4 A5/5A, JK BMS, and generic Modbus RTU drivers
19+
- **Home Assistant** — native REST integration, MQTT auto-discovery, and energy-dashboard sensors
1620
- **Runs anywhere** — your Mac, a Raspberry Pi, a Docker container
17-
- **Dashboard-ready** — works with Grafana or any SQL-based visualization tool
21+
- **Dashboard-ready** — built-in web UI plus Grafana dashboards
1822

1923
## Status
2024

@@ -31,10 +35,12 @@ This project is actively running on private hardware monitoring an EG4 6000XP in
3135
| 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 |
3236
| RS-485 / BMS | ✅ Live | `lux-mon-rs485` daemon, EG4 A5/5A battery BMS driver deployed |
3337
| Alerts | ✅ Live | SMTP + webhook notifications, rate-limited, UI configurable |
34-
| Home Assistant | ✅ Integrated | MQTT auto-discovery, energy dashboard sensors |
38+
| Solar forecast | ✅ Live | Open-Meteo weather forecast + historical calibration (v1.2.1) |
39+
| Quick charge | ✅ Live | Timed grid charge with restore-on-expiry |
40+
| Home Assistant | ✅ Integrated | Native REST integration + MQTT auto-discovery + energy sensors |
3541
| Backup/restore | ✅ Built | Nightly systemd timer + one-command restore |
36-
| Grafana | ✅ Built | Pre-loaded `lux-mon-charts` dashboard and data source |
37-
| Automation rules | ✅ Built | Time + sensor conditions that write holding registers |
42+
| Grafana | ✅ Built | Pre-loaded dashboards and data source |
43+
| Automation rules | ✅ Built | Rule-table model (time + sensor conditions) that writes holding registers |
3844

3945
### In progress / near-term
4046

@@ -44,10 +50,10 @@ This project is actively running on private hardware monitoring an EG4 6000XP in
4450

4551
### Roadmap
4652

47-
- Quick Charge button (EG4 Monitor-style timed grid charge)
48-
- Generator and AC-coupled charge support
49-
- Battery protection automations (SOC threshold + restore)
53+
- Generator and AC-coupled charge support (generator-charge register path is stubbed)
54+
- Battery protection automations (SOC threshold + restore) — rule-table model supports this, needs UI
5055
- More inverter models via pluggable Modbus RTU drivers
56+
- Forecast.Solar provider (listed in settings, not yet wired)
5157

5258
## Architecture
5359

@@ -78,6 +84,8 @@ EG4/LuxPower Inverter → WiFi Dongle (TCP :8000)
7884
│ /api/status │ port 80
7985
│ /api/history │
8086
└─────────────────┘
87+
88+
RS-485 BMS (optional) → lux-mon-rs485 daemon → same backends
8189
```
8290

8391
## Quick Start
@@ -183,30 +191,49 @@ The API server runs on port 80 and provides:
183191
| `GET /api/summary` | Compact key metrics for dashboards |
184192
| `GET /api/history?minutes=60&fields=soc,battery_voltage` | Time-series data |
185193
| `GET /api/health` | Health check |
194+
| `GET /api/energy` | Energy totals and hourly rollups |
195+
| `GET /api/forecast?hours=48` | Stored solar PV forecast (predicted + corrected watts) |
196+
| `GET /api/alerts` | Recent alert events |
197+
| `GET /api/alerts/live` | Current alert states (for HA binary sensors) |
186198
| `GET /api/settings` | All runtime settings |
199+
| `GET /api/settings/controllable` | Settings exposed as HA entities |
187200
| `GET /api/settings/{name}` | Single setting value |
188201
| `PUT /api/settings/{name}` | Update a setting (JSON body: `{"value": "..."}`) |
189202
| `GET /api/automation/registers` | List writable holding registers for automation actions |
203+
| `GET /api/automation/types` | List automation types (rule_table, battery_soc, battery_protection, notify) |
190204
| `GET /api/automation/rules` | List automation rules and global enable flag |
191205
| `POST /api/automation/rules` | Replace the full rule set (JSON array) |
206+
| `DELETE /api/automation/rules/{rule_id}` | Delete a single rule |
192207
| `POST /api/automation/enable` | Globally enable/disable the automation engine |
193208
| `POST /api/automation/test` | Dry-run evaluate rules against the latest snapshot |
194209
| `GET /api/automation/log` | Recent automation actions / dry-runs |
210+
| `GET /api/quick-charge/status` | Current quick-charge state and defaults |
211+
| `POST /api/quick-charge/start` | Start a timed quick charge (JSON: `{"amps": 85, "minutes": 30}`) |
212+
| `POST /api/quick-charge/stop` | Stop an active quick charge, restoring the prior value |
195213
| `POST /api/backup` | Create a backup archive |
196214
| `GET /api/backups` | List backup archives |
197215
| `POST /api/prune` | Prune old detail data |
198216
| `GET /api/storage` | Show DB table sizes and disk usage |
217+
| `WS /ws` | Live snapshot WebSocket stream |
199218

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

202221
### Automation rules
203222

204-
Rules are stored as JSON in the `automation_rules` setting and evaluated after every snapshot. A rule can set a holding register to a static value or choose a value from a sensor-range table (e.g., different charge amps for different battery-voltage bands). Each rule supports:
223+
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:
224+
225+
- `rule_table` — a multi-dimensional rule table (e.g. grid charge current as a function of time-of-day and battery voltage)
226+
- `battery_soc` — battery state-of-charge control (time + SOC thresholds → grid/battery output source)
227+
- `battery_protection` — "if SOC ≤ X%, shutdown output; restore to Y when recovered" (restore-on-exit)
228+
- `notify` — send an email/webhook notification when a condition is met
229+
230+
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:
205231

206232
- `time_window`: `{"start": "21:00", "end": "06:00"}` (wraps past midnight)
207233
- `conditions`: list of `{"sensor": "battery_voltage", "min": 0, "max": 54}` checks
208234
- `action`: `{"register": "ac_charge_battery_current", "value": 85}` or a `ranges` table
209235
- `dry_run`: when `true`, the rule logs what it *would* write but never sends a Modbus command
236+
- `restore`: a value written back when conditions stop matching (or the automation is disabled)
210237

211238
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:
212239

@@ -234,11 +261,37 @@ Enable the engine globally with the `automation_enabled` setting (or the dashboa
234261

235262
**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.
236263

237-
The API also serves a built-in web dashboard at `/` with real-time gauges, power flow, historic charts, battery details, energy totals, and runtime settings.
264+
### Quick Charge / Generator Charge
265+
266+
lux-mon implements the two one-shot inverter actions the EG4 Monitor portal exposes:
267+
268+
- **Quick Charge** — write `ac_charge_battery_current` (register 168) to a target current for a fixed number of minutes, then restore the prior value.
269+
- **Generator Charge** — write the generator charge current / enable path (register path stubbed, see roadmap).
270+
271+
State is persisted in the `lux_settings` table so it survives collector restarts. Writes reuse the automation engine's safe write helpers (fresh socket, echo verification, clamping). Defaults are configurable via the `quick_charge_amps` and `quick_charge_minutes` settings.
272+
273+
### Solar PV forecast
274+
275+
lux-mon can forecast PV production using a weather-based model (Option A) with optional historical calibration (Option B):
276+
277+
1. Fetch hourly weather (cloud cover + shortwave radiation) from **Open-Meteo** (free, no API key).
278+
2. Compute a clear-sky PV power curve using **pvlib** (sun position + clear-sky irradiance transposed onto the tilted array plane).
279+
3. Scale by a cloud factor, then apply bifacial back-side gain.
280+
4. Optionally correct today's forecast using the last N days of actual-vs-forecast error (bucketed by hour-of-day and cloud cover).
281+
5. Persist the predicted/corrected watts to MariaDB (`lux_solar_forecast` table).
238282

239-
A pre-built Grafana dashboard is included in `grafana/dashboards/lux-mon-charts.json` and provisioned automatically by `scripts/setup-grafana-stack.sh`. It uses a SolarAssistant-compatible InfluxDB schema so existing SolarAssistant dashboards import directly.
283+
The forecast is exposed at `GET /api/forecast` and overlaid on the dashboard's PV chart (corrected forecast shown as a yellow dashed line). All forecast parameters (location, array kWp/azimuth/tilt, bifacial gain, provider, horizon, refresh interval, calibration) are runtime settings editable from the dashboard ⚙️ tab.
240284

241-
For Home Assistant users, see [`docs/home-assistant-energy.md`](docs/home-assistant-energy.md).
285+
### Alerts
286+
287+
Alert thresholds are evaluated after every snapshot and published to MQTT (for HA binary sensors) and MariaDB (`lux_alerts` table). Supported alerts:
288+
289+
- Battery SOC low / critical
290+
- Battery temperature high
291+
- Inverter temperature high
292+
- Grid lost (configurable threshold in seconds)
293+
294+
Notifications are dispatched via authenticated SMTP relay and/or webhook, rate-limited to one per 5 minutes per alert. All thresholds and notification targets are runtime settings.
242295

243296
### Home Assistant integration
244297

@@ -306,9 +359,25 @@ All config can be set via env vars:
306359
| `LUX_API_HOST` | `0.0.0.0` | API bind address |
307360
| `LUX_API_PORT` | `80` | API port |
308361

362+
### RS-485 / BMS environment variables
363+
364+
The `lux-mon-rs485` daemon (see `collector/rs485_collector.py`) polls an RS-485/serial device and writes to the same backends:
365+
366+
| Variable | Default | Description |
367+
|----------|---------|-------------|
368+
| `LUX_RS485_ENABLED` | `false` | Enable the RS-485 collector |
369+
| `LUX_RS485_PORT` | `/dev/ttyUSB0` | Serial port |
370+
| `LUX_RS485_BAUD` | `115200` | Baud rate |
371+
| `LUX_RS485_DEVICE_TYPE` | — | `jk_bms` \| `modbus_rtu` \| `raw` \| `eg4_a5_bms` \| `eg4_bms` |
372+
| `LUX_RS485_POLL_INTERVAL` | `2.0` | Seconds between reads |
373+
| `LUX_RS485_SLAVE_ID` | `1` | Modbus slave ID |
374+
| `LUX_RS485_MODBUS_START` | `0` | Modbus register start |
375+
| `LUX_RS485_MODBUS_COUNT` | `40` | Modbus register count |
376+
| `LUX_RS485_PREFIX` | `rs485` | Measurement/topic prefix |
377+
309378
## Storage Backends
310379

311-
The collector supports two storage backends. Choose one by setting `LUX_STORAGE_TYPE`.
380+
The collector supports two storage backends, which can be enabled together. Choose by setting `LUX_STORAGE_TYPE` (or the individual `LUX_*_ENABLED` flags).
312381

313382
### MariaDB / MySQL (default)
314383

@@ -329,6 +398,10 @@ LUX_MARIADB_DATABASE=luxmon
329398
Tables are auto-created on first run:
330399
- `lux_snapshots` — one row per write interval with timestamp and raw register JSON
331400
- `lux_registers` — one row per decoded register value, indexed by timestamp and name
401+
- `lux_settings` — runtime settings (key/value)
402+
- `lux_alerts` — alert events
403+
- `lux_solar_forecast` — solar PV forecast time series
404+
- `lux_automation_log` — automation actions / dry-runs
332405

333406
Set `LUX_MARIADB_TABLE_PREFIX` to change the table prefix from `lux_` if needed.
334407

@@ -348,6 +421,8 @@ LUX_INFLUX_ORG=your-org
348421
LUX_INFLUX_BUCKET=solar
349422
```
350423

424+
The InfluxDB schema is **SolarAssistant-compatible** (one measurement per metric, `inverter_0`/`combined` fields), so existing SolarAssistant Grafana dashboards import directly.
425+
351426
### Adding a New Backend
352427

353428
The collector uses a pluggable writer pattern. To add support for PostgreSQL, SQLite, or another database:
@@ -360,7 +435,7 @@ See `_create_mariadb_writer()` and `_write_mariadb()` for the pattern to follow.
360435

361436
## Runtime Settings
362437

363-
Settings are stored in the `lux_settings` MariaDB table (auto-created) and read live by the API and dashboard — no config files, no restarts. Change any value with a single API call and the dashboard picks it up on the next refresh.
438+
Settings are stored in the `lux_settings` MariaDB table (auto-created) and read live by the API and dashboard — no config files, no restarts. Change any value with a single API call and the dashboard picks it up on the next refresh. The collector detects config changes on each write cycle and re-applies live-safe settings in place (or exits for a Docker restart when a transport/model change requires it).
364439

365440
| Setting | Default | Description |
366441
|---------|---------|-------------|
@@ -373,6 +448,20 @@ Settings are stored in the `lux_settings` MariaDB table (auto-created) and read
373448
| `dashboard_refresh_sec` | `5` | Dashboard auto-refresh interval |
374449
| `chart_default_hours` | `6` | Default chart time range |
375450
| `write_interval_sec` | `5` | Seconds between MariaDB writes |
451+
| `timezone` | `America/Chicago` | Local timezone for scheduling |
452+
| `temperature_unit` | `celsius` | Temperature unit |
453+
| `quick_charge_amps` | — | Default quick-charge target current (A) |
454+
| `quick_charge_minutes` | — | Default quick-charge duration (min) |
455+
| `forecast_enabled` | `false` | Enable solar forecast |
456+
| `forecast_latitude` / `forecast_longitude` | site | Forecast location |
457+
| `array_kwp` / `array_azimuth` / `array_tilt` | — | Array geometry for forecast |
458+
| `array_bifacial_gain` | `0.10` | Bifacial back-side gain |
459+
| `forecast_provider` | `open-meteo` | Forecast data source |
460+
| `forecast_hours` | `48` | Forecast horizon |
461+
| `forecast_refresh_min` | `120` | Forecast refresh interval |
462+
| `forecast_bias_enabled` | `true` | Historical calibration |
463+
| `forecast_bias_lookback_days` | `7` | Calibration lookback |
464+
| `forecast_bias_min_samples` | `3` | Min samples per bucket |
376465

377466
```bash
378467
# Read all settings

0 commit comments

Comments
 (0)