Skip to content

foxess_h3_smart 0.9.5: pending charge goes universal — the BMS limit follows activity, not capability - #98

Merged
frahlg merged 4 commits into
mainfrom
foxess-universal-charge-pending
Aug 20, 2026
Merged

foxess_h3_smart 0.9.5: pending charge goes universal — the BMS limit follows activity, not capability#98
frahlg merged 4 commits into
mainfrom
foxess-universal-charge-pending

Conversation

@Leitet

@Leitet Leitet commented Aug 20, 2026

Copy link
Copy Markdown
Member

Follow-up to #96, from the same live site within a day of the 0.9.4 stable promotion.

What happened. The night refusal loop is fixed — and reappeared in daylight. Two incidents (2026-08-19 11:32 and 2026-08-20 07:48, both at 11–14% SoC with the battery idle at the SoC floor under weak PV): the BMS limit read below the 250 W floor with the sun up, the daylight branch refused instantly, the host re-commanded every 5 minutes, and the fault flapped indefinitely — one operator page per throttle window, forever.

The corrected model. Pwr_limit_Bat_up follows the battery's activity, not its capability. A battery that has sat idle dozes off and reports no headroom until a session asks and waits — at night because the whole inverter stands by, in daylight because the pack itself idles at the floor. The day/night split in 0.9.4 was treating a property of the battery as a property of the sun.

Fix. The pending-accept path is now universal: a floor-level limit always accepts the charge as pending — battery held at zero (night: AC = 0; daylight: AC = pv, PV passes to house + grid), session alive, limit re-read every poll, true setpoint applied the moment it rises, 3-minute patience before treating the refusal as genuine (full/cold pack costs at most 3 minutes of held-zero before native self-use resumes). Two new daylight harness tests mirror the night trio; 70 total pass.

Also corrects the incident dates the 0.9.4 notes carried (August, not September — my error, caught against the repository clock).

The discovering site runs the HA-side symptom (fault-flap paging) tiered and throttled meanwhile; this PR removes the flap's cause. Local-override validation on the live site to follow tonight's planner slots — same protocol as #96: verification notes get the measured result before this merges.

🤖 Generated with Claude Code

Leitet and others added 3 commits August 20, 2026 08:07
…lows activity, not capability (0.9.5)

0.9.4 fixed the night refusal loop but kept the instant refusal in
daylight, and the same loop promptly reappeared with the sun up: a
battery idle at the SoC floor under weak PV dozes off and reports no
charge headroom even in daylight (two incidents on the discovering
1K5, 2026-08-19/20: SoC 11-14%, battery 0 W, limit below the floor;
the host re-commanded every 5 minutes and the fault flapped
indefinitely, paging the operator).

A floor-level limit now always accepts the charge as pending: hold
the battery at zero (night: AC = 0; daylight: AC = pv so PV passes to
house and grid), keep the session alive, re-read the limit every
poll, apply the true setpoint the moment it rises, give up after the
3 min patience window. A genuinely full or cold pack costs at most
3 minutes of held-zero before native self-use resumes.

Also corrects the incident dates the 0.9.4 notes carried (August,
not September; author error).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Leitet <johan@sourceful-labs.com>
Live metric data (2026-08-20 morning, the discovering 1K5): the BMS
limit register stayed exactly 0 for 12+ minutes across held-at-zero
pending sessions at the SoC floor -- Pwr_limit_Bat_up follows power
FLOW, not capability, and a session that asks for nothing never wakes
the pack. The unguarded v0.2.0 charged 4.5 kW from this exact idle
state just by asking: asking is the wake-up call.

The pending state now commands a 200 W probe charge (night:
AC = -200; daylight: AC = pv - 200) -- below the refusal floor,
harmless to a genuinely full pack, bounded by the 3 min patience
window. The full setpoint still waits for the register to actually
rise; the BMS-ceiling invariant is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Leitet <johan@sourceful-labs.com>
… is dead

Live metric data went one step further than the probe design assumed:
Pwr_limit_Bat_up stayed flat 0 even while the pack demonstrably
charged at the probe level. In the idle-at-floor state the register is
not slow -- it is dead, and register-gated escalation can never fire.

Capability is now read from measured uptake when the register sleeps:
the request ramps 500 W per poll while the battery follows (half the
current request as the follow threshold), decays one step on stalls
rather than resetting (the inverter can lag a poll behind), never
exceeds the commanded target, and hands over to the register path the
moment the register actually rises. The patience window only counts
time without uptake -- a charging pack is success in progress, not a
refusal to time out. This is the uptake-feedback loop the header's
charge section always named as the next step, arrived at with the
hardware evidence it asked for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Leitet <johan@sourceful-labs.com>
@Leitet

Leitet commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

Live validation on the discovering 1K5 — two hardware findings and a working fix, all from this morning:

Finding 1: waiting doesn't wake the pack. The first 0.9.5 design (hold at battery-zero, wait for the register) engaged cleanly — pending accepted, no fault, no flap — but the new foxess_bms_charge_limit_w metric showed Pwr_limit_Bat_up flat 0 for 12+ minutes across held sessions. Patience expired, released, re-accepted, forever.

Finding 2: the register is dead even under current flow. A 200 W probe charge landed — the battery demonstrably charged at probe level — while the register stayed 0. In the idle-at-floor state the register doesn't lag; it lies. Register-gated escalation can never fire. (This also retroactively explains v0.2.0's happy 4.5 kW night import: the pack takes what's asked; only the register claims otherwise.)

The fix that ships in this PR: ramp on measured uptake. When the register sleeps, the request grows 500 W per poll while the battery follows (half the current request as the follow threshold), decays one step on stalls, never exceeds the commanded target, and hands over to the register path the moment the register actually rises. Patience only counts time without uptake — a charging pack is success in progress.

Measured result (2026-08-20 08:34 CEST, SoC 11%, register flat 0 throughout): commanded +2000 W → battery climbed 721 → 1119 → 1560 → 1938 → ~2000 W in ~90 s and held around target (± cloud-driven scatter of the documented one-poll-lag kind; mean on target). Three new harness tests cover the ramp, the stall decay, and uptake-blocks-timeout; 73 total pass.

The candidate build runs as the local override on the discovering site for the natural planner slots tonight. Review and merge at will — the HA-side alert tiering already keeps the operator's phone quiet meanwhile.

🤖 Generated with Claude Code

@frahlg
frahlg merged commit 525583a into main Aug 20, 2026
6 checks passed
@frahlg
frahlg deleted the foxess-universal-charge-pending branch August 20, 2026 07:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants