Skip to content

fix(loadpoint): a duty quarter runs the car for the plan's on-time - #1827

Merged
frahlg merged 2 commits into
masterfrom
fix/ev-keeps-order-at-quarter-end
Oct 25, 2026
Merged

frahlg merged 2 commits into
masterfrom
fix/ev-keeps-order-at-quarter-end

Conversation

@frahlg

@frahlg frahlg commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Field case

Home box, 25 Oct 2026, v0.155.1-beta.1. The site is 3 × 20 A with an Easee charger, a 9.6 kWh Sungrow battery and planner mode. I read the box read-only: the journal, /api/series, /api/mpc/diagnose/* and /api/status.

  • From 02:37 the plan gave the car its 10 350 W step for the rest of the quarter: a duty slot with on-power 10 350 W. The mains ran at 230.8 V and Core computes with 230 V. The charger runs whole amps, so 15 A drew 10 386 W and the quarter's energy ran out about 3 s early. Core ordered 0 W.
  • The Easee applied that stop at 02:45:08, in the next quarter. The plan wanted 8 970 W there, with the battery at 2 091 W, which fits: L1 2.6 + 13 + 3 = 18.6 A of the 19 A working limit.
  • With Core's own offer at 0, the EV hard limit took its idle-start path. It read the car's own current, still on L1 (17.9 A), and the heat that current left as house load. The journal shows fuse_cap_w=0 … below minimum step from 02:45:00 to 02:46:24, then a cap of 5 936 W (9 A).
  • Then charging.interrupted fired and a fuse-saturated replan ran. The new plan put the battery at 5 kW in the 02:45 quarter and moved the car to 03:45.

The battery did not take the room. It charged 260 W when the cap went to 0, and rose to its planned 2 091 W only after the car stopped. Its 3.8 kW from 02:47 was the new plan, made while the car was idle.

Change

computeCommandForBudget, duty branch:

  • The car runs its on-power for the plan's on-time: the slot's energy at that power, counted from the start of the plan's interval, which is the replan time after a replan. It also runs on until the metered energy is spent, if that comes later.
  • A car that draws more than its step, as on mains above 230 V, uses up the energy early but keeps running for the on-time. A car that draws less runs until the energy is spent, as before.
  • An off-time at the slot's end shorter than CommandSettle (20 s) would reach the charger only in the next slot, so the car runs to the slot's end.
  • The extra on-time only continues an order that is already running. A car that the fuse cap, a hold or anything else stopped stays stopped.
  • The rule needs energy above zero and a slot that has begun. The deadline reserve keeps its own rule.
  • docs/energyplan-contract.md now says the same.

The order still passes every clamp after this point: the fuse clamp, surplus, vehicle completion and the deadline reserve's stop when power is missing.

The extra charge is what the car's extra draw gives over the plan's on-time: about 3 % at 237 V, which is 70 Wh in a full quarter at 10 kW. A dropped off-time adds at most 20 s at on-power.

Deleted: nothing. Production code: +18/−2 lines in computeCommandForBudget.

Scenario numbers

TestDutySlotRunsItsOnTime, ticks every 3 s:

Case master 8e33174 this branch
full quarter at 230.8 V (the home box) stops at 897 s runs to the end
full quarter at 239 V stops at 867 s runs to the end
replanned at 5 min, 239 V stops at 879 s runs to the end
nine tenths, car draws 4 % more stops at 780 s stops at 810 s, the on-time
nine tenths, car draws 5 % less stops at 855 s same
off-time of 15 s stops at 885 s runs to the end
off-time of 20 s stops at 882 s same
off-time of 25 s, car draws 2 % more stops at 858 s stops at 876 s
no energy 0 W at once same
fuse cap 5 000 W from 840 s 4 140 W same
fuse cap 0 W at 783–795 s, energy spent stops at 780 s stops at 783 s, stays stopped

Closed loop and field matrix:

Check master 8e33174 this branch
sitesim "home box night, quarter's energy spent 3 s early" (230.8 V) fails at 02:45:06: offer 0 W (fuse_limit); car idle 1 min 45 s, back at 10 A, not 13 A passes; 13 A and battery 2 091 W at 02:50, peak 18.5 A
sitesim "home box night at 237 V, energy spent 27 s early" fails at 02:44:42: offer 0 W (plan) passes; peak 18.4 A
Field matrix 340 cases, 29 fail (idle 23, own-raise 6) same 29, known rows unchanged

The import ceiling wins

The on-time only shapes what the loadpoint asks for. The EV hard limit's cap carries both the fuse and the hour's import ceiling. freezeFuseBudget and applyFuseClamp apply that cap after computeCommandForBudget, so a ceiling cut always wins. Once it has cut the car, the spent energy keeps the car off: the extra on-time only continues an order that is still running.

TestSiteSimDutyOnTimeYieldsToTheImportCeiling (sitesim, real EVFuseBudget and hour logic):

  • At 237 V the car spends the 02:45 quarter's energy at 02:59:33, and the on-time keeps it at 10 350 W.
  • At 02:59:40 the hour's ceiling leaves 5 kW for the rest of the hour.
  • The next tick orders 0 W on import_limit, and no order above 0 W follows before 03:00.

Two independent Sonnet reviews ran on this branch.

  • The first, on an earlier version that kept the order for a slot's last 20 s only, measured that it stops again from about 236 V. That version fails the 239 V, 4 % and 25 s cases and the 237 V row.
  • The second found that the extra on-time could restart a car the fuse had stopped. The 0 W fuse-cap case covers that guard. It also asked for the guard against zero energy, parentheses, the comment on one-phase units, the contract text and the added cases.
  • Neither found a clamp the order bypasses.

make verify passes.

Not in this PR

  • fix(control): fuse protection as one function #1789 stays as it was: the one-function refactor, a draft for later. fix(control): reserve pending battery rises before releasing relief #1816 is stacked on its head c60da23, so I did not rewrite that branch.
  • The start test adds a rise on top of the fuse heat (fuseHeat.levelA: max(reading, heat) + pending). After the car's own stop, that kept it off 84 s with 14 A of room on the reading. Under the first-order model, a rise is safe when the heat is under the limit and reading + pending + rise is too. The heat also slows an EV rise into room the battery's discharge made, so this is a fuse-model change for a separate decision.
  • A planned off-time of 20 s or more before a slot that charges again still stops and restarts the car. That is the plan's choice. The restart then waits for the fuse heat to cool and for the 35 s restart dwell. In the review's sitesim run, a plan stop 30 s before 02:45 restarted only at 02:46:16.
  • The fuse-saturated replan that moved the car is gone: fix(mpc): drop the fuse-saturation replan; Core replans name their cause #1824, merged.

Hardware

Not yet run on the box. Real-hardware evidence is still needed: a quarter boundary on the home box with the car at a full step.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 25, 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-25T08:53:49.129128Z 2a5c978 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 and others added 2 commits October 25, 2026 10:25
The home box on 25 Oct 2026 ran at 230.8 V, so the car drew 10386 W
against the plan's 10350 W and spent the quarter's energy about 3 s
early. Core ordered 0 W; the stop reached the Easee in the next
quarter, which wanted 8970 W. With Core's own offer at 0, the EV hard
limit took the start path and read the car's own current still on L1
as house load: fuse_cap_w=0 for 84 s, then 9 A, then
charging.interrupted and a replan that moved the car an hour later.

A duty slot now commands its on-power for the plan's on-time, the
slot's energy at that power from the plan's interval start, and on
until the energy is spent. A car that draws more than its step keeps
running for the on-time; one that draws less runs until the energy is
spent, as before. The extra on-time only continues a running order,
so a car something else stopped stays stopped. An off-time shorter
than CommandSettle would reach the charger only in the next slot, so
the car runs to the slot's end. The deadline reserve keeps its own
rule.

The sitesim rows replay the night at 230.8 V and 237 V and fail on
master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At 237 V the car spends the 02:45 quarter's energy at 02:59:33 and the
on-time keeps it on. At 02:59:40 the hour's ceiling leaves 5 kW for
the rest of the hour. The EV hard limit's cap applies after the
loadpoint's order, so the car goes to 0 W on import_limit at the next
tick and stays off: the spent energy keeps it there.

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