Skip to content

fix(loadpoint): a charger is never asked for more current per phase than it supports - #1791

Merged
frahlg merged 1 commit into
masterfrom
fix/charger-phase-current-cap
Oct 21, 2026
Merged

frahlg merged 1 commit into
masterfrom
fix/charger-phase-current-cap

Conversation

@frahlg

@frahlg frahlg commented Oct 21, 2026

Copy link
Copy Markdown
Member

Problem

When Core sends phase_mode: 1p to a charger that can run three phases, the
order's max_amps_per_phase is the site fuse. Nothing in Core knows the
charger's own current per phase. In surplus mode the day's 1p lock hands back
the whole step table, so a 16 A charger on a 25 A fuse is offered 18–25 A on
one phase. The simulator run of 21 October (case s5s, Easee steps, phase mode
auto, 9.3 kW surplus, a plan with no PV) showed OCPP numberPhases: 1 at
18–25 A; by the investigation, Easee Cloud gets phaseMode=1 and then
dynamicChargerCurrent up to 25 A. Charge now on 1p does the same: in the
new test, a 7,360 W 1p hold on a 16 A charger beside 3 A of house load goes
out at 4,830 W, 21 A, with only the fuse room as its limit.

The owner's rule: the charger's own limits and the fuse decide. No new
setting.

Change

