Files
shaggy-solar/LVX6048/2026-04-26-parallel.md
2026-04-27 06:50:04 -04:00

178 lines
9.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```
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)
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)
## 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