Files
shaggy-solar/.claude/skills/calibration-charge/SKILL.md
noise 76765a95ed 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

127 lines
7.3 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.
---
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 — 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`.)
## 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.
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.