Skip to content

fix(control): phase current takes its direction from the phase's power - #1782

Draft
frahlg wants to merge 2 commits into
masterfrom
fix/fuse-phase-direction
Draft

frahlg wants to merge 2 commits into
masterfrom
fix/fuse-phase-direction

Conversation

@frahlg

@frahlg frahlg commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Draft, next beta (Fredrik, 21 October 2026). This PR ships together with the fix for the charger's late readback in surplus charging (surplus_reserve.go:128, the site simulation's known row "9 kW surplus, readback 10 s late"). On master 8d5c6bf, #1782 alone drops three-phase surplus charging with a 10 s readback from a mean 7,616 W to 1,477 W: master's slow ramp under export hid that bug. Do not merge until the beta is cut and the readback fix is ready to go with it.

Step 4 of the fuse rule in Fredrik's review of 21 October 2026: fuse protection counts the site meter's current per phase with direction, import positive, plus our own raises that cannot show yet. This PR handles the direction. The first of three PRs, then import_limit and one function.

Problem

phaseCurrentsA (go/internal/control/fuse_heat.go) took math.Abs of each phase current. Under export a phase counted as import, and charging the car would lower the current through the fuse. On a 25 A fuse with 20 A of solar export per phase, Charge now never started (fuse_limit). On a 16 A fuse, 8 A of export (about 5.5 kW) was enough to block it. Introduced with #1701.

Change

  • phaseCurrentsA signs each phase by the same phase's power in the reading (l1_w/l2_w/l3_w). P1 DSMR reports current as a magnitude and power with a sign. Pixii signs l1_a itself, also from phase power, and SolarEdge and SDM630 emit l*_w too. Without that phase's power the current counts as import, the safe side.
  • Rises and starts read the level with direction (fuseHeat.levelA). An exporting phase counts at its reading, so new load there has more room: room = limit − (I_p + pending_p). An importing phase still counts at the higher of its reading and its heat, as before.
  • The heat filter keeps I². Only an importing phase can be over for the EV cut (overA), because cutting a car on an exporting phase raises the export.
  • The site simulation's meter now emits signed phase power, as P1 does, also when its currents are unsigned.

Deleted: the math.Abs on phase current in phaseCurrentsA. Net lines are small here; the large deletions come in the third PR.

The battery's share beside a car changes too: evBatteryPhaseSpareW (clampEVBatteryHeadroom) reads worstPhaseA, which now takes each phase's direction, so an exporting phase counts as 0 and the battery's charge can get more room there.

Not changed here: the battery side's fuse paths still read magnitudes. perPhaseOverageW (dispatch.go:3878), the #1536 imbalance proxy phaseImbalanceA (fuse_phase.go:44) and the per-phase relief (dispatch.go:3606, :4468) attribute direction from the total. A probe with L1 exporting 27 A while the site imports made the fuse saver discharge and push L1 to 29.7 A, on master and on this PR alike. The one-function PR (#1789, next beta) replaces them with the same reader.

Site simulation, before → after

Scenario master 6aa082f this PR
export start fails as known: car idle 1 min 7 s with room passes: idle 5 s, peak 20.0 A (export), 0 s over 24 A
export start, unsigned phase currents fails as known: car idle 1 min 7 s with room passes: idle 5 s, peak 20.0 A, 0 s over 24 A
9 kW surplus, readback 10 s late fails as known (surplus_reserve.go:128), idle 24 s with room still fails as known, idle 55 s with room
all other rows unchanged unchanged

The surplus row's known cause is the charger's late readback added back to surplus, not the fuse. Its idle time moved because the fuse no longer holds the car back. That row is not in this PR's scope.

New unit test: TestPhaseCurrentsTakeDirectionFromPhasePower (P1 magnitudes, Pixii signed, no phase power → import).

Evidence

  • go test -p 2 ./internal/control ./internal/sitesim ./internal/loadpoint ./cmd/ftw: pass
  • make verify (with GOFLAGS=-p=2): pass

Real-hardware evidence is still needed: a P1 or Pixii site under solar export, with the car starting on Charge now.

🤖 Generated with Claude Code

Fuse protection read every phase current as a magnitude. Under 20 A of
solar export a phase counted as 20 A of import, so on a 25 A fuse a car
never started, and on a 16 A fuse 8 A of export was enough to block it.

phaseCurrentsA now signs each phase by the same phase's power in the
reading (l1_w, l2_w, l3_w, which P1, Pixii, SolarEdge and SDM630 emit).
Without that power the current counts as import, the safe side. Rises
and starts read the level with direction: an exporting phase counts at
its reading, so new load there has more room. An importing phase still
counts at the higher of its reading and its heat. The heat filter keeps
I², and only an importing phase can be over for the EV cut.

The site simulation's meter now emits signed phase power, as P1 does,
also when its currents lose their sign. Both export rows pass and lose
their known line.

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-21T14:36:25.964648Z 5b587de 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: 5b587de8e6

ℹ️ 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 on lines +133 to +135
signed := math.Abs(*a)
if w := powers[i]; w != nil && finiteFuseValue(*w) && *w < 0 {
signed = -signed

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Normalize phase-current signs at the driver boundary

For meters reporting unsigned phase currents, this introduces sign normalization inside Core control rather than at the driver boundary, contrary to the non-negotiable invariant in AGENTS.md. Consequently, the EV guard interprets a different current convention from other consumers of the same telemetry, such as phaseImbalanceA and perPhaseOverageW, which still use magnitudes. Normalize the phase-current direction in the upstream drivers and update the bundled snapshot, so Core consumers receive one consistent convention rather than independently adapting vendor readings.

AGENTS.md reference: AGENTS.md:L63-L64

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

l1_w/l2_w/l3_w already carry the site sign: the drivers convert them at the boundary (P1 takes each phase's export from its import, Pixii and SolarEdge emit them signed). Core flips no vendor sign here. It combines two driver values, the current's size and the same phase power's direction, as the fuse rule of 21 October says; without phase power the current counts as import. The other Core readers (perPhaseOverageW, phaseImbalanceA) move to this one reader in #1789.

Review of #1782: evBatteryPhaseSpareW reads worstPhaseA, which now takes
each phase's direction, so an exporting phase counts as 0 and leaves the
battery's charge more room. The comment said magnitudes could not label
the worst phase; it now says what the code does. No code change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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