Skip to content

test(site): rig scenarios R1-R5 and judges at the fuse rating (rig stack #1888, #1907, #1908, #1920, #1922) - #1888

Merged
frahlg merged 127 commits into
masterfrom
w0-4-rig-scenarios-r1-r5
Oct 28, 2026
Merged

frahlg merged 127 commits into
masterfrom
w0-4-rig-scenarios-r1-r5

Conversation

@frahlg

@frahlg frahlg commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

This PR now carries the whole rig-judge stack. Each PR was reviewed on its own and merged into its base branch; this one lands them on master as one commit.

PR Change
#1888 P0 rig scenarios R1–R5
#1907 The rig judges "over the limit" above the fuse, not fuse − 1 A
#1908 The rig expects the room the fuse rating gives
#1920 Random runs judged by R2–R7; owner decisions of 28 Oct: a car cut into export counts as FTW's own, and the heat budget caps what events add to a stretch
#1922 R8: a forced battery mode FTW does not dispatch must end within 10 s of Core having the inverter back

Merging master (with #1915, #1930 and the fuse stack) shrank R8's allowance for Core's timers: defaultRetryMaxS is 0 now that #1915 sends the default on the first good poll, the reconnect backoff is read from Core's source (10 s since #1930), and an inverter that returns while Core is down starts its backoff when Core is up again. 207539's 20 s after Core's first read now fails, as #1922 said it would.

Tests: go test -race ./test/site/ (unit tests). Follow-ups: #1941.

Test-only, no changeset.

🤖 Generated with Claude Code

frahlg and others added 2 commits October 27, 2026 14:11
The battery side read a 20 s pool log of EV offers (State.evOffers), while
the EV fuse limit keeps each charger's offers for its declared response
time, or 180 s when it declares none. After 20 s the battery counted a slow
charger's unshown raise as already in the meter and charged into that room;
when the car started, a 25 A site read 32.6 A per phase for 3 s.

Delete State.evOffers and its two writers. evInMeterW and
clampEVBatteryHeadroom read evShownW, each charger's latest offer less
what it may not show yet. The hourly peak in stepEVMeterLimit takes each
charger's highest offer of the last evSettle from the same log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stale meter, battery dropout, the planner floor, a SIGKILL restart and
a start into a running site had no judged rig scenario. Add r1-25 to
r5c-25 with their judges and known lines naming the Wave 1 items (A1,
A3/A4, E1, D6). The rig gains stopFTW(sig), a planner soc_min field, a
charger plugged at start and a delayed Core start. r1-25 is skipped
until device-simulator can silence the site meter.

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

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 🔄 Running since 2026-10-27T13:20:38.749995Z 8fe5dbb 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 and others added 3 commits October 27, 2026 14:26
Delete perPhaseOverageW, perPhaseOverageReadingW and perPhaseReliefW.
The battery fuse guard, the fuse saver and the forced discharge read the
signed phase currents (phaseCurrentsA, with #1849's sum check and dead
zone), count the batteries' own move on each phase, and relieve to the
car's working limit, the fuse less max(1 A, safety_margin_a).

Item A5 of the stable plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first A6 change gave the battery side the EV limit's 180 s guess
for every charger that declares no response time. Only Easee Cloud
declares one, so on every other charger the battery reserved the car's
whole demand on top of a reading that already showed the car, and
stopped charging for 3 minutes after each car start or raise. On the
#1868 shape (rig s18) battery charge stayed at 0 W until 180 s.

evShownW now uses the charger's declared power_max_age_s, and evSettle
when it declares none, as master did. Easee Cloud declares 180 s, so the
slow-charger case stays fixed.

Tests: the slow-charger test now declares 180 s, and a new plant test
runs the s18 shape with a fast charger that declares nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guard took the phase relief from the original request, then the 30 s
fuse hold capped that request and the relief came off again. A charge
limited by one phase switched between 1380 W and 0 W on every dispatch.
Predict the aggregate and the phases from the held targets instead.

The phase reader now takes the meter's raw total from OnlineMeterW, the
same reading as its currents, so an offline meter's cached phases drive
no relief. The batteries' raw power is read in one helper. dispatch.go
keeps its 41 cached telemetry reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg frahlg added the no-changeset PR intentionally exempt from the changeset requirement (dev tooling / non-shipping) label Oct 27, 2026
frahlg and others added 5 commits October 27, 2026 16:01
Review fixes for W0-4:

- r4: Core refuses a battery hold over 1800 s, so the 3600 s hold made
  the run error before any kill. Post a 1800 s hold 300 s before the
  first kill; it outlasts both.
- r2: the site stays under 1.2 x the fuse, where Core cuts the car on
  its documented 60 s heat budget (control/fuse_heat.go), not at once.
  Judge that contract: over 1.2 x the fuse at most the charger's delay
  + 7 s, and the 60 s heat under 1.13 x the fuse. Record the time over
  fuse - 1 A and when Core sends the cut.
- r5a: start the battery at its 5 % floor so the site runs 30 A before
  Core starts, check that as setup, and judge against fuse - 0.5 A,
  Core's own line (as r3 does).
- r5b: drop the D6 known line. The saved plan is younger than
  MaxPlanAge and a fresh plan lands within seconds, so the deadline
  reserve never acts and the run does not reach D6.
- r1, r4: Charge now counts as back only once it runs unbroken to the
  next event, so a stop that starts after the restart is seen.
- README: move the r rows off the s17 line #1875 edits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Core's working limit is the fuse rating less safety_margin_a, which is
opt-in (owner rule of 27 Oct). R3 and R5a now judge the overage above
the fuse, R2 and R4 record it there, and the fixtures put Core's own
line at 25.0 A on a 25 A fuse. New cases hold exactly 25.0 A (passes)
and 25.1 A beyond the time budget (fails) for R3 and R5a.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
R5a's second criterion now starts its clock at the first sample at or
under the fuse, not under fuse - 0.5 A. So a site that dips to 24.8 A
and then holds 26 A for 10 s fails, where the old judge passed. Add that
case so the clock start cannot drift back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Core's working limit per phase is the fuse rating less safety_margin_a,
and the margin is opt-in (owner rule of 27 Oct). The invariants judge,
the overage helper behind S9-S11, S3, S8 and S15bat now count a phase as
over the limit only above the fuse rating. The fuse - 1 A and
fuse - 0.5 A metrics go; the I2 and back-under metrics read at the fuse.
Time budgets stay as they were.

This only loosens the judges, so Core that still holds In - 1 A passes.
Room expectations (S1, S12, the random roomA) stay at fuse - 1 A for now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
S1 targets floor(fuse - house) in 1 A steps (8 970 W on 16 A with a 3 A
house, was 8 280 W). S12 counts a cut on room alone while every phase is
under fuse - 0.1 A and records fuse_limit with 6 A of room under the fuse.
The random judge measures Charge now room against the fuse, with the
car's 6 A minimum as the threshold. The stack pins safety_margin_a to 0
so the rig does not depend on Core's default.

This tightens the rig: it passes only once Core works to the rating.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg and others added 15 commits October 27, 2026 17:56
…e house

A run on a Core that stops at fuse - 1 A gave the car 9 590 W with the
battery at -5 000 W, above the 8 970 W target. S1-16 does not catch the
old line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… safety_margin_a

The EV phase limit was In - 1 A, fixed in code. It is now fuseLineA():
In - safety_margin_a, with no floor. A rise moves one step once it fits
under the line, a start takes all the room, and battery charge beside a
car uses the same line. The hour's import limit keeps its own margins.
Starts and per-phase cuts round down to whole charger amps, since
chargers round offers to the nearest amp.

Step 3 (PR b) of the fuse-at-rating plan (owner rule of 27 Oct).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…of EV room

Two review findings on the fuse-line change:

- A cut rounds to whole charger amps only when a new cut is needed. When
  cuts on their way cover the overage, the offer stays as it is. Rounding
  a kept step-table offer took the car 6200 -> 5500 -> 4800 -> 0 W in 3 s
  on a 0.6 A overage while a 180 s cut travelled.
- Dispatch records the discharge the fuse guard adds to each battery, and
  an EV rise or start does not count it as room. Before, the car rose into
  the relief and the battery fed it 650 W at the line for as long as the
  house load stayed.

The noisy-line sim test now pins the slow one-step cycle at an exact fit
(at most 5 cuts an hour, never a stop) and no moves with part of a step
left, instead of allowing 45 moves an hour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Delete perPhaseOverageW, perPhaseOverageReadingW and perPhaseReliefW.
The battery fuse guard, the fuse saver and the forced discharge read the
signed phase currents (phaseCurrentsA, with #1849's sum check and dead
zone), count the batteries' own move on each phase, and relieve to the
car's working limit, the fuse less max(1 A, safety_margin_a).

Item A5 of the stable plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guard took the phase relief from the original request, then the 30 s
fuse hold capped that request and the relief came off again. A charge
limited by one phase switched between 1380 W and 0 W on every dispatch.
Predict the aggregate and the phases from the held targets instead.

The phase reader now takes the meter's raw total from OnlineMeterW, the
same reading as its currents, so an offline meter's cached phases drive
no relief. The batteries' raw power is read in one helper. dispatch.go
keeps its 41 cached telemetry reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
phaseFuseReliefW now reads fuseLineA(), the line the car uses since the
EV change, instead of max(1 A, safety_margin_a). There is no floor under
the margin: at margin 0 the battery works to the fuse rating.

Tests return to the rating where they had moved to the 1 A limit, and a
new property test covers In x phases x margin in both directions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replayed from stored inputs; only records whose dispatch changed against
the parent branch were replaced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elow

applyFuseGuard cut a battery charge or discharge by the overage plus
half of safety_margin_a. The guard now predicts from the held targets
and keeps its 30 s hold, so the extra cut only parked the phase below
the line the user set. Cut by the overage only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The changeset said the phase lands on the fuse less safety_margin_a.
On #1891 as it stands, the phase line is the fuse less max(1 A,
safety_margin_a), so at the 0.5 A default a 16 A phase settles at
15 A, not 15.5 A. Say what this PR changes, cutting only by the
overage, without naming a line that depends on step 4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Core now works to the fuse rating unless a site sets a margin. Every
explicit safety_margin_a is kept. A stored value of exactly 0.5 A, the
old default, adds one load-time notice and changes nothing.

The peak-limit error names the margin only when it is above 0, and the
settings field shows 0 with help that says the margin is opt-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The changeset said FTW works to the fuse rating unless a site sets a
margin. On this PR's base every per-phase line still sits 1 A under the
rating; that holds only once #1910 and the step-4 amendment of #1891
land. The changeset now names what this PR changes: the default, and
the whole-site import and export limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1881 (the old base) squash-merged. Conflicts kept the branch's
fuseLineA() log line and sitesim meterNoise field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings #1910, now up to date with master, under #1891. Clean merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…533)

On a 16 A fuse with no margin the battery holds 3 kW of discharge with L1
at 14.4 A; PV drops 1280 W and L1 reads 16.3 A. The relief must bring L1
to 16 A and keep it there while the reading sits at the line with the
battery at its relief power. Without phaseFuseReliefW's own-move count
the relief flips between -3214 W and -3005 W on every tick and L1 crosses
the rating 13 times; with it, L1 crosses once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The random judge called eight nightly seeds on master 83c2b24 wrong in
ways the triage traced to the judge, not to Core:

- R2: a stretch over the fuse took only the windows open at its first
  sample. Every window that opens inside it while it is still allowed
  now extends it, a Core restart included (207530, 207539).
- R3: the battery counted as the 2 s lever while its inverter was
  offline or had no spare power. It is the lever only while it runs,
  is above its floor and its spare power covers the overage; otherwise
  the charger, at max(2, command_response_s) (207530, 207534, 207539).
- R4: a car that starts on the charger's own default, or on an order
  still on its way, is a step FTW did not take, answered by the charger
  from draw start + max(2, command_response_s) + 5 s. A stretch is
  FTW's own only after FTW's last change to the car's order was a rise,
  or its battery order moved toward import (207532, 207534, 207537).
- R5 (owner rule): between the fuse and 1.2 x the fuse a phase counts
  only once Core's 60 s I2 heat is over the fuse too; above 1.2 x at
  once (207536). r3 and r5a take the same heat budget (#1888 review).
- R6: idle seconds with room under Charge now are counted from when the
  room appears and may last evRestartDwell + command + startup + a tick
  (+ 2 s meter and tick + 3 s). The count starts again on a Charge now
  that changes the request, on each FTW cut and when Core answers after
  a restart. Only meter and charger faults excuse the car.
- R7: the floor judges FTW's own orders (status.dispatch), not the
  inverter register, against max(inverter floor, planner soc_min) with
  no +0.5 %.

TestJudge now also judges the random seeds a results directory holds,
from each run's plan.json, so a nightly run can be judged again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg and others added 21 commits October 28, 2026 09:30
Two regressions against #1913 from the final review.

A car the loadpoint has not seen on three phases may draw on one phase
Core does not know. The alone rise (aloneA) missed its landing when any
other phase moved a step in the same reading, so the whole raise counted
as still to come and C4 cut a car with every phase under the line, at
most session starts with a 10 s meter. riseAheadA now takes the largest
rise for such a car, as for a one-phase car whose phase is not known,
and the smallest only once the car is seen on three phases.

A one-phase order never knows the car's phase, so a house load moving
alone on any phase read as its cut landing and took the cover off a cut
still on its way. A one-phase move now counts only on a known car phase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 5 s clock allowance let the restart credit count a charger sample
taken 5-7 s before the site reading. A car that stopped after that
sample (a fast charger applying the 0 A sent while the meter was stale,
or the car pausing) still counted, the battery ended the discharge the
car's return needed, and L2 reached 21 A on a 16 A fuse when it resumed.

Core now judges a sample by when it first arrived on Core's clock. It
counts as in the reading only when it arrived evMeterDelay (1 s) before
the reading, and no more than one control interval before that. A sample
already there when Core began to watch the charger arrived at a time
Core does not know, so it never counts. The charger's stamp only tells
samples apart, so a charger clock behind Core's no longer matters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At margin 0 the car in s8-60-25 settles at the rating, as the owner's
rule says, and the rig's old 1 A hidden margin failed it: 271 s above
24 A against 67 s allowed, with no second over 25 A. siterig/full has the
same threshold in overage() (s9, s10, s11), the random invariants and
s15bat.

Port #1907's judge hunks: s3, s8, s9-s11, s15bat and the invariants now
count time over the fuse rating. TestS8JudgesAtTheFuse and #1907's
TestInvariantsJudgeAtTheFuse fail on the old judges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xport is FTW's own

Owner decisions of 28 Oct 2026 for the random judge:

- A car cut FTW sends that pushes export over the fuse is FTW's own, as
  a battery move toward export already was: after a step's meter delay
  and before the stretch, or while the stretch is over the fuse.
- What events add to a stretch (R2 growth) stops at the fuse's heat
  budget: once a phase's 60 s heat passes the fuse, Core's cut gets its
  lever's time and no further event extends the stretch. A window that
  opens after the allowance ran out, with the current under the fuse
  since, is a new step and its cap runs from it. This closes E3 (small
  real rises chaining a stretch's allowance).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(config): fuse safety_margin_a defaults to 0
fix(control): after a restart the battery credits the car the meter shows
fix(control): the car comes before battery charge
fix(control): a battery charge rise fits each phase before it is sent
fix(control): a landed EV cut covers nothing later; a raise that no longer fits comes off
fix(control): battery fuse cut lands on the line, not half a margin below
fix(control): battery fuse guard reads each phase with its direction
Joins #1878's split of fuse relief (fuse part, import-limit-only part)
with the stack's per-phase relief: fuseReliefPartsFromSnapshot takes the
phase sample and returns both parts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg and others added 6 commits October 28, 2026 11:12
test(site): R8 counts a forced battery from when Core has the inverter back
test(site): the rig expects the room the fuse rating gives
test(site): the rig judges "over the limit" above the fuse
test(site): judge random runs by rig items R2-R7 of the 27 Oct triage
The judge stack (#1888, #1907, #1908, #1920, #1922) keeps its own
random_test.go; the fuse stack had only copied #1907's change into it.
rig_unit_test.go keeps the judge side and adds the tests only the other
side has (s18 and s8 at the fuse, the planner mode check from #1942).

With #1915 and #1930 on master, R8's allowance for Core's timers shrinks:
defaultRetryMaxS is 0, the backoff is read as 10 s, and an inverter that
returns while Core is down starts its backoff when Core is up again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	go/test/site/random_test.go
#	go/test/site/rig_unit_test.go
@frahlg frahlg changed the title test(site): add P0 rig scenarios R1-R5 test(site): rig scenarios R1-R5 and judges at the fuse rating (rig stack #1888, #1907, #1908, #1920, #1922) Oct 28, 2026
@frahlg
frahlg merged commit c63568b into master Oct 28, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changeset PR intentionally exempt from the changeset requirement (dev tooling / non-shipping)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant