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>
Battery lifetime charge/discharge counters (already-read energy block, added only when it answered), inverter heatsink temp on the pv stream, rated power parsed from the family name (1K5-HI-<kW>), device-fault raise/clear from fault codes 39067-69 with a change-latch and the read-it-or-touch-nothing rule, and the diagnostic metrics this week's debugging kept needing: inverter state, RC session flag, live vendor setpoint. Harness gains set_rated_w. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
…r 0.7.0 The static field scanner attributes every out.field assignment to every emit that uses the same variable name — pv's temp_c leaked into the meter's field set. pv_out/bat_out/met_out disambiguate. Capture fixture grows the CT block to the 8 registers the driver reads and asserts the per-phase and lifetime-counter fields. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
…slation Raw DC PV fed into the AC setpoint demands ~3.5% more than PV can deliver; the inverter covers the gap from the battery -- a steady ~-70 W drain at every held zero, spotted by the site owner from the dashboard (residual/PV ~= 3.3% across hold samples). PV_AC_EFF = 0.965, calibrated from those residuals. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
0.965 overshot: the residual flipped to +71 W at 4025 W PV. With the 2026-08-05 point (-70 W at 2455 W, factor 1.0) the efficiency curve shows 0.972 -> 0.983 rising with load; 0.977 keeps held-zero residual within ~+/-25 W across the daytime range, erring toward a few watts of charge rather than a steady drain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
The lua DRIVER block moved to 0.7.1 and 0.7.2 while the manifest and package recipe stayed at 0.7.0 — bump_driver.py exists precisely so a driver cannot misreport itself to the host; use it. The tested_devices note still said Read-only on a manifest that declares control: true; it now records what was actually validated on hardware. Adds the missing CHANGELOG entry for the control build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
index.yaml, devices.yaml and the support-status pair re-derive from the manifest; make check requires them committed in the same change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
Brings in the published sigenergy fix and the merged #69 base so the signed-channel check compares against the channel's current state. Signed-off-by: Leitet <johan@sourceful-labs.com>
ec4906a to
017ae50
Compare
The host's curtail dispatch sends an absolute cap; on this hardware the one validated lever is the remote-control AC setpoint, so the cap becomes a ceiling on it. Hybrid ordering documented in the header: PV beyond the cap charges the battery first (live BMS limit), genuine curtailment past that -- hardware-proven with the full-battery incident. Curtail-only sessions hold battery-at-zero under the ceiling in daylight and stand down at night so an AC=0 hold can never block self-use discharge; lease expiry, default mode and cleanup disarm the cap. Setpoints are now rounded to whole watts before the word split (the derate made them fractional; only Go's coercion made that work). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
The check's generators run in sequence and stop at the first diff, so the previous commit staged everything up to devices.yaml but never reached the support-status pair. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
Implements the contract from srcfl/ftw#738/#741, merged today. driver_command_v2 and driver_default_mode_v2 return structured results the host can hold to account: applied requires a write ack plus a setpoint readback the host itself observed, and default mode always writes the remote-control disable and reads it back. The v1 entrypoints stay for local operator builds; their default mode keeps the skip-if-not-ours courtesy toward FoxESS-app schedule periods, which v2 cannot prove and therefore does not offer. The migration surfaced a latent v1 bug: the host write bindings return an error string rather than raising, so pcall alone reported failed writes as success. checked_write/checked_write_multi now test both layers on every write path. Package recipe moves to the v2 runtimes (gopher-lua-source-v2, sourceful.host/*/v2) with driver_default_mode_v2 as the default-mode entrypoint; both targets build as unsigned candidates locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
Every tested_devices entry said untested, and no hardware ever validated the 11000-range map. On this vendor's hardware that is a trap, not a fallback: a Fox ESS inverter answers unknown registers with silence, so the wrong map produces timeouts with no error -- a real 1K5 answered none of it. This is not a clean supersession: foxess_h3_smart covers 1K5-HI and H3-Smart, not the H1 / H3 / H3-PRO / AIO-H3 families the old driver claimed, and those lose their only, unvalidated, listing. Coverage for them can return the way H3-Smart did -- written against hardware someone actually has. generate_history now carries forward the record of a driver whose manifest is gone: the history is the channel's memory of bytes that ran on hardware, not a mirror of the current catalog. foxess 1.0.0 stays recorded; the channel simply stops offering it. check-versions already accepts removals (it compares added and changed drivers). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Leitet <johan@sourceful-labs.com>
|
Marking this ready for review. Summary of where it stands and what the review decides: Engineering state. CI fully green, including Hardware evidence. Everything control-shaped here was validated on a live 1K5-HI-10-V1 over four days: discharge in full sun (PV uncurtailed, battery tracks), charge within and above PV surplus (bounded import, exact balance), hold-at-zero calibrated to ±25 W across the daytime range (two-point efficiency calibration recorded in the header), and PV curtailment (2000 W cap → export pinned at cap−load through cloud swings while the battery absorbed the excess). The register semantics (46003/46004 = AC active power, export-positive) were misdiagnosed twice before the model fell out and are confirmed against nathanmarlor/foxess_modbus; the header documents the evidence trail. What merging decides. The foxess removal, honestly framed. The last commit removes the untested 11000-range 🤖 Generated with Claude Code |
Draft pending #70's rollout decision — CI is now fully green. This is the control implementation behind the hardware findings posted on #70. #69 merged 2026-08-06 and the branch tracks
main. The v2 control contract (srcfl/ftw#738, #741) merged the same day, and this driver now implements it —driver_command_v2/driver_default_mode_v2with structured results, host-verified write evidence (write ack + setpoint readback), and a default mode that writes the release and proves it. Both package targets build as unsigned candidates and thepackagesjob passes; what remains before un-drafting is the #70 product question of rolling out the control tier, not engineering. The driver runs today as a local operator override on the 1K5 site (v1 entrypoints, kept for exactly that), 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. The register's semantics were reverse-engineered on hardware and then confirmed against nathanmarlor/foxess_modbus: it is the inverter's AC active power, export-positive — not battery power and not a grid target. A naivevendor = -targetcharges by importing (it displaced PV for 12 h on this site before the model fell out), and a naive positive setpoint curtails PV instead of discharging. The driver therefore translatesvendor = pv × PV_AC_EFF − battery_target, with the charge path guarded: BMS ceiling (46018/46019) minus a 200 W margin, a daylight split on PV string voltage, a one-cycle 0 W pause on import/export sign crossings, and hold-at-zero (a commanded 0 is a held setpoint, not a release — releasing produced a ~90 s limit cycle against the host's absorb ceiling).PV_AC_EFF = 0.977is calibrated from two held-zero hardware points; the residual stays within ±25 W across the daytime range. Base work mode stays Self-use — remote control is an overlay and Self-use is the only fallback.Safety posture (all hardware-exercised)
driver_default_mode; 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
Validated live on a 1K5-HI-10-V1 in all three regimes: discharge in full sun (−1000 W commanded → battery −1080 W, PV uncurtailed), charge within PV surplus (no import, no curtailment, on target), and charge above surplus (import appears gradually and bounded, balance exact). Hold-at-zero verified across multiple days including the −70 W → +71 W → ±25 W calibration arc recorded in the driver header. Upstream telemetry tests (
drivers/tests/test_foxess_h3_smart.py) replay the hardware capture, including per-phase CT and lifetime counters.Covered since the earlier draft notes
packagesCI job green. The v2 default mode always writes the disable and reads it back — the v1 skip-if-not-ours courtesy toward FoxESS-app schedule periods cannot be proven to the host, so the signed package owns 46001 outright (documented in the driver header).drivers/tests/test_foxess_h3_smart_control.py): statuses, codes, device_state, evidence lists, and the registers actually written, including the failure paths.pcallreported failed writes as success.pv-curtail, 0.8.0) via an AC-setpoint ceiling, hardware-validated: a 2000 W cap pinned export at cap−load while the battery absorbed the excess and PV ran uncurtailed.bump_driver.py; catalogs regenerated; changelog entry current.🤖 Generated with Claude Code