Skip to content

fix(control): an idle car's start reads the phases now, not its own heat - #1829

Merged
frahlg merged 1 commit into
masterfrom
fix/ev-start-reads-the-meter
Oct 25, 2026
Merged

frahlg merged 1 commit into
masterfrom
fix/ev-start-reads-the-meter

Conversation

@frahlg

@frahlg frahlg commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Field case

Home box, 25 Oct 2026, v0.155.1-beta.1, on a 3 × 20 A site:

  • The car stopped from 15 A while L1 read 17.9 A of the 19 A working limit.
  • After the stop, L1 read 4.7 A.
  • Its next start still added the new current on top of the fuse heat its own charging had left (fuseHeat.levelA: max(reading, heat) + pending). The cap stayed at 0 for 84 s, and then gave only 9 A.

#1827 removes the stop that started that night's chain. This PR changes how fast and how high a car starts again after any short stop.

Change

Fredrik's decision:

  • An idle car's start reads each phase's current now, plus the car's new current, against the working limit.
  • The heat filter blocks a start only while its own level is over that limit.
  • The heat keeps its job on the cut path and for a running car's rises.

In code:

  • fuseHeat.startLevelA: per phase, the reading plus our orders the reading may not show yet. The heat counts only on a phase whose filtered current is over the working limit.
  • evMeterRoom.startW / startRoomA: the start's room. stepEVIdleStart reads startW.
  • For an idle pool, the split between cars reads the same room (EVFusePhaseRoomA). Without that change, the loadpoint's own per-phase cap would still hold the start back by the heat.
  • On the start path without a measured hour, the reason label compares the rise level with the start's room.
  • docs/safety.md says the same.
  • Nothing else changes: the cut path (overA), a running pool's rises, the restart dwell and fix(control): an idle car starts on the hour's slack under the import ceiling #1828's own start rule under the import ceiling.

Deleted: nothing. Production code: +35/−8 lines in fuse_heat.go and ev_fuse_budget.go.

Tests

Check master 8e33174 this branch
TestEVIdleStartReadsThePhasesNotTheCarsOwnHeat: stop from 14 A with L1 at 22.9 of 24 A, start 20 s later start 6 210 W after 1 min 7 s (fails) 9 660 W after 21 s
TestStartLevelReadsHeatOnlyOverTheLimit: heat just under and over the limit, with orders on their way, one phase new unit test passes
TestEVIdleStartWaitsForTheHeatOfARealOverage: L1 at 26 A for 5 min, then 8 A start after 1 min 37 s start after 47 s; without the heat gate it would start after 36 s (checked, fails)
Field matrix 340 cases, 29 fail (idle 23, own-raise 6) 30 fail (idle 23, own-raise 7)

The one new matrix row is pr1763/25A/3p/3p-15A/easee120s/car3p/6-2-2/pause+battery own-raise relief. It is the site where the battery charges from the plan while the car charges, with an Easee answering in 120 s.

  • The earlier restart, at 22:07:37, changes the timeline.
  • At 22:11 the kettle comes on, the battery relieves L1 and then ends its relief, and L1 reads 24.0 A for more than 3 s.
  • It now fails at the same second, with the same phases (24.0/14.0/14.0 A, battery 0 W, car 8 300 W), as its three listed siblings with other charger timings. So I gave it their label, relief: the battery ends its relief before Core's own changes show. On master it passed on timing.

Random-house property test

TestSiteSimRandomHouse judges five fixed random houses for an hour. Two edits to the test:

  • When a house step happens. In that loop the house load is set after the second's control tick, so it flows from the next plant second. The judge dated it to the tick's own second. A start Core sent in that tick, against a reading from before the step, therefore counted as the step's cause. Seed 1 hit exactly that: Core started at 9 A with L1 reading 8.9 A, then the house stepped to 12.4 A. The judge now dates the step to the next plant second. With master's control code this changes no result.
  • Seed 2's known line is removed. Seed 2 no longer reaches the case where a landed cut still covers the next overage. That cost has not changed and is still pinned in TestEVFuseLandedCutCoversOnlyItsRun.

The trade-off, per seed (1 h each):

