Repository navigation
fix(feedback): a charger that draws against FTW's order is not following - #1795
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: 0992c7d13d
ℹ️ 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".
| // FTW cannot stop a charger that draws more than it is told. A car | ||
| // that takes less only charges slower. | ||
| if f.Kind == "ev" && (reason == "power_while_idle" || reason == "power_above_target") { | ||
| f.Escalated = cmd.LastObservation.Sub(cmd.PowerMismatchSince) >= evMismatchWait+evOverdrawAlarm |
There was a problem hiding this comment.
Track sustained overdraw separately before raising an alarm
If a car draws below its order for more than twelve minutes and then jumps above it without an intervening matching reading, the first above-target reading immediately raises an alarm. PowerMismatchSince tracks disagreement in either direction: observeCommand does not reset it when the gap changes from underdelivery to overdelivery. For example, a charger reporting 3 kW against a steady 7 kW order can jump to 9 kW and immediately inherit the entire underdelivery duration. Track or reset a separate overdraw timer so the alarm requires the promised sustained overdraw rather than counting time when the car was merely charging slowly.
Useful? React with 👍 / 👎.
All 29 rows labelled #1795 in the field matrix pass: a charger that draws against its order reads not following. No other row changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
366e6c1 to
f81ffe1
Compare
#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>
…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>
… cuts a running order (#1799) * fix(loadpoint): planned charging runs on three phases, and room never 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> * fix(loadpoint): review findings on three-phase charging and the kept 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> * test(control): field rows after three-phase charging #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> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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>
…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>
f81ffe1 to
f19b3b0
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>
…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, Easee Cloud. The plan asked for 7.6 kW on three phases. The car drew 3.13 kW all night (about 13.6 A on one phase) and took about 13 kWh of the 24 kWh planned. At 06:32 FTW cut the order to 0 W for the main fuse and the charger kept drawing 3.13 kW. The feedback said "Limited by the main fuse", with the receipt "Sent ✓, Accepted ✓, Measured: Waiting for fresh readings". It never said the charger was not following.
Cause
classifyControlFeedback(go/internal/api/api_control_feedback.go:435on master) returnslimitedforfuse_limit,import_limit,charger_limit,fuse_cooldownand the two stale-meter stops before it compares the charger's readings with the order. Under any of these reasons a charger that draws against the order reads as Limited for as long as it draws. Only the no-draw case (no_power_response) was checked first.control_power_confirmed(device-drivers#149, in beta.1's pin), soobserveCommandsees a new observation every poll (command_evidence.go:407-435). "Waiting for fresh readings" is the receipt for evidence still ataccepted: a cut is a material step and starts a new comparison, which needs 10 s of readings before it counts as measured. The screenshot was taken in the minute of the cut.command_evidence.go:161). Core sends a charger its order on every control tick, so the two-minute response window shrank to 16 s at a 2 s tick (40 s at 5 s). A slow cloud charger could be judged on an order it could not have applied yet.Change
limited:power_while_idleorpower_above_target: Not following, a warning, and an alarm ten minutes after the two-minute wait, counted only from when the draw went above the order (PowerOverSince), since FTW cannot stop it;power_below_target: Not following, a warning, as under a plan order today. It does not escalate: a car that takes less only charges slower.No fuse logic, setting or client word changes. The web words for
power_while_idlealready read "Asked to stay idle, but it is charging 3.1 kW."Evidence
Field trace replayed through telemetry, command evidence and the API (
TestChargerDrawingAgainstAFuseStopIsNotFollowing): an hour at 7.6 kW, then 0 W for the fuse, an Easee-like charger confirming 3.13 kW every 5 s.TestChargerFollowingAFuseStopStaysLimited: a charger that stops after 100 s or 175 s (Easee's declared 180 s) stays Limited throughout, never an alarm.TestOverdrawAfterAShortfallAlarmsOnItsOwnTime: 13 min at 3 kW against a 7 kW order, then 9 kW: a warning at once, an alarm after 12 min of overdraw. With the run counted from the first mismatch in either direction it was an alarm after 5 s (review finding).TestCarBelowAFuseLimitedOrderIsNotFollowing: 3.1 kW against an 8.3 kW fuse-limited order is Not following, a warning, and stays a warning after 30 min. On master it read Limited.TestRepeatedOrderKeepsEarlierOrderInForce: fails on master (the 7.6 kW order is out of force 16 s after a cut repeated every 2 s), passes here.go test ./internal/api/... ./internal/telemetry/... ./internal/control/... ./internal/loadpoint/... ./internal/ocpp/... ./internal/sitesim/... ./cmd/ftw/...passes.Not in this PR
Command history keeps 8 orders. With a repeated order stored once, the window holds unless the order changes on every tick; then it still shrinks to 8 ticks. Follow-up: keep orders by age, at least max(ResponseDelay, declared power age), with a larger hard cap.
no_power_responseafter a rise at 2 min 5 s for a charger that applies at 175 s is older than this PR.The alarm sends no push. No kind in
contract/push-catalogue.yamlfits ("Charging stopped early" says the opposite), and the catalogue is paired with ftw-webapp. It needs its own paired PRs.When the charger's own readings are stale, FTW already shows No contact, and Lost control (alarm) after two minutes. The site meter alone cannot tell the charger's draw from other loads without a clear step, so it does not turn No contact into Not following here.
The one-phase draw itself (a three-phase order the charger serves on one phase) and the plan's rate for it: separate PR.
Open PRs #1789, #1762 and #1537 touch the same function; the change here is one guard around the limit return and one flag.
🤖 Generated with Claude Code