Skip to content

fix(loadpoint): a one-phase car on a three-phase charger is seen, planned and ordered as one - #1801

Merged
frahlg merged 5 commits into
masterfrom
fix/ev-one-phase-car
Oct 22, 2026
Merged

frahlg merged 5 commits into
masterfrom
fix/ev-one-phase-car

Conversation

@frahlg

@frahlg frahlg commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Builds on #1799 and #1802 (merged). Replaces draft #1796.

Problem

A one-phase car, as many plug-in hybrids are, draws a three-phase order's current per phase on one phase only. With the field site's Easee table and #1799, the planner got [0, 4800] W. A 4,800 W order is 7 A a phase, which the car took as 1,600 W, and the charger panel said "Not following" all night. Fredrik (22 Oct 2026): keep the charger on three phases, learn the car's phases from the site meter, plan with what it takes. This must not regress one-phase cars on mixed step tables.

Change

  • Detection (loadpoint/one_phase_car.go): each rise of the session's three-phase order by at least 2 A a phase starts a test. The test compares the site meter's phases with the reading taken before the first raise since the order it measures from. A one-phase car shows one phase rising by about the order's rise (between half of it and 2 A more) while the other two move at most max(1 A, a quarter of that). It must hold for 30 s within the charger's response time (its declared power age, at least 60 s; 3 minutes when it declares none), and the charger's own draw must have changed by about that much since the baseline (0.6–1.5×). A rise on all three phases, with the charger's draw up by about their sum, is a three-phase car and clears the mark. With no reading from before the car drew, as after a Core restart, the charger's own draw is the reference: an unmarked car whose 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. A two-phase car moves two phases and is never marked. A house load does not follow FTW's orders, and with the car drawing nothing the charger's reading rules it out. The order may keep rising during a test, as a plan catching up does; a test whose order falls back is dropped.
  • Mark: State.CarPhases = 1 and CarPhase (1–3) for the session. Unplugging or a new session resets it. fix(charging): carry two-phase cars through plans, current limits and Easee #1792's car profile phases can seed the same field later.
  • Plan: the planner gets the car's one-phase levels, 6 A up to the charger's current per phase, within the site fuse (OnePhaseCarSteps: 1,380–3,680 W at 16 A). The car's own maximum still caps it (LimitPower).
  • Order: the charger stays on three phases. The plan's power is what the car takes, so the order is three times it, which names the current per phase the car should take, within the charger's maximum. A plan ceiling is scaled the same way. The driver contract has no current field: Easee and OCPP turn power_w into amps as power / (V × phases), so this mapping sits in one place, computeCommandForBudget.
  • Evidence: the command carries expected_w, a third of the order. Command evidence compares the car's draw with that, so a car that does what it can is not "Not following". Drivers ignore the field.
  • Feedback: Limited, info: "Charging on one phase" — "The car charges on L1 only, at most 3.7 kW. FTW plans with what it takes." (charging_phase, charging_phase_max_w). It applies only when the car draws its third within tolerance, after the limit guard, a changed setpoint and a response still due: a fuse-limited one-phase car reads fuse_limit.
  • Fuse: unchanged. A three-phase order of X A a phase is counted as X A on every phase, which covers the car's phase. The meter is the only evidence in the fuse path.

Evidence

