178 lines
9.0 KiB
Markdown
178 lines
9.0 KiB
Markdown
# 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
|