fix(control): size per-phase relief by the site's phase count - #808
Closed
frahlg wants to merge 1 commit into
Closed
fix(control): size per-phase relief by the site's phase count#808frahlg wants to merge 1 commit into
frahlg wants to merge 1 commit into
Conversation
A worst-phase overage is watts on one phase; dispatch commands aggregate battery watts. The conversion between them is the site's phase count, and it was written twice as a bare `* 3.0` — once in applyFuseGuard, once in forceFuseDischarge — while SiteFusePhases sat in the same struct and fuseSafetyMarginW read it correctly two functions away. On a 1-phase site the aggregate meter and the single phase are the same wire, so relief is the overage once. Demanding three times it overshoots the ceiling and can push the meter through zero into a violation on the other side. One helper now owns the conversion. perPhaseOverageW reads only the configured phases, in L1/L2/L3 order, the way sitePhaseCurrentsAt already does in cmd/ftw, and converts amps to watts through the configured voltage only — no invented 230 V, the same law fuseSafetyMarginW states in its own comment. A fuse described by amps alone gets no per-phase clamp instead of one computed from numbers nobody configured; config fills both fields (230 V / 3 phases by default, validation rejects <= 0), so only harnesses that wire SiteFuseAmps alone see that. forceFuseDischarge keeps its import-only gate. That is deliberate, not drift: its single lever is more discharge, which relieves import and worsens an export-side phase trip, and applyFuseGuard's export branch already shrinks discharge for that case. The limit now sits at the call that needs it rather than inside a second copy of the conversion. Three-phase 230 V behaviour is arithmetically identical: the golden corpus replays unmoved, with the same 33 records exercising the per-phase path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Member
Author
|
Superseded by #812. GitHub closed this PR automatically when #803 merged and its base branch |
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.
Stacked on #803 (
agent/fix-deadband-runs-fuse-saver) — it touches the same file. Review the top commit; the base PR carries the rest.The bug
perPhaseOverageW(...) * 3.0was written twice indispatch.go— once inapplyFuseGuard, once inforceFuseDischarge— whileSiteFusePhasessat in the sameStatestruct andfuseSafetyMarginW, two functions away, read it correctly and says so in its own comment: "No hardcoded 230 V / 3 phases here — both come from config."A worst-phase overage is watts on one phase. Dispatch commands aggregate battery watts. The conversion between the two is the site's phase count: a battery spreads its action across the phases, so each aggregate watt relieves 1/N on the worst phase and an overage of N watts needs
N × phasesof action. Writing that as3.0is only right on a three-phase site.On a 1-phase site the aggregate meter and the single phase are the same wire, so the two numbers must agree — and they didn't:
perPhaseOverageWhad two more of the same flavour. It walked L1/L2/L3 unconditionally, so current reported on a phase a 1Φ site does not own fired the clamp on a site nowhere near its fuse. And it fell back to a hardcoded230for voltage — the exact thingfuseSafetyMarginWrefuses to do.The fix
One helper,
perPhaseReliefW, owns the conversion, and both call sites use it.SiteFusePhases.SiteFuseVoltage, the way every other watt in this file gets it (fuseSafetyMarginW,Fuse.MaxPowerW,main.go'sfuseMaxW). No invented 230 V.sitePhaseCurrentsAtalready follows incmd/ftw/site_dispatch_safety.go.config.godefaultsfuse.voltageto 230 andfuse.phasesto 3 and rejects<= 0in validation, andcontrol_state.gocopies all three fields, so no configured site loses protection — only harnesses that wireSiteFuseAmpsalone.Was the import-only gate intentional or drift?
Intentional, and it stays. The two call sites genuinely need different behaviour:
forceFuseDischargehas exactly one lever: command more discharge. That relieves an import-side phase and worsens an export-side one. Honouring a direction-agnostic overage there would push the over-current phase further over the breaker.applyFuseGuardhas both levers — shrink charge, shrink discharge — so it uses relief in both directions and attributes it by the live aggregate sign.The evidence: the gate arrived with the PR #219 review, its comment says so, and
TestForceFuseDischargeIgnoresExportSidePerPhasehas pinned it since. So the answer to "one of these two is wrong" is that the duplicated* 3.0was the wrong half, not the gate. What was wrong about the gate is that it was the only thing distinguishing the sites while the conversion was silently duplicated. The direction limit now sits at the call that needs it; the conversion is stated once.Tests
fuse_saver_test.gopinned 3-phase behaviour only. New siblings, all four of which fail against the old code:TestPerPhaseReliefMatchesSinglePhaseSiteOnImport— 1Φ site, idle battery: forced discharge equals the overage once, and the post-dispatch meter stays on the import side of zero.TestPerPhaseReliefMatchesSinglePhaseSiteOnExport— the export directionforceFuseDischargedeliberately skips andapplyFuseGuardowns: same law, one phase count. Under× 3the discharge is zeroed and the site swings to 520 W of import.TestPerPhaseClampReadsConfiguredPhasesOnly— a 1Φ site with L1 at 10 A ignores 24 A reported on phases it does not have.TestPerPhaseClampOffWhenFuseDescriptionIncomplete— amps without a voltage or a phase count clamp nothing.TestPerPhaseClampUsesConfiguredVoltage— a 240 V site converts at 240 V (2560 W, not the 2620 W a hardcoded 230 V gives).setupPerPhasenow states the site it builds (3Φ, 230 V) and delegates tosetupPerPhaseSite, which takes both.Golden corpus
Predicted before looking: zero records move. The corpus was recorded on 3-phase sites, so
× SiteFusePhasesis arithmetically× 3.0there, the walk covers the same three phases, and the configured voltage is the same 230 V the fallback invented.Verified after:
TestGoldenCorpusReplaypasses unchanged at the 0.01 W tolerance, all 590 records, andTestGoldenCorpusCoveragestill reportsper_phase=33— the same records exercise the path, none of them silently switched off. Every record in the corpus that carries phase amps issite_fuse_voltage=230, site_fuse_phases=3; the 33 with a non-zero per-phase overage are all in that set.make verifygreen (pre-commit gate).