Skip to content

fix(mpc): deadline reserve waits for the latest start (#1718) - #1896

Merged
frahlg merged 2 commits into
masterfrom
d6-deadline-reserve-latest-start
Oct 27, 2026
Merged

frahlg merged 2 commits into
masterfrom
d6-deadline-reserve-latest-start

Conversation

@frahlg

@frahlg frahlg commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Item D6 of the stable plan. Fixes #1718.

Problem

When no plan is in force, a saved car goal falls back to the deadline reserve (SlotDirectiveAt, service.go:667-675). This happens with no plan, a failed replan, a restored archive, or a plan the contract rejects. DeadlineNeed never looks at the deadline, so the reserve orders full power whatever the slack.

Example: a car at 30 % of 60 kWh with a goal of 80 % in 10 h needs about 33.3 kWh, or about 3 h at 11 kW. That leaves about 7 h of slack. On master the loadpoint still orders 11,000 W ("EV deadline reserve") in all three fault cases. The next plan then pauses the car, as in the #1718 log (10,350 W at startup, then 0 W after the replan).

A second, smaller fault: DeadlineNeed refused a loadpoint with min_charge_w: 0, which config allows, even when its steps give a legal minimum.

Change

  • withDeadlineReserve reserves a goal only from its latest start: now >= TargetTime - hours - 1 h, where hours sums need/peak over every saved goal due at or before this goal's deadline. One hour is one slot at the planner's longest (hourly) price slot.
  • The sum covers cars that share one EV fuse allowance. freezeFuseBudget gives that allowance earliest deadline first, so from a goal's latest start the cars due by its deadline can finish one after another. With one car this is need/peak. With cars on separate fuse room they only start earlier, as on master. Before that the loadpoint gets no reserve, and the controller orders 0 W (no_plan_budget), as it does for any car without a plan budget.
  • When the car is already running, the margin is two hours. A 1 % SoC step at 11 kW moves need/peak by about 4 min. Without the wider margin, the car would stop and start again a few minutes later.
  • Manual Charge now and an accepted zero budget are unchanged.
  • DeadlineNeed checks the minW it is given, not st.MinChargeW.

Tests

  • New TestDeadlineReserveWaitsForLatestStart, copied from the audit's repro (TestReproDeadlineReserveIgnoresSlack). It fails on master with 11000 W for no plan, failed replacement and restored archive, and passes here.
  • New TestDeadlineReserveLatestStart covers the start boundary and the running margin.
  • New TestDeadlineReserveSharedAllowance: two 60 kWh cars, each 30 % to 80 %, share 11 kW with no plan, minute by minute, SoC read in whole percent. Both reach 80 % with the same deadline (earliest deadline first, and even split), with b due one hour later, and with b due much later. With a per-car latest start, three of the four cases miss (SoC 0.46 to 0.63).
  • New TestDeadlineReserveZeroMinWithSteps: a min_charge_w: 0 loadpoint with steps gets the reserve, and the controller orders power. It fails with master's deadline_need.go.
  • Three existing tests gave a goal with slack and checked the reserve's size. They now set a deadline within the latest start, or ask at the deadline (cars/reserve_test.go). What they check is unchanged.
  • go test -race -count=1 for ./internal/mpc/ ./internal/loadpoint/ ./internal/cars/ ./internal/control/ ./internal/api/ ./cmd/ftw/: all ok.
  • go vet ./... and go build ./...: ok.
  • ./internal/drivers/ fails in this worktree only because drivers/*.lua is not checked out (TestBundledDriversMeetStartupContract cannot read the files). This change does not touch drivers.
  • Rig scenario R5 (stale plan after 20 min downtime: no positive EV order in the first 30 s) is left to the siterig runner.

Risk

Low. Without a plan, a goal now starts later than before. The start uses each charger's peak and assumes cars that share an allowance get it in deadline order, as freezeFuseBudget gives it. If the fuse room, house load or a one-phase car delivers less than the peak, only the one-hour margin covers the gap: a single car held to 75 % of its peak just makes 80 %, and at 50 % it ends near 63 %. That needs no plan for the whole window, for example a long price outage with no kept plan (W2-13). The reserve still needs a known fuse cap, and that check is unchanged. #1865 touches the manual branch of withDeadlineReserve. Its hunks do not overlap this change.

🤖 Generated with Claude Code

Without an accepted plan, a saved goal reserved full charger power whatever
the slack, so a restart or one failed replan started the car and the next
plan paused it. The reserve now acts only once the rest of the goal at full
power plus one hourly slot reaches the deadline; a running reserve keeps a
second slot so a SoC step does not cycle it. DeadlineNeed checks the passed
minW, so a loadpoint with min_charge_w 0 and steps gets the reserve.

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

chatgpt-codex-connector Bot commented Oct 27, 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-27T13:34:49.838222Z 551b44d 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 551b44da50

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/internal/mpc/deadline_directive.go Outdated
Comment on lines +60 to +62
latestStart := st.TargetTime.Add(-time.Duration(need / peak * float64(time.Hour))).Add(-margin)
if now.Before(latestStart) {
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Account for shared fuse capacity before delaying charging

When multiple cars share a fuse budget and no executable plan is available, each car's latest start assumes it can independently receive its full peak. For example, two cars each needing 33.3 kWh, rated at 11 kW and due in 10 hours both wait until roughly four hours before departure. However, freezeFuseBudget in go/internal/loadpoint/controller_fuse_budget.go splits the site's total EV allowance between them: with an 11 kW shared allowance, only about 44 kWh can then be delivered against their combined 66.6 kWh need. Both goals were feasible before this delay, and the one-hour margin cannot cover the lost time. Compute the start threshold using joint remaining demand and available site capacity, or retain a conservative fallback when independent peaks cannot be supplied simultaneously.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 49ce18d. A goal's latest start now sums need/peak over every saved goal due at or before its deadline. The fuse budget serves the earliest deadline first, so the cars due by that deadline can finish one after another. TestDeadlineReserveSharedAllowance covers two cars on a shared 11 kW allowance; with the per-car rule three of its four cases missed the goal.

…1718)

Each car's latest start assumed it alone got its full peak. Two goal cars
on one EV fuse allowance with no plan then both waited until about
need/peak + 1 h, and the fuse budget, which serves the earliest deadline
first, could not finish both. A goal's latest start now counts the hours
at full power of every saved goal due at or before its deadline. With one
car the rule is unchanged; with cars on separate fuse room they only start
earlier, as before #1718.

Adds TestDeadlineReserveSharedAllowance (two cars, shared 11 kW, earliest
deadline first and even split) and TestDeadlineReserveZeroMinWithSteps,
which covers the DeadlineNeed minW change at directive and controller
level. Both fail without their fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg merged commit 930a102 into master Oct 27, 2026
24 checks passed
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.

EV briefly resumes on Core startup before restored plan is rejected as stale

1 participant