Skip to content

fix(loadpoint): a three-phase order runs at least 6 A on each phase (#1861) - #1865

Open
frahlg wants to merge 2 commits into
masterfrom
1861-ev-below-phase-minimum
Open

frahlg wants to merge 2 commits into
masterfrom
1861-ev-below-phase-minimum

Conversation

@frahlg

@frahlg frahlg commented Oct 26, 2026

Copy link
Copy Markdown
Member

Fixes the main report in #1861. The second comment there (vehicle SoC confidence from Home Assistant) is not part of this PR, so this PR does not close the issue.

Problem

Field report, v0.155.1-beta.1: a loadpoint with phase_mode: 3p, min = max = 3,800 W and steps [0, 3800] on a three-phase site. A charger cannot run three phases below 6 A each, 4,140 W at 230 V. The car did not charge all weekend, and the reason said plan.

Cause

Reproduced in a controller test on master:

Path Master sends Why
Planned slot (3p) 0 W, reason plan fuseConfig raised the order's minimum to 4,140 W, above the 3,800 W maximum. SnapChargeW returns 0 when max < min.
Planner plans 3,800 W threePhaseCapable was false (max < 4,140 W), so PlanningSteps gave the one-phase table, while Core ordered three phases.
Charge now (3p) 3,800 W on three phases, 5.5 A each manualMinChargeW had no 3p case, so the order skipped the phase minimum.
Same numbers, phase mode unset or auto 3,680 W on one phase Already worked: the charger reads as one-phase (#1799).

Decision from the physics

An order the charger can run is 0, or at least 6 A on each phase it names. On a 3p pin with a maximum below that, the only way to charge is the lowest three-phase level, 6 A each, 340 W above the configured 3,800 W. 6 A is the least current any IEC charger offers, and the fuse clamp still applies after it. This is the workaround the reporter used by hand, and the car then charged. FTW warns about the setting on save and at startup and says what it orders. No new setting.

Where the loadpoint allows one phase and the level fits, the order runs on one phase, as before. Where no valid order exists, the reason says so: below_phase_minimum, never plan.

Change

  • phaseLimits(cfg, mode, site) (manual_limits.go): the range a command on those phases can run. The minimum is 6 A on each phase; a maximum below it rises to it; a step table with no level in reach gets that level.
  • One source for the plan, the fuse budget and the order: fuseConfig, FuseConfigs (so the fuse budget waits for room for the real least order), clampManualPower and PlanningSteps use it. PlanningLimits widens the session's range to the levels the charger can run. A 3p pin counts as three-phase capable.
  • tickConfig: when a three-phase order's own limits cannot run, the whole tick uses the phase limits, so spare solar, a wake-kick, the car's completion order and cap_amps_per_phase name 4,140 W and 6 A too. Every other loadpoint keeps its own limits.
  • SnapChargeW runs at the minimum when the maximum is below it, instead of returning 0. A plan level below the least order runs at that order for as long as the slot's energy lasts (2,070 Wh at a 3,600 W level runs 30 min at 4,140 W, not 34.5).
  • The planner's reserve for Charge now at full power holds the order Core sends (mpc/deadline_directive.go).
  • chargerPhaseMaxA floors at 6 A, so a one-phase car on such a charger plans 1,380 W.
  • PlanningSteps: a whole-amp ladder whose minimum and maximum fall between two whole amps keeps the highest (min = max = 3,800 W on one phase plans 3,680 W, which is what Core orders).
  • Charge now: holdPhaseMode treats a client's 3p below 6 A a phase as the loadpoint's own mode (one phase where allowed). On a 3p pin such a request gets no order and the reason below_phase_minimum. A Charge now at the configured maximum asks for full power, which is the phase minimum when the maximum is below it.
  • below_phase_minimum: limited and warning in /api/control, a manual-status limit reason, and text in the on-box UI.
  • Config warning in LoadpointWarnings for a maximum or table below the phase minimum. It shows on save (Settings → Chargers) and in the plan's warnings, and Core now logs all loadpoint warnings at startup.
  • Charge now slider: a top below 6 A a phase reads and sends 6 A (6 A · 4.1 kW, not 7 A · 3.8 kW).

Behaviour changes

  • A 3p pin with a maximum below 4,140 W charges at 4,140 W, above its configured maximum, with a warning. That holds for planned charging, the deadline reserve, spare solar and Charge now.
  • A step table with no level in reach on the commanded phases orders the phase minimum instead of 0 W.
  • A Charge now request below 6 A a phase that names 3p now runs on one phase where the loadpoint allows it (before: a three-phase order below 6 A that no charger runs). A Stop keeps the client's 3p.
  • The fuse budget's idle-start test uses the least order on the commanded phases, not the configured minimum. A one-phase order starts with room for 6 A instead of the three-phase configured minimum; a three-phase order waits for room for 6 A on each.
  • A plan level below the least order on its phases runs at that order instead of 0 W, for a shorter on-time.
  • A Charge now below 6 A a phase on a 3p pin reads below_phase_minimum ("Below 6 A a phase"), not charger_limit.

Tests

Written first, failing on master:

  • go/internal/loadpoint/phase_minimum_test.go: the snap, planner steps and limits (three-phase car, one-phase car, unset), the controller's order for the old plan, the new plan and Charge now, one phase where allowed, below_phase_minimum, spare solar with the current cap, and a plan level below the minimum keeping the slot's energy.
  • go/internal/mpc/deadline_directive_test.go: the reserve for Charge now at full power.
  • go/internal/config/loadpoint_phase_minimum_test.go: warnings on save, and gone once fixed.
  • api_clamped_no_draw_test.go, api_control_scenarios_test.go, manual_status_test.go, control-feedback.test.mjs, ev-manual-feedback.test.mjs, ev-charge-now-range.test.mjs.

Field matrix (TestSiteSimFieldMatrix): 31 new issue1861 rows. Phase mode 3p on the report's own table [0, 3800] (one-phase level), a three-phase table cut by the maximum, and no table; planned and Charge now; fast and Easee timing; three- and one-phase cars; the deadline reserve; spare solar. Unset on a one-phase charger without a table.

  • master: 31 of 31 fail (energy 13, idle 22)
  • this branch: 385 cases, 0 fail; sitesim_field_known.txt stays empty
  • fieldRoomW now also bounds the expected energy by the loadpoint maximum (never below 6 A a phase). The reviewer checked the 72 planned and restart rows where that maximum is below the charger's 16 A: outside the new rows the bound moves by 0.6 % or less, and no row changes outcome.

make verify: clean ("vet + test + build clean") on both commits. The release Docker context check was skipped: no Docker daemon on this machine.

Browser review

Local rig (sim-ferroamp, Core on this branch, sim-ocpp Easee, 3 × 20 A, the report's loadpoint). Home box not touched.

  • Startup log and Settings → Chargers on save: "Below the phase minimum: charger "rav4" on three phases cannot charge below 4140 W (6 A on each). Its maximum of 3800 W is below that, so FTW charges at 4140 W. Raise the maximum to 4140 W or more, or set the phase mode to auto if the charger can switch to one phase."
  • Charge now: the panel read "Starts at up to 6 A · 4.1 kW"; the charger got 4,140 W on three phases and the card read "Charging at 4.1 kW".
  • A 2,760 W 3p hold: 0 W, "Below the phase minimum" (warning) in "Are we in control?", and "Not charging: 4 A is below 6 A on each phase, and no charger runs lower. Ask for 6 A or more." in the Charge now status.
  • Not seen in the browser: planned charging, since the rig runs without the planner. The controller tests and matrix rows cover it.

Review

Independent review by a Sonnet agent on the first commit, given the code and the issue, not my conclusions.

  • Safety: no fuse or charger-rating hazard found. The raise is capped at 6 A a phase; the fuse cap still wins (a 4,000 W EV cap gives 0 W, fuse_limit).
  • Fixed in the second commit: spare solar, wake-kick and the car's completion still used the raw configuration (0 W with 10 kW of export on the report's table; 3,800 W on three phases without one); cap_amps_per_phase was 5.5 A under a 6 A order; a plan level below the minimum over-delivered the slot's energy; the planner reserved 3,800 W for a 4,140 W Charge now; the UI title "Below the phase minimum" was jargon.
  • Kept: a Charge now hold above a maximum below 4,140 W (3,900 W on 3,800 W) orders 4,140 W, as a hold at or above the maximum asks for full power. For min = max loadpoints the slider has two positions that both read "6 A · 4.1 kW" (the existing min = max slider rule). charging_phase_max_w for a one-phase car on such a charger now reads 1,380 W, which is what it takes.
  • Corrected: the review said Easee turns a 3,800 W three-phase order into 0. easee_cloud.lua rounds to the nearest amp, so that order runs at 6 A there; the sim does the same. A driver that floors would get 5 A and stop. The planned path in v0.155.1-beta.1 (#1799): EV loadpoint with min=max below 3-phase minimum silently charges 0 W #1861 sent 0 W, which no rounding helps.
  • With the loadpoint code put back to master, the reviewer saw every new row fail. No regressions found for normal configurations.

Overlap with open PRs

Follow-ups, not in this PR

  • Unset or auto, min = max = 3,800 W, table [0, 3800], on a one-phase reading: the table's only level is above the charger's 16 A on one phase, so the shared fuse budget finds no step under its cap and orders 0 W with fuse_limit. It fails the same way on master. I left that row out of the matrix.
  • ftw-app: FTWKit/Format/EV.swift maps manual limit reasons and falls back to "The main fuse limits charging right now", so it would blame the fuse for below_phase_minimum. It needs a line of its own.

🤖 Generated with Claude Code

frahlg and others added 2 commits October 26, 2026 13:17
…1861)

A loadpoint pinned to 3p with min = max = 3,800 W asked for less than
three phases can run, 6 A each (4,140 W at 230 V). fuseConfig raised the
order's minimum to 4,140 W above the 3,800 W maximum, SnapChargeW
returned 0 for max < min, and every planned slot went out as 0 W with
the reason "plan". The planner meanwhile got the one-phase step 3,800 W,
and Charge now sent 3,800 W on three phases, 5.5 A each.

phaseLimits holds a command to what its phases can run: the minimum is
6 A on each phase, a maximum below that rises to it, and a table with no
level in reach gets that level. fuseConfig, FuseConfigs, the Charge now
clamp and the planner's steps use it, so the plan, the fuse budget and
the order agree on 4,140 W. A 3p pin counts as three-phase capable.
SnapChargeW runs at the minimum when the maximum is below it.

Where the loadpoint allows one phase and the level fits, the order runs
on one phase, as before. A client's 3p Charge now below 6 A a phase now
goes out on the loadpoint's own mode. On a 3p pin such a request gets
no order and the reason below_phase_minimum, shown in the UI. Save and
startup warn about a maximum or table below the phase minimum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of the first commit found three paths that still used the raw
configuration. On the loadpoint pinned to 3p with a 3,800 W maximum:

- Spare solar snapped to the planner's [0, 4140] within the 3,800 W
  maximum and paused with 10 kW of export; without a table it sent
  3,800 W on three phases. A wake-kick and the car's completion order
  sent 3,800 W on three phases too.
- cap_amps_per_phase came from the raw maximum: 5.5 A under a 6 A order.
- A plan level below the phase minimum ran at the minimum for the
  on-time of the lower level, so the slot over-delivered.

tickConfig gives the tick's order the phase limits when the loadpoint's
own limits cannot run on three phases; other loadpoints keep theirs. A
plan level below the least order runs at that order for as long as the
slot's energy lasts. The planner's reserve for Charge now at full power
holds the order Core sends. Three surplus rows join the field matrix.
The UI title reads "Below 6 A a phase".

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

chatgpt-codex-connector Bot commented Oct 26, 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-26T12:39:38.286604Z 38aa6dd 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.

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