Repository navigation
fix(config): fuse safety_margin_a defaults to 0 - #1914
Merged
Merged
Conversation
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>
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. |
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>
This was referenced Oct 27, 2026
Merged
Rebases #1914 onto the top of the fuse stack. The one conflict was the margin in the site sim's boot; it now reads simFuseSafetyA. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
simFuseSafetyA follows the config default, now 0. fitScenario takes the room under the line and sets the house from simFuseSafetyA, so its exact fit stays twelve whole steps at any margin. "charger's own load balancer" passes at margin 0, so its known line goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the stack under it, the default moves the car's and the battery's per-phase line as well as the site totals. 0.5 A stops being the default, so #1910's changeset no longer calls it that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frahlg
changed the base branch from
c-battery-fuse-no-buffer
to
ev-base-after-restart
October 28, 2026 00:04
At the 0 A default the scenario passes only because the charger's own balancer sits at 25 A, the rating and so the line. The bug is still in Core: a cut comes off an offer the car does not draw. Two known variants keep it tracked and turn red once it is fixed: the stored 0.5 A margin (over 24.5 A for more than 11 s) and a balancer at 25.5 A at margin 0 (over 25 A for more than 11 s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the current tips of #1910, #1891, #1913, #1923, #1924, #1925 and #1926. The only conflict was #1910's changeset: it now carries #1910's new sentence on an offline battery's relief, and keeps this branch's "a 0.5 A margin" wording, since 0.5 A is no longer the default here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…move The SiteFuseSafetyA comment read as if the margin were needed by default; it now says zero is the default and when to set one. The changeset named "the site's total import and export limits", which reads as the hourly import_limit; that limit does not move. It now names the fuse's import and export ceilings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mutation that restored a hidden 0.5 A margin at startup passed every suite. This test pins the value newControlStateFromConfig hands Core. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At margin 0 Core holds the phases at the 16 A line. Rig s18-16's 0.1 A house step at small_step then lands over the line until Core cuts: on fdfbfec the phase read 16.101 A for 3 s and Core cut the battery 4851 -> 4719 W within 2 s. The criterion "own commands stay inside the rated fuse" counted that stretch as Core's and failed. The owner's rule judges an overage between In and 1.2 x In by the I2t model. The criterion now skips the 10 s after the step, the same gap the minor window already leaves. On the fdfbfec record Core's own commands peak at 16.008 A before the step and 16.005 A after it, inside 16.01 A. 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>
3 tasks
frahlg
added a commit
that referenced
this pull request
Oct 28, 2026
…1913, #1923, #1924, #1925, #1926, #1914) * fix(control): one log of FTW's own EV offers 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> * fix(control): battery fuse guard reads each phase with its direction 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> * fix(control): battery waits only for a declared charger response time 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> * fix(control): fuse guard reads phases after the hold caps the targets 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> * fix(control): car and battery-beside-car work to the fuse rating less 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> * fix(control): keep covered offers whole and leave battery relief out 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> * fix(control): battery fuse guard reads each phase with its direction 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> * fix(control): fuse guard reads phases after the hold caps the targets 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> * fix(control): battery relieves to the fuse less safety_margin_a 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> * test(control): re-record golden records at the fuse less safety_margin_a 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> * fix(control): battery fuse cut lands on the line, not half a margin below 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> * docs(changeset): name the limit the battery cut lands on 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> * fix(config): fuse safety_margin_a defaults to 0 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> * docs(changeset): say what the default change moves on its own 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> * test(control): relief stays at the line after a PV drop (C3, seed 207533) 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> * test(control): phase relief reads each battery live, in the decision's reading The merge of #1876 moved stopPhaseReliefW's live reads into phaseSample.moveW. Pin it: a battery that went offline, moved on to a newer reading or has no reading gives no phase relief. With the cached RawW read #1891 had, the offline and newer-reading cases relieve 2310 W and the no-reading case counts a 2000 W charge from 0 W. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): battery relief holds when the battery's reading lags the meter The phase relief counts the battery's move from its own reading. When that reading lags the meter by a tick, the meter already shows the relief while the reading does not, so the relief came off; on the next tick it went back on top of the reading's catch-up. In the 207533 shape (16 A, margin 0) with a one-tick lag the order ran -3214, -3005, -3422, -3005, ... -5717 W and L1 went back to 16.3 A on every other tick: 13 crossings. A move from the battery's last command (the one it got in the last dispatch) may now not pass the room the meter shows either. That bound only tightens, and a phase already over the line gives it no room, so it never asks for more than to stay at the last command: a battery that falls short of its order does not wind it up. With lag 1 or 2 the order holds -3214 W with L1 at 16.0 A and one crossing, on the PV step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): a landed EV cut covers nothing later; a raise that no longer fits comes off The site meter now tells Core how much of a charger's offers it already shows. A move shaped like an order landing (every phase alike, or one phase alone for a one-phase car) moves that level toward the latest offer. A cut the meter shows no longer covers an overage that an outside step causes after it, and no longer nets against a raise sent after it. Where a landing and a house step fall in one reading, a cut covers a phase that has risen since only up to the overage it had then. C4: our own rise on its way, as the meter tells it, is judged as the reading it will make. Above 1.2 x In, or over the line on a phase whose heat budget is spent, it comes off on this reading, down to the line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): a one-phase house move is not the car's cut landing seeOffers kept one shown level for every phase, so a house load moving on one phase alone read as a three-phase cut landing and took the cut's cover off the phase it was sent for. With a 40 s charger Core then stacked a second cut and left L1 2 A under the line for over 2 min. The shown level is now kept per phase: a three-phase order moves every phase alike, and a phase that moves alone moves only its own level. The loadpoint now reports a car it has seen draw on all three phases as CarPhases 3. For such a car a one-phase move is never the car, so a house step on one phase while a raise travels no longer hides the raise, and C4 cuts on that reading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): a battery charge rise fits each phase before it is sent The battery fuse guard judged a rise from the battery's own reading and the meter's phases, so it took a rise the meter did not show yet as already shown. With the meter a poll behind the battery, a PV surplus that grew each tick stacked rises past the line, then the relief cut them back: 16.4 A on a 16 A fuse and a charge that rose and fell (C2, seeds 207530, 207535). fitBatteryRisesToFuse caps charge above the battery's latest order to (fuseLineA - worst phase - own unshown rises) x V x phases. The unshown rises are the battery's orders of the last evSettle and the chargers' raises, counted as evShownW counts them, so the car comes first. Holding an order is never a rise and the check never cuts; an overage stays with the relief. No margin and no floor: the order ends at the line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): battery rise check counts slow batteries and direction-less phases A battery order now stays unshown until evSettle after the battery's own reading reaches it, so a battery that ramps slower than evSettle no longer gets a second rise on top of an order the meter cannot show yet. On a meter with phase currents but no phase power while the total exports, a phase counts as export only when the other phases cannot carry the export alone; any other phase counts as import. Before, every phase flipped to export, so a three-phase charge could rise into a phase that imports beside two exporting ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): the car comes before battery charge With battery_covers_ev off, dispatch took the car out of the meter reading before the battery decided. The battery then saw the PV the car was using as export and charged from it while the site imported for the car (rig seeds 207535, 207539, 207530: about 10 kW import). The battery now charges only from the surplus the meter shows after the car. Where the house alone has surplus, the battery sees the meter with the car included, and any discharge stops at once, since it would feed the car. A planned PV charge on the energy path takes only the surplus the car leaves; a planned grid purchase keeps its charge. Cover off still never discharges the battery for the car. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): peak shaving drops a running charge for the car Manual peak_shaving gave zero error for any import under the peak limit, so a battery charge that was running when the car started held while the site imported for the car, with battery_covers_ev on or off. When the site imports while the car and the battery both charge, the charge now comes down by the import, to 0 at most. Under the peak limit the battery still never discharges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): after a restart the battery credits the car the meter shows After a restart, the 0 A orders FTW sent while the meter was stale counted as the EV base the meter shows. With the car still drawing 8.2 kW from before, a new 6.2 kW order then counted as a 9 A rise to come, and the battery was held in discharge for the car (seed 207532). Until a charger's first order of this run can have landed, the battery side now counts the charger's live measured draw as in the reading, never above the latest order and never above the consumption the site meter shows. The car's own limit does not read it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): credit a found car only from a current charger sample Review of #1926 found two ways the restart credit let the battery end a discharge into room the car's pending rise still needs: - The charger reading counted with no check that it was current. A held reading after a failed poll, or a sample older than the site reading, could show a car that a fast charger had already stopped. The draw now counts only when the last poll succeeded and the charger measured it within one control interval of the site reading. - The consumption bound subtracted the smoothed battery, PV and V2X values, which lag an output that just fell. It now takes the higher of raw and smoothed for each, so the bound stays at or below what the meter shows. New tests pin both, plus the shown-offer subtractions and a window that outlasts evSettle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(control): run the site sim at the new 0 A default margin simFuseSafetyA follows the config default, now 0. fitScenario takes the room under the line and sets the house from simFuseSafetyA, so its exact fit stays twelve whole steps at any margin. "charger's own load balancer" passes at margin 0, so its known line goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changeset): say what margin 0 moves once the fuse stack lands With the stack under it, the default moves the car's and the battery's per-phase line as well as the site totals. 0.5 A stops being the default, so #1910's changeset no longer calls it that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(control): keep the load-balancer bug as a known line above the line At the 0 A default the scenario passes only because the charger's own balancer sits at 25 A, the rating and so the line. The bug is still in Core: a cut comes off an offer the car does not draw. Two known variants keep it tracked and turn red once it is fixed: the stored 0.5 A margin (over 24.5 A for more than 11 s) and a balancer at 25.5 A at margin 0 (over 25 A for more than 11 s). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): EV room reads battery relief through live telemetry evFuseReliefW read each battery's cached RawW, which survives a driver going offline. It now reads Store.OnlineRawW. A battery with no live reading counts its whole recorded relief, so the car never builds on discharge Core can no longer hold. fuseSaverEarlyExit read each battery's SmoothedW a second time to record relief. It now records against the live snapshot the fuse-saver decision already took (fleetAtLivePower), so it adds no read of its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): an offline battery's fuse relief ends after evSettle With no battery online, ComputeDispatch returns before it records relief, so batteryFuseRelief kept its last value and evFuseReliefW counted all of it for as long as the battery stayed offline. The car was held up to the whole relief below the line. The relief now counts in full until the default mode Core sent at the transition can have reached the meter (evSettle), then ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): each offline battery's fuse relief ends after evSettle The expiry in 8150b45 ran only when no battery was online. With a second battery online, an offline battery's relief stayed until a recorded dispatch rewrote the map: never while the legacy modes sat in the deadband, and on the first tick when one did record, so the car could rise onto discharge Core ordered a second earlier. Each offline battery now has its own clock: its relief counts whole for evSettle and then ends, on every dispatch path. A battery back online leaves the clock, so its next offline period counts whole again; a test now covers that reset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): battery phase relief neither pins mixed phases nor winds up The last-command bound held the battery at its last command when one phase sat at the import line and another was over the export line: the guard cut the discharge for the exporting phase and the bound put it back. Now the bound gives way on one side while any phase is over the other side's line, and phaseFuseReliefW's least-over move decides. When the meter does not show the battery's move (a frozen meter, or a worst phase a one-phase battery cannot reach), each tick added the same overage again and the order ran out to the discharge limit. Now the relief toward the worst phase stops at the last command once the batteries' raw power has moved one ampere per phase since the worst phase last moved by half an ampere. One ampere is the step in which P1 meters report phase current. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): battery phase anti-windup stops only when the meter cannot show the move phaseUnresponsive stopped the relief whenever the batteries had moved 690 W and the worst phase had not moved half an ampere. On a live meter that also happens when a load cancels the move (a balanced load starting with the relief step, or a slow rise on the worst phase), and when the battery's reading catches up with an order the meter already showed. The relief then held over the line with no end. Now it stops only when the meter repeats its phases exactly while the batteries moved one ampere per phase, or when another phase moved by twice the balanced share of that move (a battery off the worst phase). The repeat is measured from the reading the meter last changed in, so a meter that freezes on any reading is still caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): battery off the worst phase goes back, and a stale reference ends Fourth review of the phase anti-windup (c066892): - The worst phase's reference had no end while that phase sat over the line. A relief that follows a slow drift keeps it there, so the reference stayed while the battery moved hundreds of watts, and a load that later switched off on another phase read as a battery off the worst phase. The relief then held L1 up to 0.49 A over the line with no time limit. The reference now also ends when the worst phase is at or inside the line, or when another phase becomes the worst. - An off-phase verdict no longer holds the last command. The batteries go back to their power when the worst phase last moved: what they moved since did not reach it. A one-phase battery no longer holds its own phase at -17.97 A for the whole overage (and -19.83 A, past 1.2 x In, when PV returns). A false verdict on a balanced battery moves the worst phase by at least one ampere on the next reading, which ends the reference, so it costs one tick instead of an open-ended hold. The verdict holds until the reference ends, so the relief does not go out again once the battery is back. - The frozen-meter test now also compares the meter's total. A P1 meter reports whole amperes, so a live meter can repeat its phases while its total moves; that latched the relief at 1.11 x In. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(control): the drift test also wants no verdict on the load change With L1 at the line ending the reference, no off-phase verdict comes when the load on L3 switches off. Without it, the step back still ends the hold, but with L1 at 16.8 and 17.7 A for two ticks; the test now sees that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(control): our battery order's move is not the car's cut landing The guard in seeOffers that stops reading the meter while our battery order moves had no test; removing it left every test green. A battery order that lands and goes back while a cut travels to a 40 s charger now must not lead to a second cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(control): the car-first discharge stop has its own test Removing the PI path's immediate discharge stop left every named test green; only the golden replay caught it. A peak_shaving loop with battery_covers_ev off, starting from a 2.9 kW discharge while the house alone has surplus, now requires that no order discharges into the car. The comment on the plan back-off said the plan's grid leaves out the car. It counts the car it planned; the comment now says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): the battery rise check reads only live battery power fitBatteryRisesToFuse, unshownBatteryW and recordBatteryReadings read the battery's cached RawW, which survives the driver going offline, and added three direct telemetry reads that the repo-hygiene check refuses. They now use Store.OnlineRawW, as phaseSample.moveW does: an offline battery's last value no longer sets the rise base or the shown level. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): judge a raise's landing from the meter's rise C4 counted a raise as landed only when a reading-to-reading move had a charger's shape. A slow ramp, or a house load on another phase that moved in the landing reading, left the whole raise counted as on its way on top of a reading that already held it, and Core cut a raise that fit under the line. The rise still to come now reads the meter's own rise since the raise went out: the smallest across the phases for a three-phase order, the car's phase for a one-phase car. For a three-phase order, a move on one phase alone no longer moves the shown level of a car not yet seen. A load cycling on the phase over the line took the cut's cover off on each drop and never gave it back, so Core cut again and again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): a car not yet seen may take a raise on one phase The smallest rise across the phases read a one-phase car not yet seen as a raise that had not landed: its phase rose, the other two did not. Core then cut the car from 11040 W to 0 with every phase under the line (field matrix, car1p cases). One phase that has risen alone since the raise went out, the others within half a step, now counts as that car taking the raise, as the shape tracker read it before. The changeset again says what holds: the one-phase house step case is closed for a car seen on three phases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): judge a one-phase landing reading by reading For a car not yet seen on three phases, a raise counted as landed on one phase only while the other phases stayed within 0.5 A of their reading when the raise went out. That window spans the charger's whole response time, so 0.6 A of house drift on another phase made the full raise count as still to come on top of a reading that already showed it, and Core cut the car with every phase under the line (field matrix with 0.7 A on L2: car1p cut 11100 -> 0 W). Each phase now keeps its rise over the readings in which the other phases moved less than a step (aloneA). A rise of a step or more there may be the car on that phase. Drift between readings, or under a step in the landing reading, no longer hides it; a house load that moves a step or more on another phase in the same reading still does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): restart credit keeps room for the car's rise to its max After a restart Core does not know the offer in force on the charger, so a car drawing below it may still rise before Core's order lands. The credit now counts only the draw less that possible rise past the latest offer, with the rise measured to the loadpoint's max_charge_w; a charger with no max_charge_w gets no credit. The charger and output reads move into health-gated telemetry accessors (OnlineLowerW, SumOnlineHigherW). Tests cover the raw and smoothed guards, a 10 s OCPP sample cadence on the plant, and the review's car rising under a 16 A offer in force. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(control): Charge now landing shown before Core logs its offer Site rig s1-16, s1-25 and s15-25 failed on c9b52c3: the charger answered within a second, and the meter reading that showed the car at 15.9 A came in 7 ms before the tick that first logged the 16 A offer. The reading could not show an offer logged after it, so the landing counted as a raise still to come, 15.9 + 16 A passed 1.2 x In, and Core cut 11,040 W to 5,520 W with 9 A of room on every phase. The same run of the base at 281f9f6, without this branch's commits, fails the same way; #1923's ab02710, merged here in c0a5274, reads the meter's own rise and fixes it. This test pins that rig shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): restart credit counts no sample newer than the meter The battery side's restart credit (evFoundDrawW) had two gaps. A charger sample measured after the site reading counted as part of it. A car that rose in that gap was credited for draw the meter did not show yet, and the battery ended its discharge into room the rise had taken. With a 3 s meter, L2 reached 19.30 A. A sample now counts only when measured at or before the site reading. The rise reserve took max_charge_w as the most the unknown offer in force can be. A charger still on an offer from before a restart that lowered max_charge_w, or on its own higher default, could rise past it: 19.00 A per phase on the unit fixture, 19.30 A on the plant. The reserve now runs to the larger of max_charge_w and the site fuse's rating. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): restart credit holds against a charger clock behind Core's The restart credit compared a charger's sample time, stamped by the charger's own clock (OCPP MeterValues), with the time the site reading arrived on Core's clock. A charger clock 1 s or more behind Core's let a rise sampled after the reading count as already in it, and L2 reached 19.30 A on the third review's plant case. A sample now counts only when stamped evClockAllowance (5 s) before the reading arrived, and the draw is the lowest of that sample and every sample since. noteDraw keeps the charger's samples while the restart window is open. Also tests the max_charge_w half of the rise cap: a 22 kW charger on a 3 x 25 A fuse keeps the reserve up to max_charge_w. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(control): the fuse margin is opt-in, and only the fuse ceilings move The SiteFuseSafetyA comment read as if the margin were needed by default; it now says zero is the default and when to set one. The changeset named "the site's total import and export limits", which reads as the hourly import_limit; that limit does not move. It now names the fuse's import and export ceilings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(control): battery beside a minimum-step car holds the fuse line (rig s18-16) On #1910 and #1891 the battery rises beside the car to the working limit, the fuse guard cuts it by the overage plus half of safety_margin_a and holds 30 s, and the battery rises again: it hunts between 15.0 and 15.5 A on a 16 A site. Where it stands when the house moves is chance, which is why rig s18-16 failed 3 of 57 runs on those heads. #1913's cut by the overage only keeps it on the line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(config): an unset fuse margin reaches Core as 0 A mutation that restored a hidden 0.5 A margin at startup passed every suite. This test pins the value newControlStateFromConfig hands Core. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(site): s18 judges own commands apart from the house's step At margin 0 Core holds the phases at the 16 A line. Rig s18-16's 0.1 A house step at small_step then lands over the line until Core cuts: on fdfbfec the phase read 16.101 A for 3 s and Core cut the battery 4851 -> 4719 W within 2 s. The criterion "own commands stay inside the rated fuse" counted that stretch as Core's and failed. The owner's rule judges an overage between In and 1.2 x In by the I2t model. The criterion now skips the 10 s after the step, the same gap the minor window already leaves. On the fdfbfec record Core's own commands peak at 16.008 A before the step and 16.005 A after it, inside 16.01 A. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(control): no cut of an unseen car, no landing from a 1p house move 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> * fix(control): judge a restart sample by its arrival, not its stamp 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> * test(site): judge overages at the fuse rating, not fuse - 1 A 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> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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 is step 6 (PR d) of the fuse-at-rating plan (owner rule of 27 Oct). Each phase's working limit is the fuse rating less
safety_margin_a. The margin is opt-in and defaults to 0, and nothing holds amps back below that line.Final review fix (
2b30933b)Blocking:
siterig/shortwent red at margin 0 on s8-60-25 as well as s18-16, because the rig still judged at fuse - 1 A. This PR now carries #1907's judge hunks (see Change). On this head:make verifyclean (Docker unavailable, so the avahi and release-context runs were skipped),go test -race ./internal/control/ ./cmd/ftw/ ./internal/configreload/and the hygiene scripts pass.siterig/shorthas not run on this head yet; the runner does that.Merge order
This PR sits on top of the fuse stack and lands last. Its base is
ev-base-after-restart(#1926). Merge in this order:b-ev-fuse-line-at-rating: the car's line moves to the rating lesssafety_margin_a.a5-battery-fuse-signed-phases: battery relief uses the same line, with no 1 A floor.c-battery-fuse-no-buffer: the battery cut lands on the line.ev-cut-landed-and-unshown: a cut the meter already shows covers nothing later.battery-rise-per-phase-cap: a battery charge rise fits each phase before it is sent.car-before-battery-charge: the car comes before battery charge.ev-base-after-restart: after a restart the battery credits the car the meter shows.Two PRs off
mastermust land before this one:whole-amp-phase-current: Core reads a whole-ampere meter's phase current from its power. Without it, margin 0 lets a phase sit up to 1 A over In on many P1 meters (see Risk).main.gointoconfigreload.Apply. The stack now holdsmaster(77b15c2), and this PR carries the new comment inconfigreload/apply.go.#1907 need not land first: this PR carries its judge hunks (see Change).
When this PR is retargeted and
origin/masteris merged in,go/cmd/ftw/main.goconflicts in the reload block. Takemaster's side: this PR's onlymain.gochange is a comment in that block, and the block now lives ingo/internal/configreload/apply.go. Then rewordapply.go's "nil → 0.5, explicit 0 → disabled" (line 88 onmaster) to "nil → 0 (work to the fuse rating), an explicit value wins". Review's trial merge on a5cdeae, withmaster'smain.gotaken, builds, vets, and passesgo testoncmd/ftw,configreloadandconfig.In the same retarget merge, add the reload-path test below as
go/internal/configreload/apply_fuse_margin_test.go. It cannot land before then: this branch has noconfigreloadpackage, and its reload block is a closure insidemain(). Without it, a hidden 0.5 A default put back on reload passes every suite (review's mutation, afterctrl.SiteFuseSafetyA = newCfg.Fuse.EffectiveSafetyMarginA():if newCfg.Fuse.SafetyMarginA == nil { ctrl.SiteFuseSafetyA = 0.5 }). I checked it on a copy ofmaster(573d24f) withDefaultFuseSafetyMarginA = 0: it passes under-race, and with that mutation inapply.goit fails with "margin after reload = 0.5 A, want 0 A". It also fails if the reload stops setting the margin, since Core starts at 0.5 A.apply_fuse_margin_test.go
When each PR squash-merges, move the next one's base to
master, then mergeorigin/masterinto it and push. The push matters: see "CI on the stack" below.Land #1910 and #1891 close together. On #1910 alone the rig's
siterig/shortfails s18-16 ("battery yields before the minimum-step car"); it passes onmasterand on #1891 and everything above it.Sync (28 Oct, 09:00)
#1913 gained 358da3b at 08:01 (a test only: the battery beside a minimum-step car holds the fuse line, rig s18-16). I merged it up the stack, each step a plain merge with no conflict, pushed fast-forward:
ev-cut-landed-and-unshownbattery-rise-per-phase-capcar-before-battery-chargeev-base-after-restartd-fuse-margin-default-0TestBatteryBesideMinimumCarHoldsTheFuseLinepasses at each step. At margin 0.5 A (#1923 to #1926) the battery holds 4473..4485 W and the peak phase is 15.59 A. On this branch, at margin 0, it holds 4818..4830 W, drops 4824 → 4756 W across the house step, and the peak phase is 16.09 A.This branch now holds the current tips of #1910 (ddab122), #1891 (b36e951), #1913 (358da3b), #1923, #1924, #1925 and #1926, plus master through 7840c33.
git branch -r --merged HEADlists every stack branch.The earlier sync (28 Oct, morning) merged
ev-base-after-restartat 551febd. Only #1910's changeset conflicted. The merge kept #1910's new sentence on an offline battery's relief and kept "a 0.5 A margin" in place of "the default 0.5 A margin".The earlier restack (28 Oct, night) merged #1891's tip up the stack. Only #1924 conflicted, in
dispatch.go. The merge keptsignedPhasesA, droppedsignedA, and replacedphaseMoveWwithphaseSample.moveW. The merge commit e2faf1e explains the resolution.Problem
An unset
fuse.safety_margin_aresolved to 0.5 A. Every line built on it sat 0.5 A per phase (345 W on 3 × 230 V) under the fuse, though nobody had asked for a margin. That covered the car's and the battery's per-phase line (after #1910 and #1891) and the aggregate ceilings: the guard, the joint EV/battery allocator, the PV export guard and the peak-limit ceiling. The settings form showed 0.5, and its help promised headroom. The peak-limit error always named "a 0.0 A safety margin", even when there was none.This PR was first blocked: on #1910 alone, margin 0 made
TestSiteSimRandomHouse/1fail. An EV cut the meter already showed kept covering a later house step, so Core cut one step and stopped. #1923 fixes that, and seed 1 passes on this branch.Change
go/internal/config/config.go:DefaultFuseSafetyMarginA = 0. The pointer stays, so any explicit value, 0 included, still wins. The field and resolver comments say the margin is opt-in.c.Retired, logged once at startup): "fuse.safety_margin_a is 0.5 A, FTW's old default. FTW now works to the fuse rating. Set 0 to use it." Both load paths (YAML and stored settings) do this. It changes no stored state, andDropRetiredSettingsleaves the value alone.go/internal/control/dispatch.go: the peak-limit error names the margin only when it is above 0. TheSiteFuseSafetyAcomment now says the margin is opt-in and zero is the default.go/cmd/ftw/control_state.go,go/internal/configreload/apply.go: the "nil → 0.5" comments now say nil → 0. fix(drivers): bound driver_init and apply fuse settings before reload #1893 moved themain.gocomment intoconfigreload/apply.go.web/settings/tabs/control.js: the field shows 0 when unset. New help: "Optional. Amps to keep below the fuse rating, for an inverter whose own phase limiter trips at the rating. 0 works to the rating."simFuseSafetyA = 0, the new default.fitScenariotakes the room under the line and sets the house fromsimFuseSafetyA, so its "exact fit" cases stay exact at any margin. The "charger's own load balancer" scenario passes at margin 0 only because the balancer sits at 25 A, which is the rating and so the new line. The Core bug remains: a cut comes off an offer the car does not draw (stepEVMeterLimit). Two known variants keep it tracked and turn red once someone fixes it: the stored 0.5 A margin, and a balancer at 25.5 A at margin 0.go/test/site/ev_battery_priority_test.go): "own commands stay inside the rated fuse" now skips the 10 s after the house's 0.1 A step, the same gap the scenario's minor window leaves. At margin 0 Core holds the phases at 16.00 A, so that step lands over the line until Core cuts. On fdfbfecsiterig/shortfailed s18-16 on this criterion (and s8-60-25, see the next item): the phase read 16.101 A for 3 s right after the step, and Core cut the battery 4851 → 4719 W within 2 s. The owner's rule judges an overage between In and 1.2 × In by the I²t model, and "fuse holds" still covers it. On that run's record Core's own commands peak at 16.008 A before the step and 16.005 A after it, inside the 16.01 A bound. A new metric,max_i_own_commands, reports that number beside the old one.go/test/site): s3, s8, s9-s11 (overage()), s15bat and the random invariants now count time over the fuse rating, not over fuse - 1 A. At margin 0 the car in s8-60-25 settles at 25 A, the rating, and the old judge counted 271 s above 24 A against 67 s allowed (fdfbfec:over_fuse_longest_s0,fuse_blownfalse). The hunks match test(site): the rig judges "over the limit" above the fuse #1907's, so test(site): the rig judges "over the limit" above the fuse #1907 merges cleanly after this.d-fuse-margin-default-0.mdsays what moves once the stack has landed. On a site with no saved margin, the car, the battery's charge and fuse relief, and the fuse's total import and export ceilings each move up 0.5 A per phase, to the rating. The hourlyimport_limitdoes not move. In fix(control): car and battery work to the fuse rating (fuse stack #1910, #1891, #1913, #1923, #1924, #1925, #1926, #1914) #1910's changeset, "with the default 0.5 A margin" now reads "with a 0.5 A margin".Tests
TestFuseSafetyMarginNilUsesDefault: an unset key resolves to 0.TestFuseSafetyMarginLoadKeepsExplicitValues: unset gives 0 with no notice. Explicit 0, 0.4 and 1.2 stay, with no notice. Explicit 0.5 stays 0.5, with exactly one notice.TestStoredOldDefaultFuseMarginIsKeptWithANotice: a 0.5 A margin in state.db keeps steering the site over two loads. It gives one notice per load, survivesDropRetiredSettingsand stays in the stored document.TestUnsetFuseMarginReachesCoreAsZero(cmd/ftw): a config with nosafety_margin_areaches Core as 0, a peak limit at the fuse (43470 W on 63 A × 3 × 230 V) is accepted and 1 W more is refused, and an explicit 0.5 A reaches Core as 0.5 A. Review showed that a mutation restoring a hidden 0.5 A default at startup (if cfg.Fuse.SafetyMarginA == nil { ctrl.SiteFuseSafetyA = 0.5 }afterEffectiveSafetyMarginA) passed every suite. This test fails on it: "unset margin reached Core as 0.5 A, want 0".configreloadtest that lands with the retarget.TestS8JudgesAtTheFuse(new) and test(site): the rig judges "over the limit" above the fuse #1907'sTestInvariantsJudgeAtTheFuse(go/test/site, no simulator): a phase at exactly the fuse passes; 0.1 A over it past the time budget fails. Both fail on the old judges ("a phase at exactly the 25 A fuse failed").TestS18OwnCommandsSkipTheHouseStep(go/test/site, runs without a simulator): 4 s at 16.1 A right after the house's step passes; the same 4 s at 50 s or 150 s, with no outside step, fails. Before the judge change it failed with "4 s at 16.1 A right after the house's step failed".TestSetPeakLimitMessageNamesOnlyARealMargin: with 0.5 A the error names the margin. With 0 it names none, and the fuse itself (11040 W) is the ceiling.#1761 replay, safety_margin_a 0.5 Akeeps the old value covered as an opt-in.TestSiteSimCarEndsAtTheLine: with the oldfitScenarioat margin 0 it fails (car 8280 W ... want 8625 W), because a 3.5 A house leaves 12.5 A of room, not twelve whole steps. WithfitScenarioreadingsimFuseSafetyA, it passes, and the car ends at 8280 W on the 16 A line.TestSiteSimScenarios: the load-balancer scenario at 25 A passes at margin 0. Its two known variants fail as known: at 0.5 A with "over Core's limit for more than 11s (heat 24.6 A, limit 24.5 A)", and with the balancer at 25.5 A with "(heat 25.1 A, limit 25.0 A)".make verifyexits 0 with its Docker steps skipped,go test -race -count=1 ./internal/control/passes (223 s), and the hygiene checks againstorigin/master(573d24f) find no planning documents, 133 telemetry reads with none added, and no new legacy brand copy. On dbc4332 (the head)go vetandgo test -race -count=1 ./test/site/...pass and the hygiene checks give the same result.go test -race -count=1passes on./cmd/ftw/and./internal/config/,make verifyexits 0, and the hygiene checks againstorigin/master(573d24f) find no planning documents and 133 telemetry reads, none added.go test -race -count=1passes on./internal/control/,./cmd/ftw/,./internal/config/,./internal/configreload/,./internal/api/,./internal/loadpoint/and./internal/telemetry/.make verifyis clean on the synced head. Its Docker steps skipped (no daemon).node --test 'web/**/*.test.mjs'(the CIwebjob): 893 of 893 pass.go/internal/telemetrygo from 134 to 133, with no file adding one; no new legacy brand copy; both helper unit suites pass.200-seed count
This is TestSiteSimRandomHouse's random house: 200 seeds, 1 h each, on the sync merge b621bab. Review reran it on fdfbfec and got the same counts and seeds. Since then this branch gained only tests (358da3b and the s18 judge). It uses review's harness. The "step judge" restarts the over-limit allowance at each outside step, as #1920 (triage R2) does for the rig.
Before the sync (4ecebfd) the counts were 7, 5 and 86. Seed 172 now passes at margin 0.
Plain-judge failures. At margin 0 these are seeds 70, 99 (twice), 138, 158, 179 and 189. At 0.5 A they are 138, 172, 179, 189 and 198. Each one has the same cause:
The step judge counts none of them.
The house-at-In row. 87 seeds fail:
power_max_age_s, so Core waitsevUnknownSettle(3 min) for it to show. For those 3 min Core held the car's 6 A minimum, with the phase at 26.5 A on a 25 A fuse and the heat rising to 26.2 A. A targeted reproduction (a house step on every phase in the same second as the cut) does the same at margin 0, on this branch and onmasteralike: 3 min over the line, heat 26.8 A. With a declared 5 s or 40 s charger delay the hold is 23 s or 43 s, the time Core gives the charger. It is not fixed here. EV fuse cut: a house step in the same second hides the cut's landing, and Core holds a phase over the line for evUnknownSettle #1931 tracks it, for a fix inev_fuse_budget.go(unshownOrders,cutForA) before stable.CI on the stack
GitHub's required check is
go test + vet, the aggregate job intest.yml.test.yml,repo-hygiene.yml,changeset-check.yml,brand-cleanup.yml,core-binaries.ymlandhistory-converter.ymlrun only on PRs intomaster.native-solver.ymlruns on any PR that touches its paths, and the macmini runner postssiterig/shorton stacked heads.As GitHub showed it at 09:00 on 28 Oct, after the sync pushes:
siterig/shortfails s18-16siterig/shortpassgo test + vetand its suites, repo hygiene, changeset, brand, Core binaries, history convertersiterig/shortpasssiterig/short)siterig/shortsiterig/short)siterig/shortsiterig/short)siterig/shortsiterig/short)siterig/shortnative-solver; itssiterig/shortfailed s18-16 on "own commands stay inside the rated fuse" (see Change)siterig/shortThe sync heads differ from the passed ones only by 358da3b, a test file in
go/internal/control.test.ymland the othermaster-only workflows listen foropened,synchronizeandreopened. Moving a PR's base tomasterstarts none of them. Each PR needs a push after the retarget, which mergingorigin/masterprovides. The local runs above (race tests,make verify, web tests, the hygiene checks) cover the whole stack's code on this branch. They do not stand in for the required check on each squash commit.Risk
Medium. On sites with no saved key, which is every rig run and most sites, every line moves up 0.5 A per phase together. An inverter whose own phase limiter trips at the rating may now cut to 0 W. That is why the old default existed, and the opt-in margin is the remedy. Watch for it on the validation sites during the beta. Sites with a stored 0.5 A keep it and see one startup notice. No new control mechanism, and no dead zone or timer moves.
With Core at the line, any house rise puts a phase over In until Core cuts. On rig s18-16 a 0.1 A step read 16.10 A for about 3 s. The I²t rule allows that, and the s18 judge now says so (see Change).
At margin 0, a whole-ampere meter has no cushion either. Many P1 meters (
p1_dsmr,zuidwijk_p1) report phase current in whole amperes, and Core read magnitude froml*_aalone. Review measured a phase at 16.60 A (truncating) and 16.45 A (rounding) on a 16 A fuse for 20 min at margin 0, against 15.60 A and 15.45 A at 0.5 A. #1932 reads each phase as at least its power over its measured voltage. On this stack with #1932, the same probes end at 15.60 A and 15.45 A at margin 0, with no time over In. Land #1932 first.At margin 0, a frozen site meter has no 0.5 A cushion. #1921 (C5, frozen meter counts as stale) is independent, with base
master, and touchesmain.goanddispatch.goaway from this PR's lines. It should reach the same beta as this PR or an earlier one.Related open PRs
safety_margin_a: 0in the rig. After this PR that pin is the default, so test(site): the rig expects the room the fuse rating gives #1908 stays correct.sitesim_test.gofrommaster, outside this stack. Whichever lands later needs a merge there.🤖 Generated with Claude Code