Skip to content

fix(control): a cut ends the reservation of the higher orders before it - #1793

Merged
frahlg merged 1 commit into
masterfrom
fix/ev-cut-ends-reservation
Oct 22, 2026
Merged

frahlg merged 1 commit into
masterfrom
fix/ev-cut-ends-reservation

Conversation

@frahlg

@frahlg frahlg commented Oct 22, 2026

Copy link
Copy Markdown
Member

Problem

Reported on real hardware by @segran2 in #1763 (v0.155.0-beta.1, Easee Cloud + Pixii, 25 A fuse): after an explicit pause, a 4,140 W request was first answered with 0 W and fuse_limit, and the first admission came about three minutes later, while the phases read 1–6 A. He also found the cause and reproduced it in the released source's evMeterSite.

#1776 counts an EV order as flowing until the charger can have applied it: commandLog.unshown takes the highest order sent within the charger's response time (Easee Cloud declares power_max_age_s=180). That included orders a later cut had already replaced. Sequence: 10,350 W, a 25 s pause at 0 W, then 4,140 W → 10,350 W stays reserved and the restart waits.

Change

go/internal/control/ev_fuse_budget.go, unshown: a cut ends the reservation of the higher orders before it. The charger follows its latest order, and the site meter shows its draw until the cut lands, so nothing above the cut can still arrive on top of the reading. A run of rises still counts its highest order, which is also its latest, so the stacking that #1776 fixed stays covered. Watts and current per phase are handled separately, so a 3p→1p switch at lower watts still counts its higher current per phase.

Fuse rule unchanged: the site meter's current plus FTW's own orders that cannot show yet. No setting, timer or constant changed.

Evidence

  • New TestEVHardLimitRestartAfterPauseIgnoresReplacedRise, the reported sequence with Easee's 180 s: master 2 min 47 s to the first offer; this change 12 s.
  • Unchanged and green: slow cloud charger 60/120 s (no stacking), 3p→1p at the same watts, two cars with a slow second charger, idle start, the closed-loop site simulation including a day. go test ./internal/control/... ./internal/loadpoint/... ./internal/sitesim/... -p 2 and make verify (78 packages) pass.
  • Accepted cost: if a slow cloud delivers a rise and a later cut in order, the replaced rise can show briefly before the cut lands. The fuse's heat filter cuts it, and a gG fuse carries 1.25 × In for an hour.

The device-simulator rig reruns the fuse cases on this commit before v0.155.0-beta.2.

Drafts #1782, #1789 and #1792 touch the same file and rebase after this.

🤖 Generated with Claude Code

#1776 counts an EV order as flowing until the charger can have applied
it, using the highest order sent within the charger's response time.
That included orders a later cut had already replaced. With Easee
Cloud's declared 180 s, a charge at 10,350 W, a 25 s pause and a
4,140 W request kept 10,350 W reserved: the restart waited 2 min 47 s
with fuse_limit while the phases read about 1.3 A. Reported on a real
Easee/Pixii site by segran2 in #1763, reproduced in evMeterSite.

The charger follows its latest order, and the meter shows its draw
until a cut lands, so a cut ends the reservation of the higher orders
before it. A run of rises still counts its highest, which is its
latest, so stacked rises stay covered. The restart now comes after
12 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T04:47:26.247396Z 6b19eba PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@frahlg
frahlg merged commit bbeac31 into master Oct 22, 2026
22 checks passed
frahlg added a commit that referenced this pull request Oct 22, 2026
Easee Cloud applies a cut after 30-120 s, but the EV fuse cap took each
cut from the highest offer of the last 20 s. Once that window passed, the
meter still showed the old draw and Core cut again: in the field matrix
10,400 -> 6,900 W, then 0 W after a 6 A kettle step. The same window
missed a raise still on its way, so a cut could leave the phase over the
limit once the raise landed.

The cut now counts our own orders that cannot have shown yet, both ways,
from the per-charger, per-phase log of #1776/#1793. A cut on its way
covers the overage it was sent for until its charger can have applied it
(max(20 s, declared power_max_age_s), 180 s undeclared); Core cuts only
what it does not cover. A raise on its way adds to the cut. A cut and
every order after it count as not shown; of the orders before it, a raise
counts from the lowest. An offer found in force at a restart, which Core
did not send in this run, counts as shown. Once a phase's filtered
current passes 1.25 x In, nothing on its way holds back a cut. Over the
fuse the cap never rises above the latest offers. Rises, starts and the
hour's import ceiling are unchanged.

Field matrix: 93 of 99 netting rows pass. The other six are cut by the
split, which floors 7,600 W to 6,900 W (7600/690*690 = 7599.999...);

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg added a commit that referenced this pull request Oct 22, 2026
)

* fix(control): a cut on its way covers the overage it was sent for

Easee Cloud applies a cut after 30-120 s, but the EV fuse cap took each
cut from the highest offer of the last 20 s. Once that window passed, the
meter still showed the old draw and Core cut again: in the field matrix
10,400 -> 6,900 W, then 0 W after a 6 A kettle step. The same window
missed a raise still on its way, so a cut could leave the phase over the
limit once the raise landed.

The cut now counts our own orders that cannot have shown yet, both ways,
from the per-charger, per-phase log of #1776/#1793. A cut on its way
covers the overage it was sent for until its charger can have applied it
(max(20 s, declared power_max_age_s), 180 s undeclared); Core cuts only
what it does not cover. A raise on its way adds to the cut. A cut and
every order after it count as not shown; of the orders before it, a raise
counts from the lowest. An offer found in force at a restart, which Core
did not send in this run, counts as shown. Once a phase's filtered
current passes 1.25 x In, nothing on its way holds back a cut. Over the
fuse the cap never rises above the latest offers. Rises, starts and the
hour's import ceiling are unchanged.

Field matrix: 93 of 99 netting rows pass. The other six are cut by the
split, which floors 7,600 W to 6,900 W (7600/690*690 = 7599.999...);

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(control): a cut covers only its own run, on known phases

Review of the first commit found three ways a cut on its way could cover
more than it removes:

- A cut that landed covered a later step on the same phase for the
  charger's whole response time: 180 s for a fast charger that declares
  nothing, such as OCPP. A cut now covers only within the run over the
  limit it was sent in. The run ends once every phase has read under the
  working limit for evSettle; a short dip, as battery relief gives, does
  not end it. Probe: 2 min 35 s over the limit becomes 22 s, as master.
- A car that draws less than its order retried each cut after 180 s
  (10 min 36 s over the limit in the review's case). Once the run's first
  cut has outlasted its charger's response time, cuts cover for evSettle,
  as before: 5 min 16 s.
- A change of phases read as a cut in amps per phase: 15 A on one phase
  to 6 A on three covered 9 A on every phase. Orders now carry their phase
  count; across a change the latest order counts in full as a raise. A
  single-phase cut covers no phase when other chargers share the pool.

Without phase currents Core cannot see the hottest phase, so there a cut
covers for evSettle, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(control): field rows after the cut netting on master 5ae590a

FTW_FIELD_KNOWN=write removes 99 over-cut netting, 1 limit netting and
4 over-cut D rows and adds none. 71 of 338 cases still fail, all listed
on master: energy 40 (39 D, 1 A), idle 25 (export, readback, hold3p),
own-raise 6 (relief).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant