Skip to content

fix(control): the EV guard gives the main fuse a heat budget (#1674) - #1701

Merged
frahlg merged 5 commits into
masterfrom
1674-fuse-thermal-budget
Oct 17, 2026
Merged

frahlg merged 5 commits into
masterfrom
1674-fuse-thermal-budget

Conversation

@frahlg

@frahlg frahlg commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Part of #1674. Follows #1695 (v0.147.2-beta.1) and #1699 (the import ceiling as an hourly mean, merged as 404a754).

Problem

Since #1695 the EV hard limit cuts on every one-second reading where a phase is over the fuse less 1 A, or where total import is over the fuse less its margin. A fuse or breaker trips on heat, not on one reading. A heat pump start or an oven switching on cuts the car, which then climbs back 1 A per confirmed step.

Göran's site on v0.147.2-beta.1, 14:20:34Z: one Pixii reading gave 19,174 W of total import while its worst phase was 5.7 A on a 25 A fuse. The total against 16,905 W (25 A × 230 V × 3 − 345 W) cut the car from 10,350 W to 8,081 W, and it took about a minute to climb back:

EV hard limit: site meter over limit grid_w=19174 worst_phase_a=5.7 overage_w=2269 offer_w=10350 cap_w=8081

Four seconds later the meter read 15,334 W with phases 12.3 / 9.5 / 7.9 A, about 6.8 kW. The Pixii's total power and its phase currents can disagree within one reading. We have not found why.

With the old rule in the test harness (25 A, car at 15 A beside 5 A of house load per phase):

  • a 5 s three-phase start to 28 A cut the car from 10,350 W to 7,590 W, and it took about 36 s to get back;
  • a 10 s spike to 29.5 A on L1 cut it to 6,210 W, about 45 s to get back.

Change

  • fuse_heat.go: each phase of the main fuse is a first-order low-pass on I² from the site meter's phase currents: θ += (I² − θ)·(1 − e^(−dt/τ)), τ = 60 s, where I is the last reading's current, held until the next reading. It steps on every new reading, whether or not a car is plugged in.
  • A phase cuts when √θ passes the fuse less 1 A (the existing margin), or at once when its current is above 1.2 × the fuse.
  • The cut takes off what the meter shows over the fuse less 1 A on that phase (see "Cut size" below).
  • Rises and restarts keep the fix(control): the EV guard trusts only the site meter and our own commands (#1674) #1695 rules (room for two steps, the last step seen, 35 s dwell) and never rise during a spike. One addition to the brief: each phase counts at the higher of the reading and its heat. Without it, repeated peaks make the car climb back in the quiet between them and get cut again: on a 16 A fuse, 4, 7 and 10 cuts in 30 peaks instead of one.
  • Where the meter reports valid phase currents, total import no longer cuts for the fuse. The phases measure the breaker itself, and a worst phase at 24 A already keeps the total under the old ceiling (24.5 A per phase with SiteFuseSafetyA 0.5). The clock hour's import limit from fix(control): the import ceiling caps each clock hour's mean import (#1674) #1699 keeps its own rule on total import. Without phase currents, total import against the fuse cuts as before.
  • Meter gap: θ holds between readings and counts a gap at the last current seen, over the real time. The reading after a gap neither fills the gap with its own current nor erases an overload from before it (Codex review). An overload the meter never showed is not counted; a stale meter still stops dispatch after a minute (the existing test passes).
  • Core restart: θ starts at the first reading's I², with no memory. That is conservative only when the first reading is high. A restart right after a spike forgets that spike's heat: from 20 A, at most one more budget, about 26 s at 1.2 × In.
  • No fuse-type setting.

Cut times

25 A fuse, worst phase at 20 A, readings every second. Times run from the first reading at the new current; the charger then needs its own delay, 5–15 s for Easee Cloud, to apply the cut:

Phase current Cut after Cut to
26 A 61 s 8,970 W
28 A 37 s 7,590 W
30 A (1.2 × In) 26 s 6,210 W
31 A, 32 A first reading 5,520 W, 4,830 W
28 A on all phases for 5 s no cut —
29.5 A on L1 for 10 s no cut —
25.5 A on all phases, sustained 73 s, once 8,970 W

Fredrik's 16 A case

A steady 17 A on a 16 A fuse must come down; 10 s peaks to 17 A with 14 A between them may be fine in total. Working limit 15 A, 30 peaks from 14 A, the car at 10 A:

Pattern Model alone: max heat With the car: result
17 A 10 s, 14 A 50 s 14.78 A, never over no cut
17 A 10 s, 14 A 30 s 15.01 A, over at 250 s no cut: the heat passes 15 A as a peak ends, when the meter already shows 14 A
17 A 10 s, 14 A 20 s 15.24 A, over at 98 s one cut at 98 s, settles 1 A lower
17 A 10 s, 14 A 10 s 15.70 A, over at 48 s one cut at 48 s, settles 2 A lower
17 A steady over at 23 s one cut at 23 s, 2 A lower

Boundary rule: a phase cuts when its filtered current is strictly over the limit; exactly on it is inside, as for a reading. The 30 s pattern goes 0.011 A over, so either rule counts it as over; it is not cut because the meter shows 14 A by then, and there is nothing to take off. Each cut takes off what the meter shows over the limit at that second, which is 2 A at a 17 A peak; the car then rises 1 A once the heat has room. The heat never passes 15.2 A.

A conflict in the brief, kept on the safe side

The brief asks for an instant cut above 1.2 × the fuse and also says a 10 s spike to 32 A should not cut (filtered 22.3 A). On 25 A, 1.2 × the fuse is 30 A, so 32 A cuts at once. I kept 1.2 × In: it stays below the 1.25 × In that a gG fuse carries for an hour. TestFuseHeatSpendsBudgetFrom20A checks the filter alone (22.3 A after 10 s at 32 A); TestEVHardLimitCutsAtOnceAboveInstantLimit checks that 32 A cuts at once. If spikes above 30 A should pass, raise fuseInstantFactor (for example to 1.45, a breaker's conventional tripping current). That is Fredrik's call.

Cut size: meter overage, not filtered overage

The brief says to size the cut from the filtered overage. When the budget runs out, √θ has only just passed the limit, so the filtered overage is close to zero. A cut by it leaves the phase over the limit, θ keeps rising, and the guard cuts again every settle time. The same harness over 10 minutes:

Case Filtered overage Meter overage (this PR)
25.5 A, all phases 3 cuts, to 8,280 W; heat peak 24.2 A 1 cut, to 8,970 W; peak 24.1 A
26 A on L1 3 cuts, to 8,280 W; peak 24.4 A 1 cut, to 8,970 W; peak 24.2 A
28 A on L1 4 cuts, to 6,210 W; peak 25.2 A 1 cut, to 7,590 W; peak 24.4 A
30 A on L1 11 changes, down to 4,140 W and back up; peak 26.0 A 1 cut, to 6,210 W; peak 24.6 A

A cut by what the meter shows over the limit brings the phase to the limit in one step, so θ stops rising. The cost: until θ falls under the limit, a spike on that phase cuts by its full size. When the cut leaves the phase below the limit, that takes about 20 s; when it leaves the phase exactly on the limit, θ only approaches it, so it lasts until the house load falls.

Fuse type

FTW does not know whether a site has a gG fuse or a miniature circuit breaker.

  • Long run: the filter holds the current at or below 24 A, 0.96 × In. That is under both one-hour points: a breaker does not trip at 1.13 × In, a gG fuse (16–63 A) does not blow at 1.25 × In.
  • Short run: only currents up to 1.2 × In get a budget. A gG fuse does not blow there within an hour. At most a minute over the limit from cold, 26 s at 1.2 × In from 20 A.
  • IEC 60898 does not bound a breaker's trip time between 1.13 and 1.45 × In. A breaker that tripped at 1.2 × In within about 30 s from 0.8 × In would also trip at 2.55 × In within about 4 s from cold, the fast end of the 1–60 s band the standard allows. Against such a breaker the budget is not safe. The old per-reading cut, which waits for the charger's own delay, was not far from that either.

Tests

  • TestFuseHeatSpendsBudgetFrom20A: 26 A → 61 s, 28 A → 37 s, 30 A → 26 s; 10 s at 32 A → 22.3 A.
  • TestFuseHeatCountsGapAtLastCurrentSeen: a spike just after a 30 s gap and an overload just before one; TestFuseHeatStartsAtFirstReadingAfterRestart.
  • TestEVHardLimitPhaseCutWaitsForHeatBudget: the cut times above end to end, with the cut bringing L1 to 24 A or below.
  • TestEVHardLimitHeatBudgetCarriesHouseSpikes: a three-phase heat pump start past the total fuse ceiling, and 10 s at 29.5 A on L1, leave the offer alone.
  • TestEVHardLimitSustainedOvenCutsOnceAfterBudget: one cut after 73 s, then holds.
  • TestEVHardLimitCutsAtOnceAboveInstantLimit: 31 A and 32 A cut on the first reading.
  • TestEVHardLimitHeatBudgetAcrossMeterGap: a house step during 30 s of silence is cut 61 s after the meter returns.
  • TestEVHardLimitRestartStartsHeatAtFirstReading.
  • TestEVHardLimitHoldsWhenTotalImportDisagreesWithPhases: Göran's 19,174 W reading with the worst phase at 5.7 A holds the 10,350 W offer; with no phase currents the same total still cuts to 8,081 W, as in the field log. With the total fuse cut put back, the first part fails with 8,081 W.
  • TestFuseHeatSixteenAmpPeaks: the model values in the 16 A table.
  • TestEVHardLimitSixteenAmpPeaksCutOnceAndSettle: the 16 A table with the car; it fails (4, 7, 10 cuts) when rises read only the reading.
  • With the old rule (instant factor 0 and the total fuse cut put back), the cut-time, spike, oven, gap and restart tests fail.
  • The fix(control): the EV guard trusts only the site meter and our own commands (#1674) #1695 field cases pass unchanged. TestEVFirstHouseStepWhileBatteryAndEVCharge now allows the car not to be cut at all: the battery takes the 2 kW step within a tick, inside the heat budget.
  • make verify: green on master 404a754 plus these three commits (76 Go packages, vet, build). check-telemetry-reads.sh origin/master: none added.

Not done

🤖 Generated with Claude Code

@frahlg
frahlg force-pushed the 1674-fuse-thermal-budget branch from f3381bb to 2d8d76d Compare October 17, 2026 15:09
frahlg and others added 3 commits October 17, 2026 17:20
Since #1695 the EV hard limit cut on every one-second reading over the
fuse less 1 A on a phase, and on any reading where total import passed
the fuse less its margin. A heat pump start or an oven switching on cut
the car, which then climbed back 1 A per confirmed step.

Each phase of the main fuse is now a first-order low-pass on I² from the
site meter, tau 60 s. A phase cuts once its filtered current passes the
fuse less 1 A, or at once above 1.2 x the fuse. The cut takes off what
the meter shows over the limit, so one cut stops the heat rising. Rises
and restarts still read the instantaneous worst phase. Where the meter
reports phase currents, they replace total import as the fuse check; a
tariff ceiling still cuts. The heat holds through a meter gap and steps
over the real time when the meter returns. After a Core restart it starts
at the first reading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Repeated house peaks on a 16 A fuse (17 A for 10 s, 14 A between) cut
the car, which climbed back in the quiet between peaks and was cut again:
4, 7 and 10 cuts in 30 peaks. Rises and starts now read each phase at the
higher of the reading and its heat, so each pattern cuts once and settles
where the peaks fit. Adds Fredrik's 16 A case as tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
…EV (#1674)

Göran's site, 14:20:34Z on v0.147.2-beta.1: one Pixii reading gave
19,174 W while its worst phase was 5.7 A, and the total against the fuse
cut the car from 10,350 W to 8,081 W. With phase currents the phases
judge the fuse and the offer holds; without them the same total still
cuts to 8,081 W.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
@frahlg
frahlg force-pushed the 1674-fuse-thermal-budget branch from 2d8d76d to db9717c Compare October 17, 2026 15:22
@frahlg
frahlg marked this pull request as ready for review October 17, 2026 15:22
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 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-17T15:25:35.897125Z db9717c Draft marked ready
ℹ️ 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: db9717cf2e

ℹ️ 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/control/fuse_heat.go Outdated
frahlg and others added 2 commits October 17, 2026 17:30
…en (#1674)

The heat model applied each new reading's current to the whole time since
the last one. After a meter gap, one high reading counted as the whole gap
at that current and could cut the car at once, and a low one erased an
overload from before the gap. A reading now stands for the time until the
next: the model integrates the last current seen over that time, then
holds the new one. Cut times, counted from the first reading at the new
current, are unchanged. The 30 s pattern on a 16 A fuse peaks just over
15 A only as a peak ends, when the meter shows 14 A, so it is not cut.

Raised by the Codex review on #1701.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.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