Repository navigation
test(site): rig scenarios R1-R5 and judges at the fuse rating (rig stack #1888, #1907, #1908, #1920, #1922) - #1888
Merged
Merged
Conversation
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>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
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>
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>
…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>
…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>
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>
# Conflicts: # go/cmd/ftw/main.go
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>
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
Merging master (with #1915, #1930 and the fuse stack) shrank R8's allowance for Core's timers:
defaultRetryMaxSis 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