Repository navigation
fix(loadpoint): planned charging runs on three phases, and room never cuts a running order - #1799
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5fb6ad8332
ℹ️ 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".
| if len(steps) > 1 { | ||
| return steps | ||
| } | ||
| } | ||
| minimum := math.Max(6, math.Ceil(cfg.MinChargeW/step)) |
There was a problem hiding this comment.
Use the same fallback steps for planning and dispatch
When an explicit step table contains no three-phase levels, this falls through to a generated three-phase ladder, but dispatch still snaps against the original AllowedStepsW. For example, the configuration added in TestPlanningStepsKeepThreePhaseLevels with PhaseMode: "auto", MaxChargeW: 5520 and steps [0, 1380, 2300, 3680] gives the planner [0, 4140, 4830, 5520]. However, computeCommandForBudget now receives a 4140 W minimum through fuseConfig, so SnapChargeW finds no eligible configured step and returns zero for every positive planned allocation; freezeFuseBudget likewise assigns zero. The plan therefore promises energy while the car never charges. Normalize the dispatch steps consistently with this fallback, or avoid advertising steps that dispatch cannot issue.
Useful? React with 👍 / 👎.
Twenty-four rows from #1799's review and fix D: a charger whose table holds only one-phase levels (max 5,520 W, auto), phase mode unset with a goal and a 2,300 W plan slot, surplus-only with a goal and 2.5 kW of export, a held 1p order raised to 3p beside a phase at 20 A, the help report's site with a one-phase car, and a long L1 step under a three-phase car. Surplus cases get the planner's PV forecast as pickSurplusSteps reads it, so the one-phase rule is judged as Core makes it: one phase only when the next 30 min stay below three phases at 6 A. Surplus cuts follow the export and are no longer judged as fuse cuts. The trace also logs a change of reason. New label hold3p: the app sends Charge now as 3p for an auto loadpoint, and a charger whose table stops below 4,140 W gets 0 W. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…otection (#1800) * test(control): field matrix for EV charging and fuse protection v0.155.0-beta.1 passed our tests and failed on field sites the same night. We tested our defaults: fixed 3p, no step table, planned charging stubbed, and chargers that applied an order at once or exactly when they declared. Easee Cloud applies in 30-60 s and declares 180 s. TestSiteSimFieldMatrix runs Core's EV charging and fuse protection in the closed loop against 314 cases: the field reports (the help report of 22 Oct, the site in #1763, the home box) with every charger timing, a sweep over fuse, phase mode, step table, car phases and house load, and a timing sweep over every request. Planned charging now runs through the real controller: a minimal planner sends the slot's Wh and on-power, from the steps main.go gives the planner. Each case runs in a synctest bubble, so command evidence, the API's verdict and live Home use run on the plant's clock. Invariants, every second: the fuse holds; FTW's limit as before; no fuse cut while every phase is under the limit; no second cut while an unapplied cut covers the overage; a planned charge delivers 90 % of what the fuse allows; planned charging and Charge now never put a three-phase charger on one phase; a charger drawing against its order reads not following; Home use stays measured while every device reads. testdata/sitesim_field_known.txt lists the rows that fail on master and the fix that turns each green. The sim gains the Easee driver's auto split and phase hold, and outside steps no longer count as our raise. The matrix skips -race builds: it finds nothing there and runs 15 times slower. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(control): the write mode logs what it would fail * test(control): probe rows from the three-phase review Twenty-four rows from #1799's review and fix D: a charger whose table holds only one-phase levels (max 5,520 W, auto), phase mode unset with a goal and a 2,300 W plan slot, surplus-only with a goal and 2.5 kW of export, a held 1p order raised to 3p beside a phase at 20 A, the help report's site with a one-phase car, and a long L1 step under a three-phase car. Surplus cases get the planner's PV forecast as pickSurplusSteps reads it, so the one-phase rule is judged as Core makes it: one phase only when the next 30 min stay below three phases at 6 A. Surplus cuts follow the export and are no longer judged as fuse cuts. The trace also logs a change of reason. New label hold3p: the app sends Charge now as 3p for an auto loadpoint, and a charger whose table stops below 4,140 W gets 0 W. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… cuts a running order Field report, v0.155.0-beta.1, 3x20 A, Easee, phase_mode auto, a step table with one-phase levels: a three-phase car charged on one phase at about 3.3 kW all night, cut to 1,600 W and 0 W with every phase under its limit. Planned charging and Charge now run on three phases (Fredrik, 22 Oct 2026). auto is 3p; only surplus charging without a schedule may use one phase (day lock, dwell, then auto). An unset mode stays the driver's default. The planner gets explicit steps at or above 6 A on the car's phases, or the three-phase ladder; LimitPower's minimum is the lowest step. The controller snaps the plan to steps legal on its command's phases. The split between cars kept no credit for a one-phase order once the site cap passed the split, and cut it to the room on the car's own phase. Room now limits rises only: the current order is kept when the order it allows lands on as many phases or more. Only a measured overage cuts. The closed-loop sim splits auto as the Easee driver does and replays the night: master 1.92 kWh in 40 min with 5 cuts with room, now 6.79 kWh on three phases with none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…order A1: the split keeps the current order only when the order it allows lands on the same phases. 5,000 W moved from one phase to three adds 7.2 A to two phases that carried none of it. B1: a solar-only loadpoint with a goal charges from surplus, so its schedule no longer forces three phases; 2.5 kW of surplus goes out on one phase again instead of an order Easee turns into 3.3 A. B2, B4: threePhaseCapable decides from the step table, or the maximum, whether the charger reaches 6 A on three phases. If not, it is a one-phase charger: its steps and orders stay one-phase. If so, unset means auto: three phases, with orders raised to the 3p minimum, never a 2,300 W unset order Easee runs at 3.3 A. Each review probe is a test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1799 turns 192 B rows, 20 A rows and one #1795 row green. It makes 35 one-phase-car rows with planned charging fail on energy, verdict or a second cut: a one-phase car takes a third of a three-phase order, which #1801 (D) handles. One 32 A one-phase charger probe now shows the second cut of the netting row class. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ed759ec to
74603d0
Compare
…field rows The field matrix showed two gaps. A charger that applies in 2 s has already taken a raise by the next reading, and a fuse ramp climbs 1 A at a time, so a test's baseline must be the meter read before the first raise since the order it measures from. A charger that declares no response time is now given 3 minutes, as the fuse limit counts it, not one. Field matrix on #1799: 55 D rows, 28 #1795 rows and one A row turn green. Every one-phase-car row now meets its energy, verdict and one-phase invariants. 25 of them still cut twice when a house step on the car's phase meets the second cut of the netting class; they are labelled netting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…field rows The field matrix showed two gaps. A charger that applies in 2 s has already taken a raise by the next reading, and a fuse ramp climbs 1 A at a time, so a test's baseline must be the meter read before the first raise since the order it measures from. A charger that declares no response time is now given 3 minutes, as the fuse limit counts it, not one. Field matrix on #1799: 55 D rows, 28 #1795 rows and one A row turn green. Every one-phase-car row now meets its energy, verdict and one-phase invariants. 25 of them still cut twice when a house step on the car's phase meets the second cut of the netting class; they are labelled netting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing (#1795) * fix(feedback): a charger that draws against FTW's order is not following A Core limit returned "limited" before the charger's readings were compared with the order. In 0.155.0 an Easee Cloud charger went on drawing 3.1 kW after FTW cut it to 0 W for the main fuse, and the verdict stayed "Limited by the main fuse". A limit explains a lower order, not a draw that differs from it. A settled mismatch now goes to the normal comparison: drawing above the order is power_while_idle or power_above_target, a warning that becomes an alarm after ten more minutes; drawing below it is power_below_target, a warning, as under a plan order. Command evidence kept the last eight orders. Core sends a charger its order on every tick, so the two-minute response window shrank to 16 to 40 s and a slow charger was judged on an order it could not have applied yet. A repeat of the last order is no longer stored again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(feedback): the overdraw alarm counts only time drawing above the order PowerMismatchSince covers both directions. A car that drew 3 kW against a 7 kW order for 13 minutes and then rose to 9 kW was an alarm on the first reading above the order. Command evidence now keeps the start of the run above every command in force (PowerOverSince), and the alarm waits two minutes plus ten from there. Tests: a cut obeyed at 175 s stays Limited; under then over warns at once and alarms after 12 minutes of overdraw. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(control): delete the field rows #1795 turns green On master with #1799: all 28 rows labelled #1795 pass, and 16 D rows whose only failure was the verdict now read not following. No row fails that did not before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…field rows The field matrix showed two gaps. A charger that applies in 2 s has already taken a raise by the next reading, and a fuse ramp climbs 1 A at a time, so a test's baseline must be the meter read before the first raise since the order it measures from. A charger that declares no response time is now given 3 minutes, as the fuse limit counts it, not one. Field matrix on #1799: 55 D rows, 28 #1795 rows and one A row turn green. Every one-phase-car row now meets its energy, verdict and one-phase invariants. 25 of them still cut twice when a house step on the car's phase meets the second cut of the netting class; they are labelled netting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…field rows The field matrix showed two gaps. A charger that applies in 2 s has already taken a raise by the next reading, and a fuse ramp climbs 1 A at a time, so a test's baseline must be the meter read before the first raise since the order it measures from. A charger that declares no response time is now given 3 minutes, as the fuse limit counts it, not one. Field matrix on #1799: 55 D rows, 28 #1795 rows and one A row turn green. Every one-phase-car row now meets its energy, verdict and one-phase invariants. 25 of them still cut twice when a house step on the car's phase meets the second cut of the netting class; they are labelled netting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nned and ordered as one (#1801) * fix(loadpoint): a one-phase car on a three-phase charger is seen, planned and ordered as one A one-phase car draws a three-phase order's current per phase on one phase. On the field table the planner got [0, 4800], the car took 1,600 W of a 4,800 W order, and the feedback said not following. The loadpoint now tests each rise of its three-phase order on the site meter: one phase rising by about the order's rise, the other two still, for 30 s within the charger's response time, while the charger's own reading shows that draw. That marks the session's car one-phase (State.CarPhases, the field a car profile can set later); a rise on all three phases clears it, and unplugging resets it. A house load does not follow the orders, and a two-phase car moves two phases, so neither is marked. Marked, the planner gets the car's one-phase levels, 6 A up to the charger's current per phase. The charger stays on three phases and the order is the plan's power times three, so its current per phase is what the car should take. The command carries expected_w, a third of the order, and command evidence compares the car with that. The verdict is Limited, "Charging on one phase, at most 3.7 kW". The fuse limit is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loadpoint): one-phase car tests on fast and undeclared chargers; field rows The field matrix showed two gaps. A charger that applies in 2 s has already taken a raise by the next reading, and a fuse ramp climbs 1 A at a time, so a test's baseline must be the meter read before the first raise since the order it measures from. A charger that declares no response time is now given 3 minutes, as the fuse limit counts it, not one. Field matrix on #1799: 55 D rows, 28 #1795 rows and one A row turn green. Every one-phase-car row now meets its energy, verdict and one-phase invariants. 25 of them still cut twice when a house step on the car's phase meets the second cut of the netting class; they are labelled netting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(loadpoint): one-phase car check compares changes, guards a clear, and survives a restart Review findings on #1801: M1: the check compared the charger's whole draw with the meter's rise, so only a rise from about 0 A could mark a car. It now compares the change in the charger's draw since the baseline. M2: a three-phase verdict had no draw check, so a heat pump starting at 2 A a phase during a 2 A rise cleared a correct mark. It now needs the charger's draw to have risen by about the three phases' sum. M3: the first reading of a session had a zero baseline, and the mark lives in memory, so after a Core restart with the car charging it was never marked again. A reading with no baseline is no longer a test, and an unmarked car whose charger's whole draw shows on one phase while the other two carry less than a third of it, for 30 s, is marked. No three- or two-phase car can show that. Each review probe is a test, and each fails on the previous head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(api): judge a one-phase car after limits, setpoint and waiting "Charging on one phase" ran before the limit guard, so a fuse limit, import limit, fuse cooldown, stale site meter, changed setpoint or a reading older than the order all read as limited/one_phase. The branch now runs after the waiting check; those verdicts come first, and a draw beyond tolerance is judged as for any car. The field matrix judged the car's draw against the full order and let only one_phase excuse a third of it. It now judges the draw against the command's expected_w, as Core does, so a marked car under a fuse limit reads fuse_limit without failing the verdict check. The known list does not change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(control): rewrite the field matrix's known list on #1802 On master with #1802, the 39 one-phase car energy rows (D) and the one energy row labelled A pass with this branch; their lines and labels go. No row is new. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Problem
Field report, 22 Oct 2026, v0.155.0-beta.1: a 3×20 A site, Easee Cloud, loadpoint
phase_mode: auto, a step table with one-phase levels (1,600–3,800 W) and three-phase levels (4,800–11,100 W), and a plan for about 24 kWh by 09:00. The car is three-phase. All night it charged on one phase at about 3.3 kW, in runs of about 3 minutes with about 4 minutes off between them. The busiest phase never went above 17 A against a working limit of 19 A, and the site meter never showed an overage. After 04:40 the charger went to three phases and the car took 8.3–9.7 kW.Causes
PlanningStepspasses an explicit step table to the planner unchanged, so the planner could legally spread the night at 3.1–3.5 kW. Only one phase can deliver that, so Core sentautoorders below the 3,680 W split and the Easee ran on one phase. The site cap for an auto loadpoint is counted on one phase (evPoolFor), which kept it near 3.5 kW.phaseRoomCapWread the order as three-phase. It credited the car's current one-phase order with nothing (creditAneeds the same phases) and capped it at the room left on the car's own phase, about 2–3 A. That room cap was then sent as a one-phase order. Each tick credited less, so the order went 3,500 → 1,600 → 0 W. Before fix(control): a cut ends the reservation of the higher orders before it #1793 the cut also kept the earlier order reserved for 180 s, which made the 4-minute gaps.Change
B, three phases for planned charging and Charge now (Fredrik, 22 Oct 2026):
threePhaseCapable: a charger can run three phases when the site has three, it is not pinned to1p, and its step table, or itsmax_charge_wwithout a table, reaches 6 A on each phase (4,140 W at 230 V). A table with nothing that high is a one-phase charger, whatever its mode says.resolvePhaseMode: an operator pin (1p,3p) is sent as is. Unset andautomean the same:1pfor a one-phase charger, otherwise3p. Only surplus charging without a schedule may use one phase: the day lock, the 30 min dwell, thenautowith the driver's hold. A solar-only loadpoint's goal does not count as a schedule, since it charges from surplus anyway.holdPhaseMode: Charge now on unset orautois3p(1pon a one-phase charger). A hold below the three-phase minimum goes out asauto, so a 1,380 W Start is not lost.PlanningSteps: on a three-phase charger the planner gets the explicit steps at or above 6 A on each phase, plus 0, or the whole-amp three-phase ladder without a table. A one-phase charger keeps its table, or gets the one-phase ladder.LimitPower's minimum is never below the lowest step.fuseConfig), so a three-phase order never goes out at 2,760 W.A, the split never cuts a running order on room alone: room limits rises.
phaseRoomCapWkeeps the current order when the order it allows lands on the same phases; only a measured overage cuts, through the site cap that the split never exceeds. The same watts on other phases are a rise on some of them, so a move between one phase and three gets the room alone.Fuse rule unchanged: the site meter's current per phase plus FTW's own orders that cannot have shown yet. No setting added. The car-phases wording matches #1792: the step rule is "6 A on the car's phases", three unless pinned, so #1792's
2pbranch sits next to the1pone.Evidence
Replay of the night in the closed-loop sim (
night plan, auto, one-phase steps): 3×20 A, house 1.6/1.7/2.3 A, Easee applying in 40 s, the field step table, a plan for 11.1 kW. The sim adapter now splitsautoas the Easee driver does when no split is sent.TestSplitKeepsTheCurrentOrdergives the field numbers on master (cap 1,600 W, then 0 W with no room) and keeps 3,500 W here; a three-phase order moving to one phase stays held to the room.TestPlanningStepsKeepThreePhaseLevels: the field table on 3×20 A gives no planner step between 0 and 4,800 W forauto, unset and3p;1pkeeps its table; a charger withmax_charge_w3,680 keeps its steps.TestChargeNowRunsOnThreePhases,TestResolvePhaseMode.autoas one phase under a schedule now pin the new rule; the two that tested the shared budget's phase check use an explicit1ploadpoint, so they still test it.go test ./internal/api/... ./internal/telemetry/... ./internal/control/... ./internal/loadpoint/... ./internal/sitesim/... ./internal/mpc/... ./internal/config/... ./cmd/ftw/...passes.Review findings (independent review of 5fb6ad8 with #1795), fixed in ed759ec
TestSplitKeepsTheCurrentOrder"one-phase order moving to three phases": 0 WTestSolarOnlyWithGoalKeepsOnePhaseSurplus: 2,300 W on 1pthreePhaseCapable: such a charger keeps its 1p steps and ordersTestPlannedOrderFollowsTheChargersPhases: 3,680 W1p;TestPlanningStepsKeepThreePhaseLevels3praised to 4,140 W, or1pon a one-phase chargerTestPlannedOrderFollowsTheChargersPhases,TestEmptyPhaseModeIsThreePhaseAlso a test: a measured cut always beats a kept order (site cap 2,000 W after an overage gives at most 2,000 W).
Known limit, stated in the code: a one-phase charger above 16 A whose table reaches 4,140 W reads as three-phase and needs phase mode
1p, as #1791 already says for its current cap. A physically one-phase car on a three-phase charger (B3) is left for the next PR.Notes
allowed_steps_walready cut to the planner minimum (normalizedSteps); no contract change.phases.🤖 Generated with Claude Code