diff --git a/.claude/skills/REFERENCE.md b/.claude/skills/REFERENCE.md index 786740c..4623617 100644 --- a/.claude/skills/REFERENCE.md +++ b/.claude/skills/REFERENCE.md @@ -114,9 +114,13 @@ MQTT — compute them yourself from the raw entities when working off the Pi. untrustworthy and use the `lifepower4_*` pack entities for any battery math; if it reads ~10 V right now, a powermon (or inverter) restart may clear it — worth testing. - **Pack 6 is an oddball**: Modbus addr `0x01` @ 115200 (packs 1–5 are `0x40` @ - 9600); ran 65 % SoC while 1–5 sat 40–44 %. Treat as a distinct member. + 9600). It reads SoC high (76 % on 2026-06-24 vs 50–55 % on packs 1–5) — but at the + SAME pack_voltage (53.4 V) and cell voltage (3.337 V), so that's **counter drift, + not real imbalance**: all packs are paralleled and physically at the same charge. - **EG4 SoC never re-anchors** (drifts because packs rarely hit 100 % to reset the - coulomb counter). See memory `project_eg4_soc_drift_remediation`. + coulomb counter). Verified live via the equal-voltage/different-SoC signature above. + Fix = the `calibration-charge` skill (periodic full charge). See memory + `project_eg4_soc_drift_remediation`. - **RS485 daisy-chain silences slave packs** — each pack needs its own FTDI; an inter-pack chain demotes slaves. See memory `project_eg4_daisy_chain_silences_slaves`. - **No per-day inverter energy** — PI18 only gives `ET` (lifetime Wh); ED/EM/EY NAK. @@ -143,3 +147,9 @@ MQTT — compute them yourself from the raw entities when working off the Pi. When a fix is outside the allowed set, report the finding and hand the user the precise command(s) to run. + +**Scoped exception — `calibration-charge` skill only:** that one skill may change +exactly one setting (`stop_charge_voltage` → Full and back) via the prepared +`eg4-lp4-v2-calibration.yaml` profile, on BOTH inverters, and ONLY after explicit +in-session user confirmation, and it must REVERT afterward. No other skill and no +other setting. This does not loosen the policy above for anything else. diff --git a/.claude/skills/calibration-charge/SKILL.md b/.claude/skills/calibration-charge/SKILL.md new file mode 100644 index 0000000..34044f3 --- /dev/null +++ b/.claude/skills/calibration-charge/SKILL.md @@ -0,0 +1,109 @@ +--- +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 (no setting change, free):** on a sunny, low-load day the bank reaches + bulk 56.4 V on its own (the 54 V cap only gates GRID charging, not solar). Just + monitor §4 and verify §5. Best when weather cooperates; no flash needed — skip §3. +- **Grid-assist (reliable, any weather):** temporarily lift the grid-charge ceiling so + grid finishes the top-off. Needs the setter change in §3. Use this if it's cloudy or + a full charge hasn't happened in a while. + +## 3. Grid-assist: apply the calibration profile (USER-CONFIRMED setter change) +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. diff --git a/LVX6048/lvx-flash/profiles/eg4-lp4-v2-calibration.yaml b/LVX6048/lvx-flash/profiles/eg4-lp4-v2-calibration.yaml new file mode 100644 index 0000000..73bd536 --- /dev/null +++ b/LVX6048/lvx-flash/profiles/eg4-lp4-v2-calibration.yaml @@ -0,0 +1,51 @@ +# LVX6048 settings profile — TEMPORARY calibration charge for the EG4 LP4 v2 bank. +# +# Purpose: re-anchor drifted EG4 pack SoC counters (and top-balance) by letting the +# bank reach a FULL charge with absorption hold. The EG4 BMS resets SoC to 100% only +# on a real full-charge termination (high cell voltage + low taper current); the +# conservative everyday profile stops grid charging at 54.0 V (mid-knee), so on cloudy +# / high-load stretches the bank may go weeks without a full charge and the coulomb +# counters drift (e.g. pack 6 read 76% while physically at ~53% on 2026-06-24). +# +# The ONLY change vs the canonical eg4-lp4-v2.yaml is: +# stop_charge_voltage: 54.0 -> 0 (= "Full"; remove the grid-charge ceiling so a +# full charge can complete even without strong sun) +# bulk_voltage stays 56.4 (the absorption target). Solar already charges past 54 V on a +# good day; this profile just lets GRID finish the top-off when solar can't. +# +# USE: this is a TEMPORARY profile driven by the `calibration-charge` skill. Apply to +# BOTH inverters, run the full charge, verify all 6 packs hit 100%, then REVERT to +# eg4-lp4-v2.yaml. Do not leave this profile applied — it removes the everyday +# grid-charge ceiling. +# +# 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 +# sudo systemctl start powermon.service powermon2.service +# # ... drive + verify the charge (see calibration-charge skill) ... +# # REVERT when all packs read 100%: +# 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 +# sudo systemctl start powermon.service powermon2.service + +battery_type: USER + +cutoff_voltage: 48.0 +stop_discharge_voltage: 48.0 + +# 0 = Full — let grid charge all the way to bulk (the calibration lever). +stop_charge_voltage: 0 + +bulk_voltage: 56.4 +float_voltage: 54.0 + +max_charging_current: 60 +max_utility_charging_current: 30 + +output_source_priority: solar_battery_utility +charger_priority: solar_first +solar_power_priority: battery_load_utility_ac + +grid_tie: disabled