Skip to content

fix(loadpoint): the deadline reserve holds to the plan's limits, not the charger's raw table - #1860

Merged
frahlg merged 2 commits into
masterfrom
fix/deadline-reserve-one-phase-car
Oct 25, 2026
Merged

frahlg merged 2 commits into
masterfrom
fix/deadline-reserve-one-phase-car

Conversation

@frahlg

@frahlg frahlg commented Oct 25, 2026

Copy link
Copy Markdown
Member

Problem

The independent review of #1807 (25 Oct 2026) found this case. With no plan, Core runs a saved goal on its deadline reserve. Take a charger whose step table has both one-phase levels (6–16 A on one phase, 1,380–3,680 W) and three-phase levels (4,140–11,040 W), with phase_mode unset, auto or 3p. A car whose own maximum is below the lowest three-phase level, such as a 3,680 W plug-in hybrid, got an order of 0 W and stood still until a plan came back. The field-matrix rows 16A|25A / empty|auto / mixed / car1p / noplan show it: 0 W, 0 kWh, idle. #1807 did not change this.

Cause

withDeadlineReserve (mpc/deadline_directive.go) took its on-power from DeadlineNeed, which held it to LimitPower(st, st.AllowedStepsW), the charger's whole table. With the car's 3,680 W maximum, that leaves the one-phase levels, so the reserve asked for 3,680 W. Planned charging runs on three phases on such a charger (resolvePhaseMode), so the controller snaps with the three-phase minimum, 4,140 W. SnapChargeW(3680, 4140, 3680, …) returns 0.