One-phase car, field table, plan for 4.8 kW #1799 this PR
Planner range [0, 4800] 1,380–3,680 W
Car draws 1.6 kW of a 4.8 kW order, more only as the plan's deficit pushes the order up 3.68 kW
Energy in 1 h (sim) 2.54 kWh 3.52 kWh
Feedback Not following after 1 min 25 s Limited, "Charging on one phase, at most 3.7 kW", never not following over 8 h
  • Field matrix (test(control): field matrix for EV charging and fuse protection #1800) on master with fix(control): a cut on its way covers the overage it was sent for #1802: 39 D rows and one A row, all energy, turn green, and their lines go. No row is new. 338 cases, 31 fail: idle 25 (export 13, readback 10, hold3p 2) and own-raise 6 (relief). None fails fuse, stale, home-use, over-cut, cut, limit, energy, one-phase or verdict.
  • The matrix's verdict check judges the car's draw against the command's expected_w when it carries one, as Core does. It used to judge against the full order and let only one_phase excuse a third of it, which would fail a fuse-limited one-phase car that reads fuse_limit. The matrix made two changes here: a test's baseline is the meter read before the first raise since its reference order, since a 2 s charger has applied it by the next reading; and a charger that declares no response time gets 3 minutes, as the fuse limit counts it.
  • Sim rows: "one-phase car, field table" (marked on L1 80 s after the start, 3,680 W by 7 min as the fuse limit lets the order rise, no fuse invariant broken), "three-phase car, kettle on L1 at the start" and "two-phase car" (never marked).
  • TestPhaseVerdict: car on L1/L3, a car below its order, three-phase, two-phase, a kettle while the car draws nothing, car plus kettle on one phase, not yet applied.
  • TestOnePhaseCarMarkedFromTheMeter: marked after 30 s, cleared by a three-phase rise, reset by unplugging.
  • TestOnePhaseCarPlansItsOwnLevels: reviewer's P4 now plans up to 3,680 W; a 10 A fuse holds it to 2,300 W.
  • TestOnePhaseCarOrderNamesItsCurrent: a 3,680 W slot goes out as 11,100 W 3p with expected_w 3,700.
  • TestOnePhaseCarThroughANight (real controller, command evidence, API): no not_following in 8 h; on fix(loadpoint): planned charging runs on three phases, and room never cuts a running order #1799 it said not following after 1 min 25 s.
  • TestExpectedDrawReplacesTheOrder.
  • go test for api, telemetry, control, loadpoint, sitesim, mpc, config, ocpp and cmd/ftw, and the web feedback tests, pass.

Review findings (independent review with #1802 and #1795)

Finding Fix Test (fails on cdf8bd1)
M1 The charger's whole draw was compared with the meter's rise, so only a rise from about 0 A could mark a car compare the change in the charger's draw since the baseline TestOnePhaseCarMarkedOnALaterRise
M2 The three-phase verdict had no draw check: a heat pump starting at 2 A a phase during a 2 A rise cleared a correct mark it needs the charger's draw up by about the three phases' sum TestHeatPumpDoesNotClearAOnePhaseCar
M3 A zero baseline on the first reading and a mark kept in memory: after a Core restart with the car charging, never marked again no test without a reading from before; a baseline-free check on the charger's whole draw TestOnePhaseCarMarkedAfterARestart (also: three- and two-phase cars are not marked)
Finding Fix Test (fails on 578119b)
5 The one-phase branch ran before the limit guard: fuse_limit, import_limit, fuse_cooldown, site_meter_stale, setpoint_changed and waiting_response all read as limited/one_phase it runs after the waiting check TestOnePhaseVerdictComesAfterLimits (13 cases; 6 failed before)

The review found the fuse path clean and a wrong mark bounded by the fuse cap, which applies after the ×3.

Limits

  • The meter's phase currents are magnitudes (phaseCurrentsA). With L1 exporting 10 A, a car taking 16 A on L1 reads as L1 falling, so a one-phase car is missed while its phase exports: daytime charging on a site with PV export. This waits for the export fix (fix(control): phase current takes its direction from the phase's power #1782) in the next beta.

  • The mark comes after the first rise has shown, about 80 s at the start of a session in the sim. Until then the car takes the three-phase order's current, as before.

  • The planner learns the car's levels at its next replan; the order already follows the mark.

  • DeadlineNeed (goal at risk) still uses the charger's table.

  • Surplus charging on auto is unchanged.

  • Old clients show the reason code one_phase until ftw-webapp and the app learn it.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 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-22T07:23:12.210993Z 6cbf167 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: 6cbf16702e

ℹ️ 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/loadpoint/one_phase_car.go Outdated
if s.test == nil {
switch {
case orderA-s.orderA >= onePhaseStepA:
s.test = &phaseTest{at: now, until: now.Add(settle + onePhaseHold), baseline: meter.A, fromA: s.orderA}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Capture the phase baseline before sending the increased order

If the charger applies an order and the site meter reports its effect before the next controller tick, this baseline already includes the charging-current rise. TickWithDispatch calls observePhases before issuing the current tick's command, so the increased commandedW is first examined on the following tick. A one-phase car that responds within that interval therefore shows no change against this baseline and remains undetected; the added integration tests avoid this by imposing a 40-second response delay. Retain the meter reading from before dispatch and associate it with the order increase so prompt responses also receive the corrected planning and command behavior.

Useful? React with 👍 / 👎.

Comment thread go/internal/loadpoint/one_phase_car.go Outdated
switch {
case rose == 2:
return -1
case quiet == 2 && finite(drawA) && drawA >= 0.6*big && drawA <= 1.5*big:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Compare incremental charger draw with the phase-current rise

When a test starts while the car is already charging, big measures only the increase from the meter baseline, but drawA is the car's total current. For example, a one-phase car increasing from 7 A to 9 A produces a valid 2 A rise on one phase, yet its 9 A total draw exceeds the 3 A upper bound here and the verdict remains unknown. Consequently, later modest order increases cannot identify a car whose initial start was missed, even with a correct pre-order meter baseline. Store the charger's baseline draw with the test and compare its change with the meter's change, or account for the existing draw separately.

Useful? React with 👍 / 👎.

frahlg added a commit that referenced this pull request Oct 22, 2026
#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>
@frahlg
frahlg force-pushed the fix/ev-plan-three-phase branch from ed759ec to 74603d0 Compare October 22, 2026 08:04
@frahlg
frahlg force-pushed the fix/ev-one-phase-car branch from 6cbf167 to 811d418 Compare October 22, 2026 08:09
frahlg added a commit that referenced this pull request Oct 22, 2026
… 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>
@frahlg
frahlg changed the base branch from fix/ev-plan-three-phase to master October 22, 2026 09:12
@frahlg
frahlg force-pushed the fix/ev-one-phase-car branch 2 times, most recently from e1ab85f to cdf8bd1 Compare October 22, 2026 09:30
frahlg added a commit that referenced this pull request Oct 22, 2026
… 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>
frahlg and others added 5 commits October 22, 2026 11:58
…nned 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>
…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>
… 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>
"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>
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>
@frahlg
frahlg force-pushed the fix/ev-one-phase-car branch from d993c05 to af1288c Compare October 22, 2026 10:02
@frahlg
frahlg merged commit 2e84710 into master Oct 22, 2026
23 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