- top-level README.md (new): system overview, subsystem map, skills pointer, notable findings. - eg4battery README/NOTES: 3 -> 6 packs (pack 6 oddball 0x01/115200); SoC drift + calibration section; closed-loop comms evaluated and rejected (loses per-pack telemetry, no native protocol, doesn't fix drift); how to force a grid charge via output-priority SUB. - LVX6048 README: closed-loop pending item -> resolved decision; new "SoC calibration & known firmware quirks" section (POP single-digit/POP01, MCHGC charge-lock, re_discharge=re-discharge can't exceed float, PIRI lag, powermon adhoc wedge); skills pointer. - lvx-control README: POP encoding fix, POP crc-but-applies quirk, verify-by- behavior, grid-charge-via-SUB usage. - troubleshoot-inverter skill: corrected the stale "dead string per inverter" claim — both strings healthy; low PV is tilt/heat/shade/curtailment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
238 lines
13 KiB
Markdown
238 lines
13 KiB
Markdown
# LVX6048 → Home Assistant
|
||
|
||
Reproducible install package for monitoring 2× MPP Solar LVX6048 inverters over
|
||
USB-HID (PI18) via [`powermon`](https://github.com/jblance/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)
|
||
|
||
```bash
|
||
# 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):
|
||
|
||
```bash
|
||
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.zip` includes a `dsp.hex` slave 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_number` decoder.** 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-check` updated 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.yaml` captures 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-control` daemon 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 in `homeassistant/`.
|
||
|
||
**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.py` for 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 — evaluated 2026-06, NOT pursuing.** Stays open-loop.
|
||
Closed-loop would need the LP4V2 to emulate Pylontech-CAN (no native EG4
|
||
support on the LVX BMS port) and would require the inter-pack daisy-chain that
|
||
silences slave packs' RS485 ports — i.e. trade away the per-pack/per-cell
|
||
telemetry that's our best diagnostic. And it wouldn't fix SoC accuracy: it just
|
||
forwards the BMS's own drifted SoC. The cure for drift is a periodic full charge
|
||
(see below), done open-loop. See `../eg4battery/NOTES.md` §"Closed-loop BMS comms".
|
||
|
||
## SoC calibration & known firmware quirks
|
||
|
||
**SoC drift fix.** The EG4 pack SoC counters drift because the conservative profile
|
||
rarely drives a true full charge. A periodic **full charge to absorption** re-anchors
|
||
every pack to 100%. Automated by the `calibration-charge` skill —
|
||
[`../.claude/skills/calibration-charge/`](../.claude/skills/calibration-charge/):
|
||
solar-only on a clear day, or grid-assisted via **output-priority SUB** on a cloudy
|
||
day (`../.claude/skills/lib/grid-cal-monitor`, which safety-monitors and auto-reverts).
|
||
|
||
**LVX6048 firmware quirks** (learned the hard way; baked into the tooling):
|
||
- **PI18 `POP` is single-digit.** `solar_battery_utility` encodes as `POP1`, NOT
|
||
`POP01` — the inverter *silently rejects* the malformed `POP01` (no error on the
|
||
result topic). Fixed in `lvx-control` + `lvx-flash/flash.py` `POP_MAP`. A `POP` set
|
||
returns "crc check fails" on the result topic but still applies; `PCP` returns "Succeeded".
|
||
- **`MCHGC`/`MUCHGC` are locked while charging** — a charge-current change NAKs whenever
|
||
`mppt1_charger_status=charging` (even though `device_mode` reads `Battery`). Set them
|
||
only in a pre-charge idle window.
|
||
- **`stop_charge_voltage` is really `battery_re_discharge_voltage`** and can't exceed
|
||
`float`; firmware NAKs `0`/"Full". Don't use it to force a grid charge — use POP=SUB.
|
||
- **PIRI readback lags ~5 min**, so verify a setter by *behavior*
|
||
(`line_power_direction`, `device_mode`), not the readback entity.
|
||
- **powermon's adhoc queue can wedge** (commands stop landing on the result topic);
|
||
`sudo systemctl restart powermon.service powermon2.service` clears it.
|
||
|
||
## Monitoring & troubleshooting skills
|
||
|
||
Agent-runnable skills for this install live in
|
||
[`../.claude/skills/`](../.claude/skills/) (see `REFERENCE.md` there for the system
|
||
map): `solar-health-check`, `troubleshoot-inverter`, `troubleshoot-battery`,
|
||
`power-usage`, and `calibration-charge`, plus helpers `lib/solar-snapshot` (live MQTT)
|
||
and `lib/ha-history` (HA recorder lookback).
|