applyChargerPhaseLimit (in go/internal/loadpoint/manual_limits.go) runs
right after applyInstallationLimits. When the order says 1p, the
loadpoint's phase mode is not 1p and the site has three phases, it lowers
max_amps_per_phase to the charger's current per phase. applyCurrentCeiling
then cuts the watts to match and the order carries charger_limit ("Limited
by FTW's charger setting"). It never raises a limit.

The charger's current per phase is the larger of:

  • max_charge_w / (voltage × 3): what the charger takes per phase at its
    maximum on three phases;
  • phase_split_w / voltage (3,680 W, 16 A, when unset): what auto already
    puts on one phase, in Core (PhaseFor) and in the drivers.

Why the larger of the two

The maximum over three phases is the right source for a charger that switches
phases. The split alone gives the 32 A charger in the simulator's case s9
(22,080 W, auto, no split) 16 A on one phase instead of 32 A.

The maximum alone breaks one-phase chargers left on an empty phase mode. Core
treats an empty mode as able to run three phases: the surplus dwell and the
day's 1p lock both send it 1p. A 16 A one-phase charger has a 3,680 W
maximum, and 3,680 W over three phases is 5.3 A. That is under 6 A, so
applyCurrentCeiling turns every 1p order into 0 W and the charger never
charges from surplus. It also turns the common 11,000 W setting (what the
settings page writes for a new charger) into 15.9 A, which floors to 15 A.

Each of the two is a current the charger already takes, so the larger never
caps a charger below what Core already asks of it. In s5s it lowers 25 A to
16 A.

One case stays limited: a one-phase charger above 16 A (say 32 A, 7,360 W)
left on an empty phase mode gets 16 A on a 1p order. Master already gives
that charger a third of its current on every 3p order (7,360 W over three
phases is 10.7 A), so it needs phase mode 1p, an existing setting. With
1p this change does nothing. With no max_charge_w, the split decides:
16 A by default.

Not changed

The surplus phase choice, the step tables and go/internal/control (except
one test). Next-beta work stays out: the forecast-driven 1p lock (s5s still
never reaches 3p on a fresh box), the empty step table (s5) and the late
readback.

Tests

New, in go/internal/loadpoint/controller_phase_choice_test.go:

  • TestSurplusOnePhaseOfferStaysWithinTheCharger: the s5s repro (surplus,
    auto, Easee steps, 9.3 kW live surplus, plan with no PV). No order above
    16 A per phase; the 1p lock charges at 3,680 W. Second row: a 16 A one-phase
    charger on an empty mode keeps 3,680 W on the same lock.
  • TestChargeNowOnOnePhaseKeepsTheChargersCurrent: Charge now on 1p.
Charger Fuse Master This PR
16 A, three-phase, auto 25 A 4,830 W, 21 A, fuse_limit 3,680 W, 16 A, charger_limit
16 A, three-phase, empty mode 25 A 4,830 W, 21 A, fuse_limit 3,680 W, 16 A, charger_limit
32 A, three-phase, auto 40 A 7,360 W, 32 A 7,360 W, 32 A
32 A, one-phase, 1p 40 A 7,360 W, 32 A 7,360 W, 32 A
16 A, one-phase, empty mode 25 A 3,680 W, 16 A 3,680 W, 16 A

Each rule alone fails a row, checked by swapping it in: the split alone
gives the 32 A three-phase charger 3,680 W. The maximum alone gives the
16 A one-phase charger on an empty mode 0 W, in surplus and on Charge now;
with the guard that skips limits under 6 A, its 1p orders keep the 25 A
fuse as their limit.

Changed: ev_fuse_phase_room_test.go:217
(TestEVFuseOnePhaseOffersCountOnTheirPhase). The hold asks 5,520 W on 1p,
24 A, of roomTestCharger, which is a 16 A charger (11,040 W, steps 6–16 A).
The test wanted at least 4,140 W (18 A) on L1. It now wants 3,680 W (16 A):
the car still climbs from 2,070 W (9 A beside 15 A) once L1 falls, and L1
still stays within 24 A, which is what the test checks.

Closed-loop simulation (sitesim), the s5s repro

Same plant as the rig: 25 A, house 1 A per phase, 10 kW PV, full battery,
surplus only, auto, Easee steps, 5 minutes.

Plan Master This PR
No PV (fresh box) 150 orders, 52 at 1p 4,140 W = 18 A; reasons fuse_limit 150 150 orders, 0 above 16 A; 54 at 1p 3,680 W = 16 A; charger_limit 54, fuse_limit 96
Sunny, or no forecast 3p only, 6–16 A Same

Simulator rig, case s5s

The real FTW binary against device-simulator over OCPP 1.6 and Modbus
(reports/simulatorprov-20261021/rerun.sh, own ports with
RIG_PORT_OFFSET=40000). 25 A, house 1 A per phase, 10 kW PV, full battery,
surplus only, auto, Easee steps, a fresh box, 5 minutes after plug-in.

Master 767b144 This change, 7d8a82f
Profiles sent 150 150
Above the charger's 16 A 55 0
Highest offer 24 A on one phase 16 A on one phase
Offers (A, phases) 10–16, 18, 21, 24 on 1 10–16 on 1
Car, mean after 60 s 3,964 W 3,196 W
Reasons fuse_limit 238 fuse_limit 127, charger_limit 112
Fuse not blown, damage 0 not blown, damage 0
Beta criterion (no offer above the rating, fuse holds) FAIL PASS
Full criterion (charges at ≥ 4,140 W 90 % of the time) FAIL FAIL

The car's mean falls because the simulated charger draws any offer, spread
over three phases: it took 4,140–5,520 W from the 18–24 A orders that a real
16 A charger cannot take. The full criterion still fails because the box
never moves to 3p on a fresh box; that is the forecast-driven 1p lock,
next-beta work.

Commands

go test ./internal/loadpoint/... ./internal/control/... ./internal/sitesim/... -p 2 -count=1
make verify

Both pass on 1467e17, rebased on master with #1783. The rig ran on 7d8a82f, the same change before the rebase. make verify ran with GOFLAGS=-p=2 on a shared laptop: vet, every Go suite and the build are clean.

🤖 Generated with Claude Code

When Core sends phase_mode 1p to a loadpoint that can run three phases,
max_amps_per_phase was the site fuse. The surplus 1p lock hands back the
whole step table, so a 16 A charger on a 25 A fuse got 18-25 A on one
phase (simulatorprov 21 Oct 2026, case s5s).

The order's per-phase limit now stops at the most the charger is known to
take on one phase: max_charge_w over three phases, or the phase split,
whichever is larger. It only ever lowers the limit.

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

chatgpt-codex-connector Bot commented Oct 21, 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-21T16:55:22.661050Z 1467e17 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 c14d31d into master Oct 21, 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