Repository navigation
Conversation
Fuse protection read every phase current as a magnitude. Under 20 A of solar export a phase counted as 20 A of import, so on a 25 A fuse a car never started, and on a 16 A fuse 8 A of export was enough to block it. phaseCurrentsA now signs each phase by the same phase's power in the reading (l1_w, l2_w, l3_w, which P1, Pixii, SolarEdge and SDM630 emit). Without that power the current counts as import, the safe side. Rises and starts read the level with direction: an exporting phase counts at its reading, so new load there has more room. An importing phase still counts at the higher of its reading and its heat. The heat filter keeps I², and only an importing phase can be over for the EV cut. The site simulation's meter now emits signed phase power, as P1 does, also when its currents lose their sign. Both export rows pass and lose their known line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e_limit Core folded the clock hour's import ceiling into the EV cap and the battery allocator under the fuse's name. A car held back by the hour's budget said fuse_limit, a boost it stopped said "Fuse protection", and the web page and apps blamed the main fuse while the phases were low. EVFuseBudget now records which limit left less room at each site reading, and EVLimitReason names it. The loadpoint reads it once per tick with the cap (SetFuseLimitReason) and labels a capped command import_limit or fuse_limit. The allocator marks FuseSaturatedByImport when the hour's limit is the ceiling it hit, and the boost stops with import_limit_block. fuse_limit now means the main fuse alone; the web page says so and gives the hour's limit its own words. The site simulation's new row is a night with a 6 kW hourly ceiling, a heavy first 50 minutes, a Core restart at 02:09 and phases at 1.5 A near the end of the hour. On master the idle car says fuse_limit; now it says import_limit and starts within 16 s of the new hour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…imit Review of #1783 (Codex): after a quiet part of the hour, the hour's limits can sit above what the fuse allows, so the label compared them and said fuse_limit. An idle car is then kept off by the ceiling's own start test, which caps the start level at the ceiling itself. Under a measured hour the idle start now names the limit by that test: the start level against this reading, and import_limit when the dwell's mean import fails it. TestImportCeilingStartTestSaysImportLimit: 40 quiet minutes under a 7.5 kW ceiling, then an 8 kW house and a car plugged in. It said fuse_limit; it says import_limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fredrik's rule of 21 October 2026: fuse protection counts the site meter's current per phase, with direction, and our own raises that cannot show yet. Nothing else. Core read the fuse in nine places with five margins and two phase-current sources. fusePhases (fuse_phase.go) is now the one reading for every charger and battery decision: the meter's signed current per phase (its total shared evenly when it reports none), one margin (safety_margin_a, at least 1 A), room for a rise (limit less level and our pending raises) and an overage for a cut (once a phase's heat budget is spent, or above 1.2 x In), import and export apart. - EVFuseBudget reads it for rises, starts and cuts; the meter total no longer stands beside the phases. - fitBatteryRisesToFuse replaces clampEVBatteryHeadroom: a battery's rise fits the phases' room after what the cars still need, EV first, with or without a car; a discharge that would end keeps what the room needs, down to the battery's floor; the export side the same with the signs turned. It sets FuseSaturated. - forceFuseDischarge relieves the import overage net of the EV cuts we sent during the run over the limit, at most evSettle. The hour's limit nets the same cuts. Deleted: applyFuseGuard's per-phase part, latch and half-margin buffer (what is left is applySiteLimits: the hour's limit and max_export_w); the aggregate fuse checks beside the phases; perPhaseOverageW and perPhaseReliefW; clampEVBatteryHeadroom and its own phase reader; the joint allocator, phaseImportCeilingW and the #1536 imbalance proxy; republishFuseEVCapAfterFuseDischarge and the dead FuseEVMaxW; pendingEVCutW; the meter-cadence gap rule (only a stale reading starts afresh); and cmd/ftw's second phase-current source, the meter_l*_a metrics. Also: fuse relief stops at the battery's soc_min, else the planner's (PowerLimits.SoCMin); an idle car starts when its minimum fits; a second car plugging in no longer stops the first; a timed-out command counts as sent. Site simulation: Pixii total/phase disagreement, both sides relieving one overage and the 16 A idle start pass; the #1761 replay is unchanged. Golden corpus re-recorded: 107 of 551 records moved, none ends with a phase further over its limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 21, 2026
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.
Step 6 of the fuse rule in Fredrik's review of 21 October 2026: fuse protection is one function. The third of three PRs. Stacked on #1782 (direction of phase current) and #1783 (
import_limit): this branch merges both onto master. Review the last commit,fix(control): fuse protection as one function; I rebase it when those land. Like #1782, it goes in the next beta together with the fix for the charger's late readback in surplus charging (surplus_reserve.go:128): signed phase currents remove the slow ramp under export that hid that bug.The function
go/internal/control/fuse_phase.goholdsfusePhases, the one reading of the main fuse that EV allocation and battery dispatch both use:safety_margin_a, at least 1 A, on every phase (was 1 A for the car, and 0.5 A plus half a margin of buffer for the battery);Who uses it:
EVFuseBudget(car): rise and start room, and the cut. The meter total no longer stands next to the phases.fitBatteryRisesToFuse(battery, replacesclampEVBatteryHeadroom): a charge rise fits the phases' room after what the cars still need, EV first; a discharge that would end keeps what the room needs, down to the battery's floor; the export side the same with signs turned. It now applies with or without a car, and setsFuseSaturated.forceFuseDischarge/fuseSaverFromLivePower(battery cut): the import overage net of EV cuts already sent during the run over the limit (at mostevSettle), plus what the targets add. The hour's import limit nets the same cuts.Deleted
applyFuseGuard's per-phase part, its 30 s latch (FuseHoldMaxChargeW/DischargeW/Until) and its half-margin buffer. What is left isapplySiteLimits: the hour's import limit andmax_export_w, both totals.applyFuseGuardandfuseReliefFromSnapshot(the total judged the fuse beside the phases; a Pixii total 5 kW above its phases cut the battery).perPhaseOverageW,perPhaseOverageReadingW,perPhaseReliefW(a second phase-current reader, magnitudes, direction from the total).clampEVBatteryHeadroom,evBatteryPhaseSpareW,worstPhaseA(ev_battery_headroom.go, a third reader with its own margin).ComputeDispatch,phaseImportCeilingWandphaseImbalanceA(the fix(control): give a planned EV its fuse share on the worst phase #1536 imbalance proxy),republishFuseEVCapAfterFuseDischarge, and the deadFuseEVMaxWoutput.pendingEVCutWandoverOffers(replaced byevCutsSent, which reads our command log).evSettlewas a "gap" that restarted the idle start and the hour's run. Only a reading older than the meter's max age (stale) does now.cmd/ftw's phase-current gate read themeter_l*_ametrics, a separate copy; it now reads the meter reading throughcontrol.SitePhaseCurrents.PhaseImbalanceW(planner) reads the same.Net: production code −209 lines (+680 −889), tests −158, golden corpus −1,647. No settings added;
safety_margin_akeeps its meaning as the one margin, with a 1 A floor.Also in this PR (from the end-to-end run and Efret's site)
soc_min, else the planner's (10 % by default);PowerLimits.SoCMincarries it and hot-reloads with the other limits. Below the floor the lever is the car.fuse_limit. It starts a step below the room, or at its minimum when only that fits. A three-phase car's minimum counts 6 A on each phase. Under an unmeasured hour's ceiling the step stays.SetEVCommandednow also logs it at send time, so pending and cut netting see it. Pinned byTestTimedOutCommandStillCountsAsSent.Site simulation, before → after
Base: master with #1782 and #1783.
The simulation's over-the-limit counter now has 0.01 A of tolerance: a phase that settles exactly at the 24 A working limit is not over it.
Golden corpus
Re-recorded. 107 of 551 records moved their targets (48 to more import, 59 to less); the rest moved only attributions (
fuse_ev_max_w, the latch andper_phase_overage_ware gone;phase_over_aandphase_export_over_aare new). Records that name onlyfuse_max_wnow describe it as a balanced 230 V three-phase fuse, as config does, and the phases' power now carries the current's sign, as drivers emit it. I checked every moved record: none ends with a phase further over its limit than before, on import or export. The largest moves:seeded_fuse/050: discharge −9,956 → −4,787 W. The total said over; the phases had room.seeded_fuse/031,slew/seeded_103_planner_arbitrage: a battery under 32 A of export now keeps charging (+5,550 W, +8,745 W) until the phase is at the limit, the lever the rule names for export.seeded_fuse/030: idle mode with a battery discharging 2,369 W and L1 at 23.4 A: the battery keeps 1,955 W of discharge instead of stopping into 26.8 A.Conflicts and open points
clampEVBatteryHeadroom, which this PR deletes. fix(control): reserve EV demand until offered power is delivered #1763 reserves the car's unmet demand from the charger's reading. The rule from 21 October says no reservation for a car that takes less than its offer ("Räknas inte: laddarens egen avläsning");TestBatteryKeepsRoomACarDoesNotTakepins that. I have not touched fix(control): reserve EV demand until offered power is delivered #1763.l*_ais Fredrik's call.notifications/service.gostill readsmeter_l1_afor its fuse notice (the review's row 9); the charger's own load balancer row; the PV export guard and the PI's aggregate ceilings (effectiveImportCeilingW,effectiveExportCeilingW).Evidence
go test -p 2 ./internal/control ./internal/sitesim ./internal/loadpoint ./internal/api ./internal/mpc ./cmd/ftw: passnpm test: 868 passmake verify(withGOFLAGS=-p=2): pass on c60da23Real-hardware evidence is still needed: a Pixii site (total and phases disagree), an Easee Cloud site with a battery, and a site under export with a battery.
🤖 Generated with Claude Code