Fuse, charger car idle with room energy to the car time over the working limit peak phase fuse damage, max
16 A, 40 s (32 steps) 11m16s → 7m2s 0.7 → 1.2 kWh 2m40s → 4m55s 19.9 → 23.9 A 0.45 → 0.59
16 A, 40 s (44 steps) 9m18s → 6m49s 0.9 → 1.1 kWh 7m21s → 7m44s 20.2 → 20.7 A 0.47 → 0.49
20 A, 2 s 14m18s → 10m58s 2.1 → 2.6 kWh 10m15s → 11m53s 25.1 → 29.9 A 0.41 → 0.42
20 A, 5 s 16m18s → 11m33s 1.5 → 2.0 kWh 3m8s → 7m30s 26.9 → 28.9 A 0.44 → 0.52
25 A, 40 s 17m7s → 15m16s 2.1 → 2.5 kWh 11m43s → 12m47s 37.8 → 37.8 A 0.60 → 0.60

The car charges more and waits less. In exchange, more of the random house steps land on a car that is already charging, so a phase spends more time over the working limit before Core's cut reaches the charger. In seed 3, on a slow charger, that takes the peak to 1.49 × In for about the charger's 40 s.

The fuse never blows (damage at most 0.59 of 1), and every invariant holds. These are outside steps the cut path answers; no start of Core's own put a phase over the limit.

Independent review (Sonnet)

The reviewer found no case where a start puts a phase over the limit by Core's own doing, and confirmed the decision as stated. It also found:

  • Cycling house load (a consequence of the decision). The heat no longer remembers recent house peaks for an idle start. In a scratch test, L1 cycled 4 A for 40 s, then 20 A for 20 s, on a 25 A site for 30 min. This branch made 30 starts and 30 instant cuts, with 150 s over 1.2 × In; master made 7 and 7, with 35 s. The peak was 35 A on both. Every cycle the start lands at the trough and the house step 5 s later. That is the worst case, with a period near the 35 s restart dwell. If it matters, the dwell is the place to damp it, not the heat.
  • Two chargers, one running. A pool with one car running is not idle, so a second car's start still reads the heat. That fits "a running car's rises keep the heat", but not a per-car reading of "an idle car's start". The harness has one charger; this was not run.
  • Random-house coverage. Over 300 seeds the judge's new date removes 11 own-raise false positives on master and 6 on this branch. In each one, recomputing without the house step leaves every phase within the limit. The "limit" kind still fails on 13 of 300 seeds on master and 10 here. None of the five pinned seeds now reaches a known failure.
  • The new matrix row fails for the same 0.03 A over 24 A at 22:11:12 as its siblings. This branch's earlier, higher restart is what exposes it.
  • Fixed after review: stale comments on levelA and phaseRoomA; docs/safety.md; the start path's reason label; the heat test now compares I² as overA does; the changeset now mentions the restart wait; and the new unit test for startLevelA.

Open drafts touch the same files: #1782, #1789 and #1792 in ev_fuse_budget.go, and #1789 also in fuse_heat.go. #1789 read the heat for starts, which this reverses as Fredrik decided.

make verify passes.

Hardware

Not yet run on the box. Real-hardware evidence is still needed: a stop and restart on the home box's busy L1.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 25, 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-25T09:34:03.249168Z d6b10c5 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.

After the car stopped on the home box on 25 Oct 2026, its start added
the new current to the fuse heat its own charging had left: the cap
stayed at 0 for 84 s with 14 A of room on the reading, then gave 9 A.

As Fredrik decided: an idle start reads each phase's current now plus
the car's new current against the working limit. The heat filter
blocks a start only while its own level is over that limit, and keeps
its job on the cut path and for a running car's rises. The split
between idle cars reads the same room.

The random-house judge now dates a house step to the plant second it
takes effect, after the control tick that set it. For the five pinned
seeds on master that changes no result; it stops counting a start sent
before a house step as the step's cause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg force-pushed the fix/ev-start-reads-the-meter branch from d6b10c5 to 76f42dd Compare October 25, 2026 10:35
@frahlg
frahlg merged commit dc98087 into master Oct 25, 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