draft(foxess_h3_smart): battery control via the remote-control block — hardware-validated, pending the control-tier pipeline (#70) - #71
Conversation
|
Branch updated through v0.5.0 — the setpoint semantics turned out to be inverter AC active power, export-positive (proved on hardware when a naive discharge command curtailed PV in full sun; confirmed against nathanmarlor/foxess_modbus, whose charge path documents the import-displaces-PV behaviour). Discharge is now hardware-validated in daylight; charge ships behind four guards adapted from the reference (BMS-ceiling cap with 200 W margin, daylight split by string voltage, sign-crossing pause, release-on-uncomputable). Full story in the commits. |
…on 1K5-HI-10-V1 LOCAL BRANCH ONLY, not for upstream until the control-tier pipeline (issue #70) exists. Remote-control block 46001-46004, vendor timeout 60 s (master samples slowly; 15 s expires unseen), 60 s command lease, release in default_mode, charge refused at SoC>=99%. Deployed on ftw.local as the operator override. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
…battery power Hardware proof 2026-08-05: with the meter at -4 W and the battery charging on PV surplus, a 500 W charge command made the site import 590 W and the battery charge that much above the surplus. The inverter obeyed exactly what it was asked: import 500 W. Discharge hid this for two days because both readings move the grid the same way, so the host's closed loop converged anyway. Translate instead, using readings this driver already polls: desired_grid = grid_now + (battery_target - battery_now) Load and pv cancel, so no load measurement is needed, and each poll recomputes from fresh values rather than integrating. Verified against four live captures including the runaway that pinned the site at 4.4 kW import for 12 hours. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
…er, not battery power" This reverts commit cdd1991. Signed-off-by: Leitet <johan@sourceful-labs.com>
…power Proved on hardware by an operator watching the roof: with a full battery in full sun, a discharge command sent as a bare +500 made the inverter curtail PV from 3191 W to ~600 W instead of discharging. It had done exactly as asked - put 500 W on the AC side - and with a full battery, throttling PV was its only route. One model now explains every observation across three days: the 12-hour grid-import runaway (write -5000, import until the battery's charge ceiling), the charge test capped by the CV taper, the PV curtailment above, and why discharge appeared to work on 2026-08-03 - PV was ~0 that evening, and the naive vendor = -target is correct exactly when PV is zero. inverter_ac = pv_now - battery_target Verified against five captures, four of them live hardware. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
… implementation's findings Charge is not a formula: imported power displaces PV before adding to it, and a naive setpoint spirals (curtailed PV -> lower reading -> deeper import). v0.5.0 guards it four ways: a live BMS-ceiling cap (Pwr_limit_Bat_up minus the reference's 200 W PV-breathing margin, 250 W refusal floor), a daylight split on PV string voltage so night charging imports cleanly, a one-cycle 0 W pause when the setpoint crosses import/export, and clean release whenever a refresh becomes uncomputable. Discharge keeps the hardware-validated pv+|target| form. Fallback work mode stays SELF_USE in both directions, diverging from the reference on purpose: a dead-man fallback should be boring. Ten-scenario harness suite covers both directions and every guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
…es not release Releasing on zero handed the inverter back to native self-use, which absorbs PV surplus into the battery -- so every time FTW commanded the battery down to 0 against its absorb ceiling, charging surged back and FTW fought it down again: a ~90 s limit cycle observed live with steady 3 kW PV (battery saw-toothing 250..2300 W). Zero now rides the translation like any setpoint (AC = PV, battery pinned, surplus exports); release remains on lease expiry and driver_default_mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
Manifest to 0.5.1 with real sha/size; package-source declares what the driver actually is: read_only false, modbus.write, a battery command with typed inputs, vendor_autonomous default via driver_default_mode, and the bounded lease the driver already implements (5 s heartbeat / 60 s max / return-to-default). The build now fails at the true boundary: ftw-core control requires the v2 command contract (driver_default_mode_v2), which is exactly the pipeline gap issue #70 asks about — the schema is otherwise ready. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
75c2edc to
6f57027
Compare
…talogs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
|
CI status is now definitive: dco, drivers (full suite incl. control-claims), signed-channel, history, boundary all green. Manifest and package metadata are fully synced and declare the truth: The single remaining red is |
CT phase pairs 38816/38818/38820 (same single block read, count 2->8), site-sign flipped; amps derived as W/V so the sign carries through -- FTW's fuse bars read l1_a..l3_a signed, negative = export on that phase. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
Draft on purpose — do not merge. This is the control implementation behind the hardware findings posted on #70, made reviewable. It builds on the read-only driver in #69 (this branch includes those commits) and must not land before (a) #69 merges and (b) the #70 control-pipeline questions have answers — the signed channel builds write-inert read-only artifacts today, so this driver cannot ship through the existing release path at all. It currently runs as a local operator override on the 1K5 site, per Fredrik's own-driver-at-own-risk guidance.
What it does
driver_command("battery", power_w)drives the H3-Smart remote-control block: enable46001=1, timeout46002, i32 setpoint46003/46004(vendor discharge-positive; site charge-positive setpoint negated on the way out). Base work mode stays Self-use — remote control is an overlay, and Self-use is what the inverter reverts to on timeout.Safety posture (all hardware-exercised)
driver_default_modeanddriver_cleanup; the disable is only written if this driver enabled the session, so a FoxESS-app strategy period is never stomped (the app uses these same registers — operators must not run app schedule periods alongside).Evidence
Closed-loop dispatch on a 1K5-HI-10-V1: FTW passive-arbitrage targets ≈ −630 W, battery tracking through the slew ramp (−184 → −465 W), grid import driven 413 → 150 W toward the zero target, SoC descending from 94%. Register state during operation read back as exactly this driver's writes (1 / 60-s window / live setpoint). Mock-harness suite in the session covered enable ordering, refresh, two's-complement setpoints both directions, lease expiry, idempotent release, and full-pack charge refusal — the harness tests are not in this diff and would come with the real control PR once the pipeline shape is known.
Known open items for the real PR:
DRIVER/DRIVER_MANIFESTstill sayread_onlyin package metadata terms the channel can accept (this build flips the Lua flag only), no structured command results, no lease integration with FTW's control-v2 path (srcfl/ftw#738/#741), and version/manifest sync deliberately not run.🤖 Generated with Claude Code