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

119 lines
6.8 KiB
Markdown
Raw 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.
---
name: calibration-charge
description: >-
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
```bash
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)
```bash
"$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.
```bash
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)
```bash
"$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)
```bash
"$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)
```bash
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.