Add calibration-charge skill to fix EG4 SoC counter drift (improvement #1)
The everyday profile caps grid charging at 54V, so the bank can go weeks
without a full charge and the EG4 BMS coulomb counters drift (proven: pack 6
read 76% SoC while at the same 53.4V/3.337V-per-cell as packs reading 50-55%
— all paralleled, so physically equal charge; the spread is pure drift).
- profiles/eg4-lp4-v2-calibration.yaml: temporary profile, identical to
canonical except stop_charge_voltage 54.0 -> 0 (Full), so grid can finish a
full charge to the 56.4V absorption hold that re-anchors every pack to 100%.
- calibration-charge skill: guided runbook (pre-flight safety, two methods
solar-only / grid-assist, live monitoring with cell-voltage/temp aborts,
re-anchor verification, mandatory revert).
- REFERENCE: scoped action-policy exception (this skill alone may flip
stop_charge, both units, user-confirmed, must revert); corrected pack-6 /
SoC-drift notes to the verified equal-voltage-different-SoC signature.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:11:47 -04:00
|
|
|
|
---
|
|
|
|
|
|
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)
|
2026-06-24 14:14:35 -04:00
|
|
|
|
- **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 calibration: correct lever is output-priority SUB, add grid-cal-monitor
Discovered live 2026-06-25 driving an actual grid calibration: forcing a full
grid charge is done via OUTPUT PRIORITY, not voltage thresholds.
- SBU (everyday) won't grid-charge unless the bank is critically low; setting
charger_priority=solar_and_utility alone does nothing at 52V.
- SUB (output_priority=solar_utility_battery) runs loads on grid AND charges the
battery to full. Combined with charger_priority=solar_and_utility, grid charging
engages (device_mode->Hybrid/Line, line_dir->input, pack current jumps to ~120A).
- Both POP/PCP set via lvx-control (all-mode-safe, atomic, no flash/USB). Revert
POP->solar_battery_utility, PCP->solar_first when done.
The re_discharge/flash.py approach is dead (firmware NAKs stop_charge>float);
profile eg4-lp4-v2-calibration.yaml marked DEPRECATED.
- grid-cal-monitor: supervises a SUB grid charge, safety aborts (cell>3.60V/
temp>45C), detects re-anchor (all 6 packs ->100%), auto-reverts POP+PCP (trap).
- calibration-charge skill §3 rewritten to the POP lever.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:35:51 -04:00
|
|
|
|
- **Grid-assist — for cloudy days (CORRECT lever, validated 2026-06-25):** force a full
|
|
|
|
|
|
grid charge by switching **output priority to SUB**, NOT by touching voltage
|
|
|
|
|
|
thresholds. In the everyday SBU mode the inverter won't grid-charge unless the bank is
|
|
|
|
|
|
critically low; SUB makes it run loads on grid AND charge the battery to full. Both
|
|
|
|
|
|
setters go through lvx-control (all-mode-safe, atomic, no flash). Needs §3.
|
|
|
|
|
|
(Do NOT use `re_discharge`/flash.py — firmware NAKs it; see memory
|
|
|
|
|
|
`project_lvx6048_grid_charge_lever`.)
|
Add calibration-charge skill to fix EG4 SoC counter drift (improvement #1)
The everyday profile caps grid charging at 54V, so the bank can go weeks
without a full charge and the EG4 BMS coulomb counters drift (proven: pack 6
read 76% SoC while at the same 53.4V/3.337V-per-cell as packs reading 50-55%
— all paralleled, so physically equal charge; the spread is pure drift).
- profiles/eg4-lp4-v2-calibration.yaml: temporary profile, identical to
canonical except stop_charge_voltage 54.0 -> 0 (Full), so grid can finish a
full charge to the 56.4V absorption hold that re-anchors every pack to 100%.
- calibration-charge skill: guided runbook (pre-flight safety, two methods
solar-only / grid-assist, live monitoring with cell-voltage/temp aborts,
re-anchor verification, mandatory revert).
- REFERENCE: scoped action-policy exception (this skill alone may flip
stop_charge, both units, user-confirmed, must revert); corrected pack-6 /
SoC-drift notes to the verified equal-voltage-different-SoC signature.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:11:47 -04:00
|
|
|
|
|
Grid calibration: correct lever is output-priority SUB, add grid-cal-monitor
Discovered live 2026-06-25 driving an actual grid calibration: forcing a full
grid charge is done via OUTPUT PRIORITY, not voltage thresholds.
- SBU (everyday) won't grid-charge unless the bank is critically low; setting
charger_priority=solar_and_utility alone does nothing at 52V.
- SUB (output_priority=solar_utility_battery) runs loads on grid AND charges the
battery to full. Combined with charger_priority=solar_and_utility, grid charging
engages (device_mode->Hybrid/Line, line_dir->input, pack current jumps to ~120A).
- Both POP/PCP set via lvx-control (all-mode-safe, atomic, no flash/USB). Revert
POP->solar_battery_utility, PCP->solar_first when done.
The re_discharge/flash.py approach is dead (firmware NAKs stop_charge>float);
profile eg4-lp4-v2-calibration.yaml marked DEPRECATED.
- grid-cal-monitor: supervises a SUB grid charge, safety aborts (cell>3.60V/
temp>45C), detects re-anchor (all 6 packs ->100%), auto-reverts POP+PCP (trap).
- calibration-charge skill §3 rewritten to the POP lever.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:35:51 -04:00
|
|
|
|
## 3. Grid-assist: enable grid charging via output priority (USER-CONFIRMED)
|
|
|
|
|
|
Publish via lvx-control (atomic to both units, no powermon stop):
|
|
|
|
|
|
```bash
|
|
|
|
|
|
B=... # broker creds from ~/.config/powermon/powermon.yaml
|
|
|
|
|
|
mosquitto_pub ... -t solar/control/lvx6048/charger_priority -m solar_and_utility
|
|
|
|
|
|
mosquitto_pub ... -t solar/control/lvx6048/output_priority -m solar_utility_battery # SUB
|
|
|
|
|
|
```
|
|
|
|
|
|
Confirm within ~1 min: `device_mode` -> Hybrid/Line, `line_power_direction` -> input,
|
|
|
|
|
|
pack current jumps. Verify BOTH units match (output_source_priority) — parallel sync.
|
|
|
|
|
|
Then run `lib/grid-cal-monitor` (detached) to drive/verify/auto-revert; it reverts
|
|
|
|
|
|
`output_priority->solar_battery_utility` + `charger_priority->solar_first` on completion
|
|
|
|
|
|
(trap-guaranteed). Skip §3 entirely for the solar-only method.
|
Add calibration-charge skill to fix EG4 SoC counter drift (improvement #1)
The everyday profile caps grid charging at 54V, so the bank can go weeks
without a full charge and the EG4 BMS coulomb counters drift (proven: pack 6
read 76% SoC while at the same 53.4V/3.337V-per-cell as packs reading 50-55%
— all paralleled, so physically equal charge; the spread is pure drift).
- profiles/eg4-lp4-v2-calibration.yaml: temporary profile, identical to
canonical except stop_charge_voltage 54.0 -> 0 (Full), so grid can finish a
full charge to the 56.4V absorption hold that re-anchors every pack to 100%.
- calibration-charge skill: guided runbook (pre-flight safety, two methods
solar-only / grid-assist, live monitoring with cell-voltage/temp aborts,
re-anchor verification, mandatory revert).
- REFERENCE: scoped action-policy exception (this skill alone may flip
stop_charge, both units, user-confirmed, must revert); corrected pack-6 /
SoC-drift notes to the verified equal-voltage-different-SoC signature.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:11:47 -04:00
|
|
|
|
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 ~15–30 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.
|