Planner: load management-aware multi-loadpoint planning - #31902
Draft
andig wants to merge 15 commits into
Draft
Conversation
Tracking doc for making tariff planning respect shared circuit capacity across multiple loadpoints, so per-loadpoint plans plus minSoc forced charging can no longer overcommit a circuit. Phased TODO checklist, no behaviour change.
Phase 0 observability for loadmanagement-aware planning: per-loadpoint plans ignore shared circuit capacity, so a planned or minSoc charge can be silently capped by the circuit and miss its target. Surface this as a warning in setLimit and cover it with a test.
Phase 0 observability, corrected: warn in the planner (not the loadpoint) and only on an actual goal miss - the required duration no longer fits before the target, so charging will overrun. Warned once per target to avoid per-cycle spam. Replaces the earlier loadpoint-side throttle warning, which fired on any transient circuit cap even when the goal was still met.
requiredDuration already clamps to the circuit's total max power via EffectiveMaxPower; that total-clamp is acceptable. The gap is shared, per-slot capacity: each loadpoint assumes it owns the whole circuit. Also correct the enforcement model to sequential lagging feedback.
Parallel planned sessions under load management must each be granted a whole effectiveMinPower chunk (charger runs >= min or off). At most floor(circuitBudget/effectiveMinPower) sessions fit a slot; the rest shift. Same semi-continuous rule as evcc-io/optimizer#91.
Under a shared circuit the reactive clamp grants budget in update order, so process fast/deadline-bound then higher-priority loadpoints first - they establish draw and the clamp throttles lower-priority ones, deterministic instead of config order. Foundation for loadmanagement-aware planning.
Reordering the round-robin update sequence does not help: updates are also event-driven and the circuit clamp reacts to measured power, so steady-state allocation is order-independent. Priority belongs in planner allocation (Phase 2), not the control loop. Round-robin sequence restored.
Tracks residual charging power per time slot on a shared circuit: Available/Reserve plus CanHost, the semi-continuous min-power gate (a charger runs >= min or off). Foundation for priority-ordered, circuit-aware planning. Single circuit + static budget for now; hierarchy TODO.
Generalise optimalPlan into planCapped: with no cap it is the old full-power duration accounting (optimalPlan delegates, behaviour unchanged); with a per-slot availability a slot delivers min(maxPower, avail) and is skipped below minPower (semi-continuous), so a power-limited slot counts less and the plan spills into more slots. Not yet wired into Plan - needs the ledger.
AllocateShared ties CapacityLedger + planCapped together: orders loadpoints forced-first then priority-desc, plans each against the residual budget with the semi-continuous min-power gate, and reserves its actual per-slot draw so later loadpoints see the remainder. Tests cover joint feasibility, slot sharing, and forced-beats-priority. Live site wiring still pending.
Without clamping, planCapped could pick globally-cheapest slots after the deadline. Add now and clamp per request; test that no slot is scheduled past the target.
computeSharedPlans groups planning loadpoints by circuit each cycle, calls AllocateShared, and stores each plan on the loadpoint; GetPlan returns it when set. Only power-limited circuits with 2+ contending loadpoints are allocated - single/no-circuit, precondition, continuous and no-tariff keep the independent path, and the reactive clamp stays the safety backstop.
The earlier revert (a42eeef) removed the test and doc but its site.go change was never committed, so the reorder stayed live. Remove the helper and restore the plain round-robin sequence; priority belongs in planner allocation, not the control loop.
Plan is reached both from the site control loop and from the /plan and /plan/preview API handlers, so the overrunWarned bookkeeping added for the warn-once behaviour was written from two goroutines. Move it behind a mutex in warnOverrun. Also drops CapacityLedger.CanHost, which had no caller outside its own test- planCapped inlines the same min-power check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The circuit clamp gave headroom to whichever loadpoint asked first, so a forced or plan-active loadpoint could be starved by a lower-priority peer that happened to update earlier. Reordering the round-robin update sequence was tried before and reverted- it is a near no-op, because the clamp reacts to measured power and is order-independent in steady state. The clamp itself has to become rank-aware. api.CircuitLoad gains GetRank (EffectivePriority plus a rankForced / rankPlanActive tier) and GetDeadlineDemand (the power and per-phase current a connected, deadline-bound loadpoint still needs, zero otherwise). Circuit stores the load list from Update, and ValidatePower/ValidateCurrent take the asking load and subtract the unmet deadline demand of strictly higher-ranked loads drawing through the same circuit, walking GetParent so a shared parent arbitrates across its children too. The reservation only ever reduces the asking load, never optimistically reclaims power already drawn, so the circuit stays within its limit at every point in time- the higher-ranked load claims the freed headroom on its own next update. AllocateShared now orders by that same rank instead of its own Forced/Priority pair, so the share a plan assumes and the share it actually gets agree. Two fixes to the shared-plan wiring while here: - GetPlan returned the site-assigned plan for any arguments, so the /plan/preview handler answered a hypothetical target with the committed plan. The plan now carries the target and duration it was computed for and is only returned for exactly that goal. - sharedPlan was read from API handler goroutines while the site wrote it; it is now behind the loadpoint mutex, and computeSharedPlans assigns in a single pass instead of clearing every loadpoint first and building each request twice. Co-Authored-By: Claude Opus 5 (1M context) <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.
Refs #24279
Problem
Planning is per-loadpoint and circuit-blind: each loadpoint independently picks the globally cheapest slots to meet its own target (
core/loadpoint_plan.go,core/planner/planner.go), unaware of the shared circuit budget or the other loadpoints. Circuit enforcement (Loadpoint.setLimit→circuit.ValidatePower/ValidateCurrent) is reactive and order-greedy: the first loadpoint in site iteration order grabs budget, later ones get the remainder. So two loadpoints that both planned the same cheap slot get clamped to half power, under-deliver, and miss their deadline — andminSocforced charging consumes budget another loadpoint's plan assumed it had, tripping load management (over current detected).Plan (phased TODOs)
planner.Plan/optimalPlanwith a per-slot power cap; site reserves forced load first, then plans loadpoints in priority order against the ledger.optimizerservice, only if greedy proves insufficient.Open decisions (defaults in the doc)
priority: yesNote that this somewhat doubles what the optimizer already does. It may still be helpful until actionable optimizer lands.
🤖 Generated with Claude Code