Local monitoring for LuxPower-based inverters — no cloud required.
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).
- Passively listens to your inverter's WiFi dongle — zero bus contention
- Actively polls as a fallback for non-broadcasting dongles
- Stores time-series data in MariaDB/MySQL (InfluxDB optional, both can run together)
- Exposes a REST API for scripting, morning briefings, Home Assistant, etc.
- Streams live snapshots over a WebSocket (
/ws) so the web dashboard updates instantly - Writes inverter settings via a safe holding-register write path with clamping and verification
- SolarAssistant-style automations — rule table, battery SOC control, battery protection, and notifications, with global dry-run for safety (38/57 settings mapped)
- Quick Charge / Generator Charge — one-shot timed grid charge with automatic restore
- Solar PV forecast — weather-based (Open-Meteo) with historical calibration, persisted to MariaDB
- Alerts — SOC/temperature/grid-loss thresholds with SMTP + webhook notifications
- RS-485 BMS monitoring — EG4 A5/5A, JK BMS, and generic Modbus RTU drivers
- Home Assistant — native REST integration, MQTT auto-discovery, and energy-dashboard sensors
- Runs anywhere — your Mac, a Raspberry Pi, a Docker container
- Dashboard-ready — built-in web UI plus Grafana dashboards
This project is actively running on private hardware monitoring an EG4 6000XP inverter.
| Component | Status | Details |
|---|---|---|
| Collector | ✅ Live | 111 input registers decoded, writes to MariaDB + InfluxDB + MQTT |
| REST API | ✅ Live | FastAPI on port 80, systemd-managed |
| Storage | ✅ Live | MariaDB, InfluxDB v2, hourly energy rollups |
| Dashboard | ✅ Live | Web UI with gauges, charts, battery, totals, settings, automations, schedule editor |
| Active polling | ✅ Built | Fallback for non-broadcasting dongles |
| Runtime settings | ✅ Live | DB-backed, editable from dashboard ⚙️ tab |
| 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 |
| RS-485 / BMS | ✅ Live | lux-mon-rs485 daemon, EG4 A5/5A battery BMS driver deployed |
| Alerts | ✅ Live | SMTP + webhook notifications, rate-limited, UI configurable |
| Solar forecast | ✅ Live | Open-Meteo weather forecast + historical calibration (v1.2.1) |
| Quick charge | ✅ Live | Timed grid charge with restore-on-expiry |
| Home Assistant | ✅ Integrated | Native REST integration + MQTT auto-discovery + energy sensors |
| Schedule editor | ✅ Fixed | Reads/writes actual inverter holding registers (Grid charge + AC first) |
| Automations | ✅ Live | SolarAssistant-style rule table, SOC control, battery protection, notifications (global dry-run; 38/57 settings mapped) |
| Backup/restore | ✅ Built | Nightly systemd timer + one-command restore |
| Grafana | ✅ Built | Pre-loaded dashboards and data source |
lux-mon speaks the Luxpower SNA and 18KPV register families. EG4 inverters are Luxpower rebadges, so the full EG4 lineup is supported via two families. Each model below maps to one of those two register maps.
Verified vs. unverified. A model is ✅ Verified only when its register map has been confirmed against a live unit (real captures, not just the vendor's protocol document).
⚠️ Unverified models share a register map with a verified sibling but have not been independently confirmed — they may expose extra data fields or decode some registers differently. Treat unverified models as best-effort and report any discrepancies.
| Model | Setting value | Register family | Status |
|---|---|---|---|
| EG4 6000XP | eg4_6000xp |
SNA | ✅ Verified (reference) |
| Luxpower SNA | luxpower_sna |
SNA | ✅ Verified |
| EG4 12000XP | eg4_12000xp |
SNA | |
| EG4 6500EX-48 | eg4_6500ex |
SNA | |
| EG4 3000EHV-48 | eg4_3000ehv |
SNA | |
| LuxPower LXP 6K | lxp_6k |
SNA | |
| BigBattery SNA-US 6K | bigbattery_sna_6k |
SNA | |
| EG4 18KPV | eg4_18kpv |
18KPV | |
| EG4 12kPV | eg4_12kpv |
18KPV | |
| LuxPower LXP 12K | lxp_12k |
18KPV | |
| LuxPower LXP 18K | lxp_18k |
18KPV | |
| Fortress Envy True 12K | fortress_envy_12k |
18KPV |
- SNA family is the validated reference — the 6000XP map was reverse-engineered from live captures. The 12000XP, 6500EX-48, and 3000EHV-48 (legacy, discontinued) share the exact same register layout.
- 18KPV family is derived from the official EG4 Modbus protocol document and has not been validated against a live unit. The 12kPV, LXP 12K/18K, and Fortress Envy True 12K share the 18KPV map.
- FlexBOSS18/21 and GridBOSS are a new platform (not a Luxpower SNA rebadge) with an unknown register map. They are intentionally not aliased to either family — doing so would silently decode garbage. A dedicated driver is needed once their protocol is documented.
Running an unverified model? We'd love to confirm it against your hardware.
If your inverter is on the list above but marked
- Set
LUX_INVERTER_MODELto your model's setting value and run lux-mon. - Compare the decoded values against your inverter's own display / the vendor's monitoring app.
- If anything looks off (wrong values, missing fields, extra registers), open a GitHub issue or email us at jaspertan219@gmail.com with a redacted log excerpt so we can validate and correct the register map.
Confirmed-good reports let us flip a model from
- Inverter Edit Mode page — manual Read/Set for every editable EG4 6000XP holding register, mirroring the EG4 Monitor Maintenance tab.
- Holding register map corrections — align
collector/protocol.pywith theLXP_REGISTERS.txtreference, add missing registers, fix ranges, and correct AC charge current to register 168.
- Generator and AC-coupled charge support (generator-charge register path is stubbed)
- A general automation/rule engine (condition → setting write) — see
docs/solarassistant-automation-map.mdfor the SolarAssistant reference field map - More inverter models via pluggable Modbus RTU drivers (FlexBOSS/GridBOSS need a dedicated register map)
- Forecast.Solar provider (listed in settings, not yet wired)
EG4/LuxPower Inverter → WiFi Dongle (TCP :8000)
│
▼ (Modbus TCP / passive listen)
┌─────────────────┐
│ lux-collector │ Python
│ (protocol │
│ parser) │
└────────┬────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ MariaDB │ │ InfluxDB │ │ MQTT │
│ │ │ (Solar │ │ (Home │
│ │ │ Assistant│ │ Assistant│
│ │ │ schema) │ │ discover)│
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└─────────────┴─────────────┘
│
▼
┌─────────────────┐
│ REST API │ FastAPI
│ /api/status │ port 80
│ /api/history │
└─────────────────┘
RS-485 BMS (optional) → lux-mon-rs485 daemon → same backends
The installer sets up everything: MariaDB, Python venv, InfluxDB, Mosquitto, Grafana, and systemd services for the collector and API.
git clone https://github.com/jmewing/lux-mon.git
cd lux-mon
LUX_INSTALL_DIR=/opt/lux-mon \
LUX_USER=$(whoami) \
LUX_MARIADB_PASSWORD='luxmon' \
LUX_INFLUX_ADMIN_PASSWORD='choose-a-password' \
LUX_DONGLE_HOST=192.168.1.100 \
bash scripts/install.shAfter install:
- API: http://YOUR-HOST:80/api/status
- Grafana: http://YOUR-HOST:3000/grafana/d/lux-mon-charts/lux-mon-charts
- Add an Apache/Nginx reverse proxy on port 80 if desired.
If you prefer containers, copy docker/.env.example to .env, fill in your
dongle IP and passwords, then run:
cp docker/.env.example .env
# edit .env
docker compose -f docker/docker-compose.yml up -d --buildThis builds the lux-mon image from source, then starts MariaDB, InfluxDB,
Mosquitto, collector, API, and Grafana with pre-loaded dashboards. To use a
pre-built image instead, set LUX_IMAGE in .env and omit --build.
Pre-built multi-arch images (amd64 + arm64) are published to:
- Stable (v1.0.x):
- Docker Hub:
jmewing/lux-mon:v1.0.1 - GitHub Container Registry:
ghcr.io/jmewing/lux-mon:v1.0.1
- Docker Hub:
- Development / beta (v1.1.0 inverter-write preview):
- Docker Hub:
jmewing/lux-mon:v1.1.0-beta.1 - GitHub Container Registry:
ghcr.io/jmewing/lux-mon:v1.1.0-beta.1
- Docker Hub:
latest always points to the most recent stable release.
Example .env for the stable image:
LUX_IMAGE=jmewing/lux-mon:v1.0.1Example .env for the beta image:
LUX_IMAGE=jmewing/lux-mon:v1.1.0-beta.1Then run:
docker compose -f docker/docker-compose.yml up -dSee docker/README.md for details.
# Clone
git clone https://github.com/jmewing/lux-mon.git
cd lux-mon
# Install Python deps
pip install -r docker/requirements.txt
# Configure via environment (copy example and edit)
cp .env.example .env
# edit .env with your DB credentials, dongle IP, InfluxDB/MQTT options
# Optional: install InfluxDB + Mosquitto + Grafana (Debian/Ubuntu)
LUX_INFLUX_ADMIN_PASSWORD='choose-a-password' bash scripts/setup-grafana-stack.sh
# Run the collector
python -m collectorFor a config-file approach you can also copy config.example.py to config.py and pass --config config.py.
The API server runs on port 80 and provides:
| Endpoint | Description |
|---|---|
GET /api/status |
Latest snapshot with all decoded registers |
GET /api/summary |
Compact key metrics for dashboards |
GET /api/history?minutes=60&fields=soc,battery_voltage |
Time-series data |
GET /api/health |
Health check |
GET /api/energy |
Energy totals and hourly rollups |
GET /api/forecast?hours=48 |
Stored solar PV forecast (predicted + corrected watts) |
GET /api/alerts |
Recent alert events |
GET /api/alerts/live |
Current alert states (for HA binary sensors) |
GET /api/settings |
All runtime settings |
GET /api/settings/controllable |
Settings exposed as HA entities |
GET /api/settings/{name} |
Single setting value |
PUT /api/settings/{name} |
Update a setting (JSON body: {"value": "..."}) |
GET /api/holding |
Read actual holding-register values directly from the inverter |
GET /api/holding/{name} |
Read a single named holding register from the inverter |
PUT /api/holding/{name} |
Write a single named holding register to the inverter |
GET /api/automation/types |
List automation types (rule_table, battery_soc, battery_protection, notify) |
GET /api/automation/conditions |
List condition dimensions for automation rules |
GET /api/automation/settings |
List the 50 SolarAssistant-style writable settings with register mapping |
GET /api/automation/rules |
List automations + global enable + dry-run |
POST /api/automation/rules |
Replace the full automation configuration |
DELETE /api/automation/rules/{id} |
Delete one automation |
POST /api/automation/enable |
Globally enable/disable the automation engine |
POST /api/automation/dry-run |
Globally enable/disable dry-run mode |
POST /api/automation/rules/{id}/disable |
Temporarily disable an automation for N minutes |
POST /api/automation/test |
Dry-run evaluate automations against the latest snapshot |
GET /api/automation/log |
Recent automation actions / dry-runs |
GET /api/automation/registers |
List writable holding registers (used by the schedule editor) |
GET /api/quick-charge/status |
Current quick-charge state and defaults |
POST /api/quick-charge/start |
Start a timed quick charge (JSON: {"minutes": 60}) |
POST /api/quick-charge/stop |
Stop an active quick charge, restoring the prior value |
POST /api/backup |
Create a backup archive |
GET /api/backups |
List backup archives |
POST /api/prune |
Prune old detail data |
GET /api/storage |
Show DB table sizes and disk usage |
WS /ws |
Live snapshot WebSocket stream |
The built-in dashboard (/) includes a Power Management page with three tools:
- Automations — SolarAssistant-style condition → action rules with a global dry-run toggle.
- Timer Schedule — Grid charge + AC first time slots.
- Quick Charge — one-shot timed grid charge.
Click Add Automation to create a rule. Choose from four types:
- Rule table — pick a setting, add up to two conditions, set the action value, and optionally a restore value.
- Battery state of charge control — time-of-day + SOC thresholds to choose grid vs battery source.
- Battery protection — if SOC drops below a threshold, write a shutdown voltage; restore when SOC recovers.
- Send notification — when a condition is met, send an email/webhook via the existing alert notifier.
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.
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.
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.
The schedule editor writes directly to the inverter's holding registers via
PUT /api/holding/{name}. It covers two groups:
- Grid charge —
ac_charge_period_*(3 slots) plus charge power and SOC limit - AC first —
ac_first_period_*(3 slots)
Time slots are encoded as (minute << 8) | hour (minute in the high byte,
hour in the low byte), matching the LuxPower register format. Writes are sent
sequentially (not in parallel) because the dongle rejects bursts of
simultaneous write connections.
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. lux-mon never writes a setting just because it happens to be readable.
lux-mon implements the inverter's native quick-charge action using the correct registers reverse-engineered from SolarAssistant's traffic:
quick_charge_duration(register 234 /0x00EA) — the actual charge controller. Setting it to N minutes starts charging for N minutes;0stops.function_enable_5(register 233 /0x00E9) — a bitfield; bit 0 is the quick-charge start toggle (read-modify-write so other bits are preserved).
Correct semantics (confirmed via tcpdump of SolarAssistant):
- The duration register is the charge controller —
0means "charge 0 minutes" = no charge / stop. - Bit 0 of the enable bitfield only toggles the mode; it does NOT start
charging on its own. Enabling with duration
0does nothing. - Start: write duration first, then set bit 0 of the enable bitfield.
- Stop: clear bit 0 of the enable bitfield AND clear the duration.
This differs from the earlier implementation, which drove
ac_charge_battery_current (register 168) — that is the grid charge current,
not the quick-charge toggle.
The quick-charge control lives on the main dashboard (a button with a
▼-toggle for a custom duration, 1–240 minutes, default 60). State is persisted
in the lux_settings table so it survives collector restarts, and all actions
are logged. Writes use the shared holding-register write helpers (fresh socket,
echo verification, clamping).
lux-mon can forecast PV production using a weather-based model (Option A) with optional historical calibration (Option B):
- Fetch hourly weather (cloud cover + shortwave radiation) from Open-Meteo (free, no API key).
- Compute a clear-sky PV power curve using pvlib (sun position + clear-sky irradiance transposed onto the tilted array plane).
- Scale by a cloud factor, then apply bifacial back-side gain.
- Optionally correct today's forecast using the last N days of actual-vs-forecast error (bucketed by hour-of-day and cloud cover).
- Persist the predicted/corrected watts to MariaDB (
lux_solar_forecasttable).
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.
Alert thresholds are evaluated after every snapshot and published to MQTT (for HA binary sensors) and MariaDB (lux_alerts table). Supported alerts:
- Battery SOC low / critical
- Battery temperature high
- Inverter temperature high
- Grid lost (configurable threshold in seconds)
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.
lux-mon has a native Home Assistant integration that connects to the REST API
and exposes live sensors, energy-dashboard sensors, controllable settings
(number/select/switch entities), alerts, and quick-charge buttons.
- Integration repo: jmewing/ha_luxmon
- Add-on repo (HAOS / Supervised): jmewing/ha_luxmon_addons
Install via HACS, the add-on, or manually — see the integration README for full instructions.
# Start the API server
python -m api
# Or install the systemd service (Linux)
sudo cp api/lux-api.service /etc/systemd/system/
sudo systemctl enable --now lux-api.serviceTo serve the dashboard on port 80, configure Apache as a reverse proxy:
# Enable proxy modules
sudo a2enmod proxy proxy_http
# Add to your default virtual host (/etc/apache2/sites-available/000-default.conf):
#
# <VirtualHost *:80>
# ProxyPreserveHost On
# ProxyPass / http://127.0.0.1:80/
# ProxyPassReverse / http://127.0.0.1:80/
# </VirtualHost>
sudo systemctl reload apache2Now the dashboard is available at http://<your-server>/ and the API at http://<your-server>/api/status, etc.
All config can be set via env vars:
| Variable | Default | Description |
|---|---|---|
LUX_DONGLE_HOST |
192.168.1.100 |
Inverter dongle IP |
LUX_DONGLE_PORT |
8000 |
Dongle TCP port |
LUX_WRITE_INTERVAL |
5 |
Seconds between DB writes |
LUX_STORAGE_TYPE |
mariadb |
mariadb or influxdb |
LUX_MARIADB_HOST |
localhost |
MariaDB host |
LUX_MARIADB_PORT |
3306 |
MariaDB port |
LUX_MARIADB_USER |
luxmon |
MariaDB user |
LUX_MARIADB_PASSWORD |
luxmon |
MariaDB password |
LUX_MARIADB_DATABASE |
luxmon |
MariaDB database |
LUX_INFLUX_URL |
http://localhost:8086 |
InfluxDB URL (optional) |
LUX_INFLUX_TOKEN |
lux-mon-token |
InfluxDB token (optional) |
LUX_INFLUX_ORG |
luxmon |
InfluxDB org (optional) |
LUX_INFLUX_BUCKET |
solar |
InfluxDB bucket (optional) |
LUX_REPLAY_FILE |
— | Replay a capture instead of live TCP |
LUX_INVERTER_MODEL |
eg4_6000xp |
Inverter / BMS model driver |
LUX_API_HOST |
0.0.0.0 |
API bind address |
LUX_API_PORT |
80 |
API port |
The lux-mon-rs485 daemon (see collector/rs485_collector.py) polls an RS-485/serial device and writes to the same backends:
| Variable | Default | Description |
|---|---|---|
LUX_RS485_ENABLED |
false |
Enable the RS-485 collector |
LUX_RS485_PORT |
/dev/ttyUSB0 |
Serial port |
LUX_RS485_BAUD |
115200 |
Baud rate |
LUX_RS485_DEVICE_TYPE |
— | jk_bms | modbus_rtu | raw | eg4_a5_bms | eg4_bms |
LUX_RS485_POLL_INTERVAL |
2.0 |
Seconds between reads |
LUX_RS485_SLAVE_ID |
1 |
Modbus slave ID |
LUX_RS485_MODBUS_START |
0 |
Modbus register start |
LUX_RS485_MODBUS_COUNT |
40 |
Modbus register count |
LUX_RS485_PREFIX |
rs485 |
Measurement/topic prefix |
The collector supports two storage backends, which can be enabled together. Choose by setting LUX_STORAGE_TYPE (or the individual LUX_*_ENABLED flags).
Best for: most users. Zero additional infrastructure if you already run MySQL/MariaDB. The REST API and dashboard read directly from MariaDB.
# Create the database and user
sudo mysql -e "CREATE DATABASE luxmon; CREATE USER 'luxmon'@'localhost' IDENTIFIED BY 'your-password'; GRANT ALL ON luxmon.* TO 'luxmon'@'localhost';"
# Configure
LUX_STORAGE_TYPE=mariadb
LUX_MARIADB_HOST=localhost
LUX_MARIADB_USER=luxmon
LUX_MARIADB_PASSWORD=your-password
LUX_MARIADB_DATABASE=luxmonTables are auto-created on first run:
lux_snapshots— one row per write interval with timestamp and raw register JSONlux_registers— one row per decoded register value, indexed by timestamp and namelux_settings— runtime settings (key/value)lux_alerts— alert eventslux_solar_forecast— solar PV forecast time serieslux_automation_log— quick-charge action log
Set LUX_MARIADB_TABLE_PREFIX to change the table prefix from lux_ if needed.
Best for: users already running InfluxDB, or who want Grafana's native InfluxDB data source. The REST API does not read from InfluxDB — you'd use Grafana or InfluxDB's built-in UI for visualization.
# Install the Python client
pip install influxdb-client
# Configure
LUX_STORAGE_TYPE=influxdb
LUX_INFLUX_URL=http://localhost:8086
LUX_INFLUX_TOKEN=your-token
LUX_INFLUX_ORG=your-org
LUX_INFLUX_BUCKET=solarThe InfluxDB schema is SolarAssistant-compatible (one measurement per metric, inverter_0/combined fields), so existing SolarAssistant Grafana dashboards import directly.
The collector uses a pluggable writer pattern. To add support for PostgreSQL, SQLite, or another database:
- Add a new
storage_typevalue (e.g."postgres") - Implement
_create_postgres_writer()and_write_postgres()incollector/collector.py - Add the corresponding
LUX_POSTGRES_*env vars toconfig_from_env()
See _create_mariadb_writer() and _write_mariadb() for the pattern to follow.
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).
| Setting | Default | Description |
|---|---|---|
pv_max_power |
8000 |
Max PV input power (W) — sets gauge ceiling |
battery_capacity |
200 |
Battery capacity (Ah) — sets battery gauge ceiling |
grid_max_power |
6000 |
Max grid pass-through (W) |
eps_max_power |
6000 |
Max EPS output (W) |
charge_max_power |
5000 |
Max charge power (W) |
discharge_max_power |
5000 |
Max discharge power (W) |
dashboard_refresh_sec |
5 |
Dashboard auto-refresh interval |
chart_default_hours |
6 |
Default chart time range |
write_interval_sec |
5 |
Seconds between MariaDB writes |
timezone |
America/Chicago |
Local timezone for scheduling |
temperature_unit |
celsius |
Temperature unit |
quick_charge_minutes |
60 |
Default quick-charge duration (min, 1–240) |
forecast_enabled |
false |
Enable solar forecast |
forecast_latitude / forecast_longitude |
site | Forecast location |
array_kwp / array_azimuth / array_tilt |
— | Array geometry for forecast |
array_bifacial_gain |
0.10 |
Bifacial back-side gain |
forecast_provider |
open-meteo |
Forecast data source |
forecast_hours |
48 |
Forecast horizon |
forecast_refresh_min |
120 |
Forecast refresh interval |
forecast_bias_enabled |
true |
Historical calibration |
forecast_bias_lookback_days |
7 |
Calibration lookback |
forecast_bias_min_samples |
3 |
Min samples per bucket |
# Read all settings
curl http://your-server/api/settings
# Read one setting
curl http://your-server/api/settings/pv_max_power
# Update a setting
curl -X PUT http://your-server/api/settings/pv_max_power \
-H 'Content-Type: application/json' \
-d '{"value": "10000"}'Settings can also be edited directly in MariaDB:
INSERT INTO lux_settings (name, value) VALUES ('pv_max_power', '10000')
ON DUPLICATE KEY UPDATE value = '10000';scripts/backup.sh creates a single compressed archive containing everything
needed to rebuild your lux-mon system on new hardware:
- MariaDB dump (
luxmondatabase) - InfluxDB v2 bucket backup
- Grafana provisioning files and dashboard JSON
.envconfiguration- Runtime settings from
/api/settings
Run manually:
# Must run as root to read Grafana provisioning files
sudo bash scripts/backup.shConfigure via .env:
LUX_BACKUP_DIR=/var/backups/lux-mon
LUX_BACKUP_KEEP_DAYS=30
# Optional off-device copy:
LUX_BACKUP_REMOTE=user@nas:/backups/lux-monThe included systemd timer runs the backup automatically every night at 02:00:
sudo cp scripts/lux-mon-backup.service /etc/systemd/system/
sudo cp scripts/lux-mon-backup.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now lux-mon-backup.timerRestore from an archive on a fresh install:
sudo LUX_BACKUP=/var/backups/lux-mon/luxmon-backup-YYYYMMDD-HHMMSS.tar.gz bash scripts/restore.shscripts/prune.sh deletes detail data older than 90 days while keeping hourly
energy rollups for one year, keeping the MariaDB database small.
lux-mon also accepts setting changes over MQTT. The collector subscribes to:
luxmon/luxmon_solar/set/<setting>
and writes valid values to MariaDB immediately. Example:
mosquitto_pub -h 192.168.1.100 -t luxmon/luxmon_solar/set/alerts_soc_low -m 25Acknowledgments and errors are published on:
luxmon/luxmon_solar/ack
luxmon/luxmon_solar/error
When Home Assistant discovery is enabled, controllable settings appear as
number entities under homeassistant/number/luxmon_*.
A built-in backup script dumps MariaDB, .env, and settings to a timestamped
tarball in /var/backups/lux-mon:
bash scripts/backup.shPrune old detail data while keeping hourly energy rollups:
bash scripts/prune.shBoth are also exposed through the REST API:
# Create a backup
curl -X POST http://your-server/api/backup
# List backups
curl http://your-server/api/backups
# Prune old detail data
curl -X POST http://your-server/api/prune
# Show DB table sizes and disk usage
curl http://your-server/api/storagelux-mon is under active development. To pull the latest changes:
cd lux-mon
git pull origin main
pip install -r docker/requirements.txt # if dependencies changed
sudo systemctl restart lux-mon.service lux-api.serviceOne-liner for cron/nightly updates:
cd ~/src/lux-mon && git pull origin main && \
venv/bin/pip install -q -r docker/requirements.txt && \
sudo systemctl restart lux-mon.service lux-api.serviceTo update automatically every night at 3am:
# Add to crontab (crontab -e)
0 3 * * * cd ~/src/lux-mon && git pull origin main && venv/bin/pip install -q -r docker/requirements.txt && sudo systemctl restart lux-mon.service lux-api.serviceNote: Your
.envfile is gitignored and will never be overwritten. Settings stored in the database (lux_settingstable) are also preserved across updates.
lux-mon uses semantic versioning (MAJOR.MINOR.PATCH) with automated
release tagging. The version lives in a single place — api/__init__.py — and
is bumped automatically by a GitHub Actions workflow on every push to main.
| Bump | When | Commit convention |
|---|---|---|
MAJOR (1.x.x) |
New inverter support | feat(inverter): add support for <model> |
MINOR (x.1.x) |
New feature | feat: <description> (or feat(scope): ...) |
PATCH (x.x.1) |
Bug fix / maintenance | fix:, chore:, docs:, refactor:, perf:, test:, ci: |
A BREAKING CHANGE footer or a ! after the type (e.g. feat!: ...) also
forces a MAJOR bump.
- On push to
main, the workflow inspects commit subjects since the last tag. - It picks the highest-priority bump (major > minor > patch).
- It updates
api/__init__.py, commits the bump, tagsvX.Y.Z, and creates a GitHub release with an auto-generated changelog.
# Preview the next version (dry run)
scripts/bump-version.sh
# Force a specific bump and write it back to api/__init__.py
scripts/bump-version.sh --bump minor --write
scripts/bump-version.sh --bump patch --write
scripts/bump-version.sh --bump major --writeNote: The base version is the higher of the file version and the last git tag, which reconciles the historical drift where tags were created without bumping the file.
Test parsing/storage without a live inverter:
python -m collector --replay <your-capture-file> --interval 5The LuxPower WiFi dongle broadcasts inverter data over TCP port 8000 using a proprietary framing protocol (not standard Modbus TCP). The protocol has been reverse-engineered — see docs/reference/lux-protocol/PROTOCOL.md for the full spec.
Key facts:
- No polling needed — the dongle pushes data every ~2 seconds when it has an active TCP client
- Single TCP client limit — the dongle accepts only one TCP connection at a time; additional connections are closed immediately. Disconnect any other client (SolarAssistant, the vendor app, another collector instance) before starting this collector.
- 6 packets per cycle: 3 input register batches + 3 holding register batches
- 40 registers per batch = 240 registers total per cycle
This project builds on the excellent reverse-engineering work of:
- jefflaplante/lux — Protocol specification
- celsworth/lxp-bridge — Original Rust bridge (MQTT/InfluxDB/Postgres)
- jaredmauch/eg4-bridge — Maintained EG4 fork
- larduino/EG4-6000XP-Home-Assistant-Local-Control — EG4 6000XP register map
Copies of these are mirrored in docs/reference/ for preservation. The authoritative reverse-engineered Modbus address map for the LXP/EG4 inverter family is docs/reference/lxp-bridge/doc/LXP_REGISTERS.txt.
MIT