The car counts as one-phase (CarPhases == 1, #1801) only after the meter sees it draw. With 0 W it never draws, so it is never marked. The plan path does not have this bug: PlanningLimits gives it the three-phase levels, keeps the lowest one, 4,140 W, and the plan orders that.

Fix

The reserve now uses the limits the plan gets:

  • DeadlineNeed(st, minW, maxW, steps) takes the limits instead of computing them from the raw table.
  • Controller.PlanningLimits(st) is PlanningLimits on the loadpoint's phase mode and the site fuse. It reads them under the controller's and the manager's own locks, so a config reload cannot race it. The controller's own check of the reserve uses it.
  • mpc.Service.PlanningLimits is a new hook. main.go wires it to the controller's method. With no hook set, the reserve uses the charger's own table, as before.
  • A charger without a table still takes any power in its range, both at full need and near the goal.

What changes on the charger:

  • Mixed table, car not yet marked: the order is 4,140 W, as on the plan path. The car takes 6 A on its phase, the meter marks it one-phase, and the order goes to 11,040 W. The car then takes 16 A on its phase.
  • A marked one-phase car: the reserve is the third of a three-phase level it takes, and the controller orders three times that. This is fix(loadpoint): plan a one-phase car above the charger's minimum, not at it #1806's rule.

The reviewer also suspected a three-phase-only table with a car below its lowest three-phase step. I checked that case:

  • Car not marked: it was never 0 W. The reserve ordered 4,140 W before this change and still does. These rows pass on master and here.
  • Marked car smaller than 3.7 kW: it got the charger's top order. A 2.3 kW car was ordered 11,040 W. It is now ordered 6,900 W, three times its 2,300 W level. The car draws the same either way. The order and the fuse reservation now match what it takes.

Evidence

The new tests fail on master 286ac14 and pass here:

  • loadpoint:
    • TestDeadlineNeedRunsOnThePlansPhases fails on master: mixed table, 3.7 kW car, on-power 3,680 W where 4,140 W is wanted; three-phase table, 2.3 kW marked car, 4,140 W where 2,300 W is wanted.
    • TestControllerPlanningLimitsReadPhaseModeAndSite is new. It covers the new method: the phase mode, a one-phase site, and a nil controller.
  • mpc: TestGoalReserveHoldsToThePlansLimits goes through SlotDirectiveAt. On master the reserve is 3,680 W, and 1,380 W near the goal; 4,140 W is wanted.
  • control: TestJoinedNoPlanGoalSmallCarGetsChargerMinimum gains mixed tables (unset, auto, 3p) and a charger whose minimum (2,000 W) is above the car's maximum (1,500 W). It runs the real service and controller to the driver order. On master the orders are [0]; here they are 4,140 W. The last case also pins the controller's check: with that check reverted to the raw table, it orders [0].
  • Field matrix:
    • 12 new noplan rows: 16 and 25 A, with mixed tables (unset, auto, 3p) and three-phase tables (unset, auto), plus a 2.3 kW one-phase car on each table.
    • On master, the 7 rows on mixed tables fail idle: order 0 W, 0.0 kWh, idle for 55–59 min. The 5 rows on three-phase tables pass on both.
    • Here all 12 pass. In the hour the 3.7 kW car takes 2.3 kWh on a 16 A fuse, with a peak phase of 20 A as in the existing homebox noplan rows, and 3.6 kWh on a 25 A fuse, peak 25 A. The 2.3 kW car takes 2.3 kWh, peak 19 A.
    • The matrix has 354 cases and none fail. The known list stays empty.
  • Full suite: go vet ./internal/... ./cmd/ftw/... and go test ./internal/... ./cmd/ftw/... are green on 63 packages, after make drivers in the fresh worktree. Race runs of the reserve tests are clean.
  • Independent review (Sonnet): found no blocker.
    • On the first commit it ran a master-vs-branch matrix of 384 cases. One-phase sites, 1p pins, stepless chargers with an unmarked car, and manual holds are unchanged.
    • For a car that is not marked, every changed order went from 0 W, or from no reserve, to a valid order. For a marked car the orders change as described above and under Limits.
    • Its two findings are fixed here. The controller's check had no test, and the first wiring read cfg without cfgMu on the control goroutine.

Limits

  • Stepless chargers with a marked one-phase car: the reserve now follows the plan's whole-amp ladder. On the home box charger (4,140–11,000 W) that is 10,350 W, 15 A a phase, instead of 11,000 W. The plan already ordered 10,350 W. A car that is not marked keeps the old order.
  • Near the goal on a mixed table: the reserve orders 4,140 W, not a one-phase level. On master that case ordered 0 W. On a stepless or three-phase-only table, a marked car near its goal is now ordered 4,140 W, three times its lowest level. Master ordered the charger's top there.
  • A one-phase table pinned 3p: the order is still 0 W, as on master. That configuration has no valid three-phase order.
  • Overlap with open PRs: fix(mpc): the plan keeps each clock hour under the import ceiling #1835 adds a different field to mpc.Service and a different line in main.go. fix(charging): carry two-phase cars through plans, current limits and Easee #1792 touches other parts of controller.go and planning_steps.go. The hunks do not overlap.
  • Not tested: this has not run on the device-simulator rig or on a real site.

🤖 Generated with Claude Code

…the charger's raw table

With no plan, withDeadlineReserve took its on-power from DeadlineNeed,
which held it to LimitPower(st, st.AllowedStepsW): the charger's whole
table. On a table with one-phase and three-phase levels, a car whose own
maximum is below the lowest three-phase level (a 3,680 W plug-in hybrid)
got a one-phase level, 3,680 W. Planned charging runs on three phases, so
the controller snapped with the three-phase minimum, 4,140 W, and sent
0 W. The car is marked one-phase only once it draws, so it never was.
The plan path gets PlanningLimits and orders 4,140 W.

DeadlineNeed now takes the limits. Controller.PlanningLimits is
PlanningLimits on the loadpoint's phase mode and the site fuse, read
under the controller's and the manager's own locks. The controller's
reserve check uses it, and main.go wires it into the new
mpc.Service.PlanningLimits; nil keeps the charger's table. A marked
one-phase car is reserved the third of a three-phase level it takes, so
a 2.3 kW car on a three-phase table is ordered 6,900 W, not the
charger's 11,040 W. A charger without a table still takes any power in
its range.

Tests fail on master 286ac14: DeadlineNeed on mixed and three-phase
tables, the reserve through SlotDirectiveAt, and the controller's order
on mixed tables (unset, auto, 3p). Of twelve new noplan field-matrix
rows, the seven on mixed tables go idle on master (order 0 W); the five
on three-phase tables pass on both. The known list stays empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@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-25T18:42:53.750214Z c5fbdd4 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 792ef29 into master Oct 25, 2026
22 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.

1 participant