LVX6048 → Home Assistant
Reproducible install package for monitoring 2× MPP Solar LVX6048 inverters over
USB-HID (PI18) via powermon, publishing
to a Home Assistant MQTT broker with HA auto-discovery.
What's in the box
LVX6048/
├── README.md ← start here
├── Install.md ← detailed walkthrough (what install.sh does)
├── Monitoring.md ← background / design notes
├── install.sh ← one-shot installer (idempotent, safe to re-run)
│
├── etc/ mirror of target system paths
│ ├── udev/rules.d/99-lvx6048.rules
│ └── systemd/system/
│ ├── lvx-resolve-links.service
│ ├── lvx-control.service
│ ├── powermon.service
│ ├── powermon2.service
│ ├── powermon.service.d/10-resolver.conf
│ └── powermon2.service.d/10-resolver.conf
│
├── config/powermon/ lands at ~/.config/powermon/ (mode 600)
│ ├── powermon.yaml ← unit #1 — edit MQTT creds before deploying
│ └── powermon2.yaml ← unit #2 — edit MQTT creds before deploying
│
├── bin/
│ └── lvx-resolve-links ← installed as /usr/local/sbin/lvx-resolve-links
│ (install.sh rewrites the shebang to the uv-installed python)
│
├── powermon-patches/ ← drop-in files for the uv tool install
│ ├── README.md ← what each patch does + upgrade path
│ ├── pi18.py, usbport.py, mqttbroker.py,
│ └── port_config_model.py, ports_init.py
│
├── lvx-flash/ ← settings-profile CLI
│ ├── flash.py ← dump / diff / apply / compare / sync-check
│ ├── README.md
│ └── profiles/
│ ├── current.yaml ← legacy snapshot
│ └── eg4-lp4-v2.yaml ← canonical EG4 LP4 v2 LiFePO4 bank profile
│
├── lvx-control/ ← HA → powermon adhoc command bridge
│ ├── lvx-control ← single-file Python daemon
│ └── README.md ← supported actions / topic layout
│
├── homeassistant/ ← HA-side control entities + dashboard
│ ├── mqtt_controls.yaml ← selects + numbers for the controls
│ ├── lovelace_controls.yaml ← dashboard view
│ └── README.md
│
└── smoketest/ ← adhoc test configs (one-off powermon -C usage)
├── console.yaml
└── smoketest.yaml
Quick start (reproducing on a fresh machine)
# 1. Clone / scp this folder into place (e.g. /home/<user>/solar/LVX6048)
cd ~/solar/LVX6048
# 2. Install uv if not already: https://docs.astral.sh/uv/
# 3. Run the installer
./install.sh
# 4. Edit the two things install.sh warns about:
# a. ~/.config/powermon/powermon{,2}.yaml — MQTT broker IP / user / password
# b. /usr/local/sbin/lvx-resolve-links — SERIAL_UNIT_1 / SERIAL_UNIT_2 (see below)
# lvx-flash/flash.py — same two constants
# 5. Capture each inverter's PI18 serial (with services stopped):
sudo systemctl stop lvx-resolve-links.service powermon.service powermon2.service
for d in /dev/hidraw0 /dev/hidraw1; do
TMP=$(mktemp --suffix=.yaml)
printf 'loop: once\ndevice:\n name: probe\n port: {type: usb, path: %s, protocol: PI18}\ncommands:\n - {command: ID, trigger: {loops: 1}}\n' "$d" > "$TMP"
echo "=== $d ==="
~/.local/bin/powermon -C "$TMP" 2>&1 | grep serial_number
rm -f "$TMP"; sleep 2
done
# Edit SERIAL_UNIT_{1,2} in /usr/local/sbin/lvx-resolve-links and lvx-flash/flash.py,
# then:
sudo systemctl start lvx-resolve-links.service powermon.service powermon2.service
How the pieces fit together
┌─────────────────────────┐ ┌─────────────────────────┐
│ LVX6048 #1 (USB-HID) │ ──▶ │ /dev/hidraw{0|1} │
│ LVX6048 #2 (USB-HID) │ ──▶ │ (vid:pid 0665:5161) │
└─────────────────────────┘ └──────────┬──────────────┘
│
│ (99-lvx6048.rules → group=dialout)
│
▼
┌──────────────────────────────────┐
│ lvx-resolve-links.service │
│ (oneshot, runs before powermon) │
│ │
│ probes each hidraw w/ PI18 ID │
│ creates /dev/lvx6048-{1,2} │
│ symlinks keyed to serial │
└──────────┬───────────────────────┘
│
│ After= / Requires=
▼
┌──────────────────────────────────┐
│ powermon.service (unit #1) │
│ powermon2.service (unit #2) │
│ │
│ poll GS / MOD / PIRI / ET │
│ publish to HA auto-discovery │
│ topics under homeassistant/* │
└──────────┬───────────────────────┘
│ MQTT (port 1883)
▼
┌──────────────────────────────────┐
│ Home Assistant Mosquitto broker │
│ ~23 auto-discovered sensors/unit │
└──────────────────────────────────┘
Separate from the monitoring pipeline, lvx-flash/ is a manual settings
tool: dump the inverter's current config into a YAML profile, diff against a
target profile, apply changes safely (stops powermon, writes via PI18 setters,
verifies via PIRI readback). Also supports compare (diff live settings
between two inverters) and sync-check (verify parallel-stack health).
Cable moves and inverter power-cycles
Identification is PI18-serial-based, so moving USB cables between hub ports
never requires config edits. The udev rule fires lvx-resolve-links and
restarts both powermon services automatically whenever a matching hidraw
device appears (ACTION=="add"), so a cable shuffle or inverter power-cycle
recovers on its own — no manual systemctl restart needed in the common case.
If recovery looks stuck (e.g. a unit was off long enough that its hidraw disappeared but the new one didn't trigger the rule for some reason):
sudo systemctl restart lvx-resolve-links.service powermon.service powermon2.service
Replacing an inverter
When a unit is swapped, capture its new PI18 serial (see step 5 of Quick start)
and update the two SERIAL_UNIT_* constants in:
/usr/local/sbin/lvx-resolve-links~/solar/LVX6048/lvx-flash/flash.py
then restart the three services.
Status — done / pending
Done (2026-04-26):
- Parallel commissioning. Both inverters operationally paralleled and
load-sharing on a 3× EG4 LP4 v2 bank. See
2026-04-26-parallel.md. - Main-CPU firmware parity. Both units now at main 06306. Slave-CPU
firmware still differs (06126 / 06021) but is benign: cluster handshake
completes, current sharing is symmetric, no faults. The vendor's
LVX6048 FW63.06.zipincludes adsp.hexslave bin if version parity is ever desired for cosmetic reasons. - Hot-plug recovery. udev rule auto-restarts the resolver + powermon services when an inverter re-enumerates. Resolver also has a retry loop to handle the brief race when both units come up nearly together.
parallel_instance_numberdecoder. Replaced upstream's misleading 2-value flag with a proper instance-index decoder (0 = master, 1+ = slaves) in both GS and PGS.flash.py sync-checkupdated to match.- MOD code 06. Added "Charge" mapping (was crashing sync-check). Educated guess on the label — revisit if it shows up in unrelated states.
- Battery profile.
lvx-flash/profiles/eg4-lp4-v2.yamlcaptures the conservative open-loop LFP policy in use (USER mode, 56.4 V bulk, 54.0 V float, 48.0 V cutoff, 60 A MCHGC). - HA remote control.
lvx-controldaemon bridges HA-friendly MQTT topics (solar/control/lvx6048/<action>) to powermon's adhoc-command queue, mirroring every change to both inverters in lock-step. Day-to-day controls (POP / PCP / PSP / MCHGC / MUCHGC) exposed; risky setters (battery thresholds, type, output mode, factory reset) deliberately not. Companion HA configs live inhomeassistant/.
Pending / nice-to-have:
- PGS field layout. The LVX6048-specific 30-field PGS response is only
partially named in
powermon-patches/pi18.py— fields 4, 13–22, 23–28, and 30 are still exposed as raw strings. Now that the cluster is actually paralleled, more of these fields produce meaningful values; worth a second pass to identify them by comparing PGS0 vs PGS1 captures from master and slave. - Control / set commands via HA. PI18 setters (POP, PCP, MCHGC, MUCHGC,
PF) are implemented in
lvx-flash/flash.pyfor offline use, but aren't exposed as HA button/select entities. Deferred until monitoring has been stable for at least a week. - Closed-loop BMS comms. Currently open-loop — inverters estimate SoC from voltage, batteries don't push real-time SoC / charge limits to the inverter. Closed-loop would give better SoC accuracy and dynamic CC tapering near full. Path is the dedicated CAN port on the master pack → inverter BMS port (separate cable from the inter-pack daisy-chain). Deferred.