Files
shaggy-solar/.claude/skills/calibration-charge/SKILL.md
noise 5e175d4d0b Fix calibration grid-assist lever: firmware NAKs stop_charge=0/Full
Live run 2026-06-24: flash.py apply NAK'd BUCD480,000 on both inverters — the
firmware rejects stop_charge_voltage=0 ("Full"). flash.py aborts on first setter
failure, so nothing changed and the cluster stayed in sync (verified).

The field flash.py calls stop_charge_voltage is actually the inverter's
battery_re_discharge_voltage (HA: sensor.lvx6048_*_battery_re_discharge_voltage):
the V at which loads switch back to battery after grid charging. 54.0 tops grid
charge to ~54V; raising to 56.0 is the corrected (but UNVALIDATED) lever and may
band-oscillate rather than hold absorption.

- calibration profile: 0 -> 56.0, with the finding documented.
- skill: solar-only is now the RECOMMENDED/known-good method; grid-assist demoted
  to advanced/unvalidated with a mandatory diff-preview gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:14:35 -04:00

6.8 KiB
Raw Blame History

name, description
name description
calibration-charge Guided runbook to re-anchor the 6 EG4 LifePower4 pack SoC counters by driving a full charge to absorption and verifying every pack resets to 100%. Use when pack SoC readings have drifted (e.g. one pack reads much higher/lower than the others while all pack voltages agree), when the user asks to "calibrate / balance / fix SoC / do a full charge", or on a monthly cadence. This is the ONE skill that changes inverter settings — and only the grid-charge ceiling, only on explicit user confirmation, and it reverts them. Everything else is read/monitor/verify.

calibration-charge

Re-anchors drifted EG4 SoC counters. The BMS only resets SoC to 100% on a real full-charge termination (high cell voltage + low taper current); the everyday profile caps grid charging at 54 V, so the bank can go weeks without a full charge and the counters drift. This runbook guarantees one full charge, then verifies.

Action-policy exception (read this)

Unlike the troubleshoot-* skills, this one MAY change two inverter settings — but ONLY stop_charge_voltage (54.0 → 0/Full) via the prepared calibration profile, applied to BOTH units, and ONLY after the user explicitly confirms in-session. You present the exact flash.py commands; the user runs them (or confirms you may). You own pre-flight, monitoring, verification, and the REVERT. Never change anything else.

0. Load context

ROOT="$(git rev-parse --show-toplevel)"; SNAP="$ROOT/.claude/skills/lib/solar-snapshot"; HIST="$ROOT/.claude/skills/lib/ha-history"
FLASH="$ROOT/LVX6048/lvx-flash"   # flash.py + profiles/ live here

Read $ROOT/.claude/skills/REFERENCE.md. The two profiles: eg4-lp4-v2.yaml (canonical/everyday) and eg4-lp4-v2-calibration.yaml (temporary; identical except stop_charge_voltage: 0).

1. Pre-flight (must pass before charging)

"$SNAP" -w 16 -g 'lifepower4_[1-6]_(soc|pack_voltage|cell_voltage_max|cell_voltage_delta_mv|temperature_pcb|protection)' 'homeassistant/sensor/+/state'

Record and check:

  • Capture the "before" SoC spread — this is what we're fixing. Confirm it's drift not real imbalance: if all 6 pack_voltage agree (±0.1 V) but SoC readings differ, it's counter drift (the target). If voltages actually diverge, STOP — that's real imbalance → troubleshoot-battery first.
  • Temps in charge range: every temperature_* between ~5 °C and 45 °C. Never charge LFP below 0 °C (BMS should block, but verify). Abort if any pack > 45 °C.
  • No protection bits set; cells reasonably balanced (cell_voltage_delta_mv < ~50).
  • Forecast/grid: solar-only needs a sunny low-load day; grid-assist works anytime.

