fix(mpc): surplus-only EV can take leftover PV while the home battery grid-charges - #957
fix(mpc): surplus-only EV can take leftover PV while the home battery grid-charges#957frahlg wants to merge 2 commits into
Conversation
…arge Surplus-only is an EV policy, not a site import ban. The car may use PV left after house load in the same slot the home battery buys from the grid. Live surplus and the near-term 3Φ gate follow the same accounting. Add a joined EV site harness (plan → charger Tick → ComputeDispatch → site identity). Isolated planner, charger and dispatch suites never ran on one clock, which is why the combo stayed invisible. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Give leftover PV, house residual and grid identity a home in loadpoint.GridW. DP, ValidatePlan, main.go and the joined site clock all read that contract. Plan→EMS mapping lives in SlotDirectiveFromMPC and SlotDirective.LoadpointDirective — the same adapters main.go uses. The site clock publishes via Service.InstallPlan and reads SlotDirectiveAt, so charger and battery cannot be given two mappings of one slot. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
|
Rätt problem och rätt riktning, men jag skulle inte släppa draften som den är. Live-surplus-formeln blandar ihop “någon importerar” med “batteriet köper från nätet”, och det är precis den läcka PR:n säger sig stänga. Inga tidigare reviews. CI grön. Relaterat: #953 (mergerad) tog idle EV + batteriimport; den här tar leftover PV + batteriimport i samma slot. #888 lämnas ifred ( Det som faktiskt är fel
if surplusOnlyActive && batW > 0 && gridW <= GridChargeImportW {
leftover = -gridW + evW // göm PV-soak
}
Enhets-testen kodar det:
Identity: leftover = 7640 W. Batteriet tar 5000 < 7640 — det är soak, inte Pixii som köper. EV+batteri = 9140, därför importerar de 1500. Att erbjuda 7640 låter EV stanna på 4140 medan importen består. En riktig combo är leftover 7500, batteri 10 kW, EV 4140, grid ≈ 6640. Alla 3Φ-grinden gör det rätt:
Med den proxyn blir (1500, 5000, 4140) → 2640 (leftover minus soak), och (6640, 10000, 4140) → 7500 (äkta combo). Det är den formeln kommentaren beskriver. Det här är samma flap #953 varnade för, bara spegelvänd: “handa batteriets soak till EV → import → ombudgetering”. Övrigt som bör fixas i samma draft1. Python-testen if action["flex_power_w"]["surplus-car"] > 1e-5:
assert action["grid_w"] <= 50 + 1e-5Det är “EV på ⇒ ingen site-import”. Slot 1 i 2. Ingen Det finns accept-combo ( 3. Python-constrainten är bara på Gamla regeln var per scenario ( 4. Kommentaren säger fortfarande att surplus är 5. Texten säger att runtime-clampen i 6. Optimizer måste släppas med Core Samma läxa som #953: HiGHS-imagen måste ha den nya constrainten, annars producerar default-pathen fortfarande “bilen stilla på billig sol”, och Beteende att känna till, inte nödvändigtvis bugOpportunistisk surplus (planens EV-budget = 0,
Bat-SoC-unlock ur 48 h-specen är rätt och lite utanför “leftover-hålet”, men motiverad. Det som är bra
VerdictRätt scope, rätt arkitektur, CI grön — men live-grenen är inte den identitet PR-texten säljer, och den enda enhetstest som rör “grid-charge + EV” använder tal som är soak+import. Fixa |
Accepted text proposal
Issue or Discussion: field reports that battery + EV charging together is broken after #953.
Maintainer comment that accepted this scope: continue from the surplus-only / home-battery combo; the remaining hole is leftover PV into the car while the house battery buys from the grid, and the missing joined EV test harness that should have caught it. Follow-up: EV must be a first-class site-power contract, not a parallel mapper in that harness.
What changed
#953 let the home battery import at night while a surplus-only EV sat plugged and idle. Surplus-only was still implemented as “no site import while the EV is charging”. That forbids the real combo: leftover PV into the car, grid into the home battery, in the same slot.
The constraint is now leftover PV after house load, in the Go DP,
ValidatePlan, and the Python optimizer:evW <= max(0, -(loadW + pvW)) + 50That identity lives in
loadpoint(GridW,PVLeftoverAfterHouseW,HouseResidualW,SurplusOnlyExceedsHousePV,BatteryDischargeFeedsEV). Planner, live surplus, and the near-term 3Φ gate read it. Site import from a simultaneous home-battery grid-charge is not the car importing. Battery→EV is still blocked.Plan→EMS mapping is one pair of adapters, used by
main.goand the site clock:control.SlotDirectiveFromMPCmpc.SlotDirective.LoadpointDirectivempc.PeakPlannedSurplusForEVThe joined site clock (
TestEVSite*) holds anmpc.Service, publishes withInstallPlan, and readsSlotDirectiveAt— the same cachemain.goreads. It does not hand-mapActionontoSlotDirective. Tick order is unchanged: charger first, then battery dispatch.SlotDirectiveAtagesGeneratedAtMson the wall clock (MaxPlanAge). Injected noon slots still stampGeneratedAtMswithtime.Now()so a simulated clock cannot bypass the live staleness rule.Why
Isolated tests exist and they passed while the combo stayed broken:
Optimize/ValidatePlanand never tick the chargerDirectiveand never run battery dispatchComputeDispatchwith a pre-bakedEVChargingWand no loadpoint controllergo/test/e2ehas Ferroamp / Sungrow batteries and no EV chargerA harness that reimplemented leftover/grid and plan→EMS mapping would have been a second EV stack. The site clock is a plant and a clock; the contract is shared.
#953’s tests asserted idle EV + battery import. They could not see “PV to the car, grid to Pixii” on one clock.
Boundaries and safety
NoBatteryToEV/ surplus-only).dispatch.gois unchanged in this PR (fix(control): mitigate PV-only export over limits #888 has that file).Out of scope:
repairPoisonedBucketsovernight prior, hybrid idle deadband, PV-only export (#888).Verification
Harness scenarios that fail on unfixed master:
Open PRs on overlapping files
dispatch.goPV export) — no shared surplus-reader change; that file is left aloneloadpoint.go) — not the surplus helpersmain.gois already on this branch from the first commitChecklist