2026-04-26 19:26:44 -04:00
|
|
|
|
# 2026-04-26 (afternoon) — LVX6048 parallel-mode commissioning
|
|
|
|
|
|
|
|
|
|
|
|
Got the two LVX6048 inverters operationally paralleled, sharing battery
|
|
|
|
|
|
charge from the EG4 LP4 v2 bank symmetrically. Several discoveries about
|
|
|
|
|
|
the firmware's parallel-state reporting along the way, plus a couple of
|
|
|
|
|
|
small patches.
|
|
|
|
|
|
|
|
|
|
|
|
## What happened
|
|
|
|
|
|
|
|
|
|
|
|
1. **Firmware parity (main CPU): achieved.** Pre-flash main versions
|
|
|
|
|
|
were 06303 (unit 1) and 06440 (unit 2). User flashed both to
|
|
|
|
|
|
**06306** using the MPP Solar `InfiniVMasterCPU_Reflash` tool. Active
|
|
|
|
|
|
fault 71 ("Parallel version different") cleared.
|
|
|
|
|
|
|
|
|
|
|
|
2. **Slave-CPU firmware: still skewed but benign.** Slaves remain
|
|
|
|
|
|
06126 / 06021. The vendor's `LVX6048 FW63.06.zip` *does* include a
|
|
|
|
|
|
`dsp.hex` slave bin, but the operational evidence (load-sharing,
|
|
|
|
|
|
matched battery V, no faults, master/slave handshake completing)
|
|
|
|
|
|
indicates the slave delta is not blocking parallel function on this
|
|
|
|
|
|
firmware family. **Not flashing the DSPs** — filed for the record only.
|
|
|
|
|
|
|
|
|
|
|
|
3. **Battery wiring + commissioning.** Both inverters tied to the same
|
|
|
|
|
|
3× EG4 LP4 v2 100 Ah bank (300 Ah at 51.2 V nominal). After fault 83
|
|
|
|
|
|
("Parallel battery voltage detect different") cleared on first
|
|
|
|
|
|
battery-connected boot, both inverters entered MOD `06` and began
|
|
|
|
|
|
sharing the charge load (~55 A each, ~110 A combined ≈ 0.37 C).
|
|
|
|
|
|
|
|
|
|
|
|
4. **MCHGC mismatch (100 A vs 60 A) → fixed.** Unit 1 was at 100 A —
|
|
|
|
|
|
above the PI18 `MCHGC` setter's 80 A ceiling, so it had to be set
|
|
|
|
|
|
via the LCD (Program 02). User dropped unit 1 to 60 A so both
|
|
|
|
|
|
match. Confirmed via `flash.py compare` (12/12 settings match).
|
|
|
|
|
|
|
|
|
|
|
|
5. **Patch (g) tweak — `outputformats/hass.py`** *(unrelated to parallel
|
|
|
|
|
|
work but lives in the same patch tree).* No change here today; the
|
|
|
|
|
|
morning's cleanup is in commit `f771ec2`.
|
|
|
|
|
|
|
|
|
|
|
|
6. **Patch (h) — `pi18.py` MOD decoder.** Inverter started reporting
|
|
|
|
|
|
MOD code `06` after batteries connected; the existing decoder only
|
|
|
|
|
|
knew 00..05 and crashed (`KeyError: '06'`). Added `"06": "Charge"`.
|
|
|
|
|
|
The "Charge" label is an educated guess based on observed behavior
|
|
|
|
|
|
(active charging, no AC out); revisit if it turns up in unrelated
|
|
|
|
|
|
states.
|
|
|
|
|
|
|
|
|
|
|
|
7. **`parallel_instance_number` decoder fixed.** The GS/PGS field is
|
|
|
|
|
|
the parallel-instance index per the PI18 spec (0 = master, 1+ =
|
|
|
|
|
|
slaves), not a 2-value flag — but powermon upstream (and our patch)
|
|
|
|
|
|
was decoding it as `["Not valid", "valid"]`, which made the master
|
|
|
|
|
|
appear as "Not valid" in HA. Replaced with an `OPTION` decoder
|
|
|
|
|
|
labeling each index ("instance 0 (master)", "instance 1", ...) in
|
|
|
|
|
|
both GS field 27 and PGS field 0. `flash.py sync-check` updated
|
|
|
|
|
|
accordingly: removed the false-positive "GS parallel_instance_number
|
|
|
|
|
|
= Not valid" issue, replaced with index-collision and no-master
|
|
|
|
|
|
checks.
|
|
|
|
|
|
|
|
|
|
|
|
8. **`lvx-resolve-links` now auto-runs on USB hot-plug.** Added an
|
|
|
|
|
|
`ACTION=="add"` udev rule for vendor 0665 / product 5161 that
|
|
|
|
|
|
`RUN+=`s `systemctl --no-block restart lvx-resolve-links.service
|
|
|
|
|
|
powermon.service powermon2.service`. Powermon's `Requires=` /
|
|
|
|
|
|
`After=` already serialize the chain. Added a retry loop to the
|
|
|
|
|
|
resolver script (poll up to ~10 s for both expected serials) so a
|
|
|
|
|
|
single hidraw appearing slightly before its sibling doesn't leave a
|
|
|
|
|
|
symlink missing. Tested via `udevadm trigger --action=add
|
|
|
|
|
|
--subsystem-match=hidraw`: all three services restarted in lock-step
|
|
|
|
|
|
and symlinks rewrote cleanly.
|
|
|
|
|
|
|
|
|
|
|
|
9. **Battery profile YAML for the bank.** Created
|
|
|
|
|
|
`lvx-flash/profiles/eg4-lp4-v2.yaml` capturing the conservative
|
|
|
|
|
|
off-grid LFP policy (USER mode, bulk 56.4 V / float 54.0 V, cutoff
|
|
|
|
|
|
48.0 V, stop-charge 54.0 V). Live state already matches — applying
|
|
|
|
|
|
would be a no-op. Kept as the canonical record of the chosen policy.
|
|
|
|
|
|
|
|
|
|
|
|
10. **Sanity dumps** — `2026-04-26-lvx6048_{1,2}-dump` capture per-unit
|
|
|
|
|
|
settings post-commissioning. Useful as a baseline for diff-on-suspect.
|
|
|
|
|
|
|
|
|
|
|
|
11. **Battery side: re-discovered that EG4 LP4 v2's inter-pack
|
|
|
|
|
|
daisy-chain silences slave packs.** User had wired the inter-pack
|
|
|
|
|
|
`Link-In/Link-Out` chain earlier in the day; it elects a master
|
|
|
|
|
|
and demotes slaves on their independent RS485 ports. Pulling the
|
|
|
|
|
|
chain restored all three packs to MQTT immediately (all reporting
|
|
|
|
|
|
54.62 V agreement). Daemon needed no change. Memorized for next
|
|
|
|
|
|
time.
|
|
|
|
|
|
|
|
|
|
|
|
## Net effect
|
|
|
|
|
|
|
|
|
|
|
|
| Metric | Before | After |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| Main-CPU firmware | 06303 / 06440 | **06306 / 06306** ✓ |
|
|
|
|
|
|
| Slave-CPU firmware | 06126 / 06021 | unchanged (benign) |
|
|
|
|
|
|
| Active fault 71 | yes | cleared |
|
|
|
|
|
|
| Active fault 83 | n/a (no battery) | cleared |
|
|
|
|
|
|
| MCHGC delta | 100 A vs 60 A | 60 A / 60 A ✓ |
|
|
|
|
|
|
| Settings parity | mismatch | 12/12 match |
|
|
|
|
|
|
| Charge-current sharing | n/a | 55 A / 57 A symmetric |
|
|
|
|
|
|
| Battery V agreement | n/a | 55.1 V / 55.1 V identical |
|
|
|
|
|
|
| `parallel_instance_number` | "Not valid" / "valid" (master mislabeled) | "instance 0 (master)" / "instance 1" |
|
|
|
|
|
|
| MOD 06 crash on sync-check | yes | fixed |
|
|
|
|
|
|
| Hot-plug symlink recovery | manual `systemctl restart` | automatic via udev `RUN+=` |
|
|
|
|
|
|
|
|
|
|
|
|
## Files touched
|
|
|
|
|
|
|
|
|
|
|
|
```
|
2026-04-27 06:50:04 -04:00
|
|
|
|
M LVX6048/README.md (status section rewrite, sensor count, hot-plug note)
|
|
|
|
|
|
M LVX6048/powermon-patches/pi18.py (MOD 06; GS instance-index decoder; PGS validity flag;
|
|
|
|
|
|
two PGS fields named: battery V + capacity)
|
|
|
|
|
|
M LVX6048/lvx-flash/flash.py (instance-index semantics; sync-check rule rewrite;
|
|
|
|
|
|
cutoff <= stop_discharge validator)
|
2026-04-26 19:26:44 -04:00
|
|
|
|
M LVX6048/bin/lvx-resolve-links (retry loop for hot-plug race)
|
|
|
|
|
|
M LVX6048/etc/udev/rules.d/99-lvx6048.rules (ACTION=="add" RUN+= cascade)
|
|
|
|
|
|
A LVX6048/2026-04-26-lvx6048_1-dump (post-commissioning baseline)
|
|
|
|
|
|
A LVX6048/2026-04-26-lvx6048_2-dump (post-commissioning baseline)
|
|
|
|
|
|
A LVX6048/lvx-flash/profiles/eg4-lp4-v2.yaml (canonical battery profile)
|
|
|
|
|
|
A LVX6048/2026-04-26-parallel.md (this file)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## How to verify
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
sudo systemctl stop powermon.service powermon2.service
|
|
|
|
|
|
sudo systemctl restart lvx-resolve-links.service # always run after inverter power-cycle
|
|
|
|
|
|
/home/noise/solar/LVX6048/lvx-flash/flash.py sync-check \
|
|
|
|
|
|
--device-a /dev/lvx6048-1 --device-b /dev/lvx6048-2
|
|
|
|
|
|
sudo systemctl start powermon.service powermon2.service
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Pass criteria post-commissioning:
|
|
|
|
|
|
- main fw 06306 on both
|
|
|
|
|
|
- `mode=06` (Charge) or `mode=03` (Battery) or `mode=05` (Hybrid) — i.e. healthy operating modes, no `04` (Fault)
|
|
|
|
|
|
- `fault=No fault` on both
|
|
|
|
|
|
- both `parallel_*` values present (one as master/0, one as slave/1)
|
|
|
|
|
|
- battery V agreement within ~0.2 V
|
|
|
|
|
|
- charge currents within ~5 A symmetric (when both are charging)
|
|
|
|
|
|
|
2026-04-27 06:50:04 -04:00
|
|
|
|
## Nice-to-have pass
|
|
|
|
|
|
|
|
|
|
|
|
12. **README "Status / pending" section rewritten.** Replaced the stale
|
|
|
|
|
|
"Next steps / not done" with current state — parallel commissioning
|
|
|
|
|
|
done, slave-fw delta intentional, hot-plug recovery automatic.
|
|
|
|
|
|
Also bumped the per-inverter sensor count from 29 → 23 in the
|
|
|
|
|
|
architecture diagram (post-MQTT-cleanup) and added a "cable moves
|
|
|
|
|
|
and inverter power-cycles" paragraph noting the udev auto-recovery.
|
|
|
|
|
|
|
|
|
|
|
|
13. **PGS partial decode (two confirmed fields).** Captured PGS0+PGS1
|
|
|
|
|
|
on both units while operating in parallel and cross-referenced
|
|
|
|
|
|
against simultaneous GS readings to identify two previously-raw
|
|
|
|
|
|
fields:
|
|
|
|
|
|
- field_14 (idx 13) → `Battery voltage (parallel view)` — V*10
|
|
|
|
|
|
- field_18 (idx 17) → `Battery capacity (parallel view)` — %
|
|
|
|
|
|
Several others have plausible candidates noted in the source
|
|
|
|
|
|
comment (charging current, mppt voltage) but couldn't be confirmed
|
|
|
|
|
|
without captures under load and during discharge. PGS isn't being
|
|
|
|
|
|
polled by powermon today, so naming these doesn't change HA — it
|
|
|
|
|
|
just makes future use of the command (or extending the pollers) cleaner.
|
|
|
|
|
|
|
|
|
|
|
|
14. **PGS field 0 reverted from the GS fix.** The `Parallel instance
|
|
|
|
|
|
number` field has different semantics in PGS than in GS — live
|
|
|
|
|
|
captures show PGS field 0 always returns "1" regardless of the
|
|
|
|
|
|
queried instance, so it's a "valid response" flag, not the index.
|
|
|
|
|
|
Kept the index labeling on GS (where it really is the unit's own
|
|
|
|
|
|
instance number) and reverted PGS to the simpler 2-value flag.
|
|
|
|
|
|
|
|
|
|
|
|
15. **`flash.py apply` round-trip verified.** Ran `flash.py diff` /
|
|
|
|
|
|
`apply --confirm` against the live state with the
|
|
|
|
|
|
`eg4-lp4-v2.yaml` profile — diff reports no changes, apply
|
|
|
|
|
|
correctly says "nothing to do", `compare` afterward shows 12/12
|
|
|
|
|
|
settings still identical. Validates the apply code path end-to-end
|
|
|
|
|
|
on a no-op. Caught one validator bug along the way — `cutoff <
|
|
|
|
|
|
stop_discharge` was strict but the inverter actually accepts
|
|
|
|
|
|
`cutoff == stop_discharge` (which the user has set). Loosened to
|
|
|
|
|
|
`cutoff <= stop_discharge`.
|
|
|
|
|
|
|
|
|
|
|
|
## Still pending
|
|
|
|
|
|
|
|
|
|
|
|
- (optional) verify MOD 06 label by observing in non-charging states
|
|
|
|
|
|
- (optional) finish PGS decode: capture under load + during discharge
|
|
|
|
|
|
- (optional, longer horizon) closed-loop BMS comms via the dedicated
|
|
|
|
|
|
pack→inverter CAN port; defer until open-loop has been stable for a
|
|
|
|
|
|
week and we can baseline SoC accuracy
|