2. Choose the method (ask the user)

  • Solar-only — RECOMMENDED (no setting change, free, known-good): on a sunny, low-load day with a full day ahead, solar drives the bank through the full CC/CV curve to bulk 56.4 V and holds absorption on its own — exactly the clean termination the BMS needs to re-anchor. No flash, no risk. Just monitor §4 and verify §5; skip §3. This is the method to default to.
  • Grid-assist — ADVANCED / UNVALIDATED: the field flash.py calls stop_charge_voltage is really battery_re_discharge_voltage; the firmware NAKs 0/"Full" (confirmed 2026-06-24 — both units rejected BUCD480,000). The corrected lever raises it 54.0 → 56.0 so grid charges higher, but it may band-oscillate near 56 V instead of holding a clean absorption, so it is not proven to re-anchor. Only use as a supervised experiment when solar can't reach full and you accept it may not fully work. Needs §3.

3. Grid-assist (ADVANCED): apply the calibration profile (USER-CONFIRMED setter change)

Run flash.py diff FIRST and confirm the ONLY change is stop_charge_voltage 54.0 -> 56.0 before applying. If any apply NAKs, the cluster is unchanged (flash.py aborts on first failure) — fall back to solar-only. Mirror to BOTH inverters (parallel cluster — mismatched settings throw fault 86). flash.py apply stops powermon for exclusive USB, so MQTT telemetry pauses briefly.

cd "$FLASH"
sudo systemctl stop powermon.service powermon2.service
./flash.py apply --device /dev/lvx6048-1 --profile profiles/eg4-lp4-v2-calibration.yaml --confirm
./flash.py apply --device /dev/lvx6048-2 --profile profiles/eg4-lp4-v2-calibration.yaml --confirm
./flash.py compare --device-a /dev/lvx6048-1 --device-b /dev/lvx6048-2   # must match
sudo systemctl start powermon.service powermon2.service

Confirm via MQTT the new ceiling took (both units): battery_recharge/stop_charge readback reflects Full, others unchanged.

4. Drive + monitor the charge (this is the agent's job — poll periodically)

"$SNAP" -w 16 -g 'lifepower4_[1-6]_(soc|pack_voltage|pack_current|cell_voltage_max|temperature_pcb)' 'homeassistant/sensor/+/state'
"$SNAP" -w 10 -g 'lvx6048_[12]_(device_mode|mppt1_input_power|ac_output_active_power)/' 'homeassistant/sensor/+/state'

Watch for, and re-poll every ~1530 min as it climbs:

  • pack_voltage rising toward ~56 V; device_mode should show charging/absorption.
  • SAFETY — abort and revert (§6) if: any cell_voltage_max > 3.60 V (BMS protects ~3.65; don't ride it), any pack temp > 45 °C, or any protection bit sets.
  • Absorption/taper: once pack voltage holds near bulk and pack_current tapers to ~5 A/pack (≈0.05 C, < ~30 A total), the BMS will flip SoC to 100%. This hold is the part that actually re-anchors — let it finish, don't cut it early.

5. Verify the re-anchor (the whole point)

"$SNAP" -w 16 -g 'lifepower4_[1-6]_(soc|pack_voltage|cell_voltage_delta_mv)' 'homeassistant/sensor/+/state'

PASS = all 6 packs read SoC 100% (≥99) with pack voltages converged (spread < ~0.1 V) and cell deltas still tight. The previously-drifted pack (e.g. pack 6) now matching the others = counter re-anchored. Report before→after SoC spread. If a pack still lags, it didn't terminate — extend the hold or investigate that pack.

6. REVERT (mandatory if you did §3 — do NOT leave the ceiling lifted)

cd "$FLASH"
sudo systemctl stop powermon.service powermon2.service
./flash.py apply --device /dev/lvx6048-1 --profile profiles/eg4-lp4-v2.yaml --confirm
./flash.py apply --device /dev/lvx6048-2 --profile profiles/eg4-lp4-v2.yaml --confirm
./flash.py compare --device-a /dev/lvx6048-1 --device-b /dev/lvx6048-2
./flash.py sync-check --device-a /dev/lvx6048-1 --device-b /dev/lvx6048-2
sudo systemctl start powermon.service powermon2.service

Confirm stop_charge is back to 54.0 on both units via MQTT.

7. Record

Note the date (for the ~monthly cadence) and before→after SoC spread. If drift returns fast, consider the opt-in soc_estimator daemon backstop (see memory project_eg4_soc_drift_remediation) as a longer-term fix.