Skip to content

Planner: grouped planning, cost tolerant#24289

Closed
iseeberg79 wants to merge 22 commits into
evcc-io:masterfrom
iseeberg79:feature/optimize-15m-planning
Closed

Planner: grouped planning, cost tolerant#24289
iseeberg79 wants to merge 22 commits into
evcc-io:masterfrom
iseeberg79:feature/optimize-15m-planning

Conversation

@iseeberg79
Copy link
Copy Markdown
Contributor

@iseeberg79 iseeberg79 commented Oct 9, 2025

Choose time windows instead of single slots, cost-optimized. Avoid short charge durations to protect devices in case of short slot durations. The optimization uses a constant (InterruptionPenaltyPercent) to define when it's valid to fragment charging windows to reduce total costs. Other optimizations are also possible. It respects preconditioning and actually sorts windows by:

    // 1. Lower cost (primary)
    // 2. Longer duration (secondary - for hardware protection)
    // 3. Later start time (tertiary)

It defines also new tests as the logic for choosen slots changed a lot.
Additionally I've added some statistics to clarify usage of "penalty" and added a selector to limit fragmented charging by a maximum charging windows contraint.

It has been created using Claude.

TODO:

  • agree on algorithm and used percentage for optimization

Screenshot:
image

@iseeberg79
Copy link
Copy Markdown
Contributor Author

@andig adapted planner for small slot duration

@andig andig added the enhancement New feature or request label Oct 10, 2025
Comment thread core/planner/planner_test.go Outdated
Comment thread core/planner/planner_test.go Outdated

firstSlot := plan[0]
slotDuration := firstSlot.End.Sub(firstSlot.Start)
assert.True(t, slotDuration <= time.Hour, "first slot should be at most 60min")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Das ergibt keinen Sinn. Bei Stundenslots ist das immer der Fall.

assert.Equal(t, simplePlan, plan, "expected simple plan")
}

func TestPrecondition(t *testing.T) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funktionieren diese Tests auch mit dem alten Planner? Falls ja wäre es schön die alten Tests stehen zu lassen und nur weitere hinzu zu fügen. Haben die Änderungen mit Slot Bundling überhaupt etwas zu tun?

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor Author

@iseeberg79 iseeberg79 Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

habe die alten Testfälle wiederhergestellt, es brauchte zwei Korrekturen:

  1. bezüglich der Verschiebung in einem Zeitfenster (m.E. vernachlässigbar, daher angepasst)
  2. Berücksichtigung von slotDuration statt time.Hour, die im erweiterten Testfall nötig wird

Durch die Aufteilung kann die neue Logik gegen die alten Tests geprüft werden, und die Neuen sind klar getrennt.

Comment thread core/planner/planner_test.go Outdated
}

// TestPreconditionLimiting verifies that precondition is limited to required duration
func TestPreconditionLimiting(t *testing.T) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dito- unabhängiger PR?

Comment thread core/planner/planner.go Outdated
// Applied as percentage of average cost - fragmentation only occurs if it saves more than this
//
// Special values:
// - 0.00: No penalty, pure cost optimization (maximum fragmentation, not recommended)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the current behavior. All existing tests should pass?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, they do - checking against the new behaviour needs adaption as it's currently taking a prepared test scenario to validate the penality. Needs double checking.

Comment thread core/planner/planner.go Outdated
// MaxChargingWindows limits the number of separate charging windows
// This protects hardware (contactors, battery management) from excessive switching cycles
// Recommended: 2-4 windows for most use cases
MaxChargingWindows = 3
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Number of windows only makes sense relative to total length of session. Why shouldn't we interrupt a 10h session 10 times if it helps?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you interrupt it 10 times, it might be because of the prices up-down in quarters. Yes, you have 10h session length but if you interrupt it 2 times per hour at beginning of the session. Wanted?

Copy link
Copy Markdown
Member

@andig andig Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 1-hour windows found be fine for me. 10 15min windows wouldn't be. But may that's subjective. Still, 3 gaps for a 45min plan out of 1:15 total time (i.e. on-off-on-off-on) is not a good plan.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really difficult, either adapt the percentage or a maximum. I agree setting 3 is not optimal but may be kind of sweet-spot. I like the idea of a slider for the percentage, using this makes maximum slots obsolete.

It was to ensure pauses of short slots aren't causing trouble for the hardware as they might happen in extreme situations.

We could skip the constant or go for a formula instead (duration / windows count > x) for security. Let's discuss.

Comment thread core/planner/planner.go Outdated

// Costs are essentially equal - prefer longer duration for hardware protection
// This reduces fragmentation when costs are identical
if a.duration > b.duration {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this condition already part of the cost comparison if gaps have penalties?

Comment thread core/planner/planner.go Outdated
var bestCandidate *planCandidate

// Strategy 1: Try single continuous window first (best for hardware)
for i := range windows {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed? 1 windows seems like a special case of N windows.

Comment thread core/planner/planner.go
}

if overlaps {
continue
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overlap heisst ja nicht, dass es nicht dennoch Zeit zu Anfang oder Ende hinzu fügen könnte- ist das korrekt?

Comment thread core/planner/planner.go Outdated

// generateChargingWindows creates all possible continuous charging windows
// AND individual slots as single-slot windows
func (t *Planner) generateChargingWindows(slots api.Rates) []chargingWindow {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bei 2 Tagen Forcast sind das wieviele Kombinationen (48h*4)?

@jeffborg
Copy link
Copy Markdown
Contributor

Can I add if during solar hours it's not really start/stopping usually will be ramp up (slot) and ramp down (back to solar) so no hardware wear and tear. (Also in Australia on amber so forecasts are almost meaningless and should be taken as a super rough guide so it's very often evcc will just start the plan now because the current price slot is cheaper than the forecast-ed prices)

@iseeberg79
Copy link
Copy Markdown
Contributor Author

iseeberg79 commented Oct 14, 2025

Could look like

image image image

(Three is not count of windows in a plan, just random)

Optimizing the cost-optimized slots to use minor charge windows recursively, verifying that single window strategy is never cheaper.

@andig
Copy link
Copy Markdown
Member

andig commented Oct 14, 2025

Make "min. Abstand" a "Continuity"/"Kontinuität"? Not a fixed value but a cost parameter?

@iseeberg79
Copy link
Copy Markdown
Contributor Author

Not self explaining, yet? It's not really related to any cost function. It just defines how many slots between elements of plan are allowed to kept free (minimum)

@andig
Copy link
Copy Markdown
Member

andig commented Oct 14, 2025

It‘s irrelevant how long the gap is when you have many gaps. I think we want low number of gaps, ie. Continuity.

@iseeberg79

This comment has been minimized.

@iseeberg79
Copy link
Copy Markdown
Contributor Author

superseeded by #24423

@iseeberg79 iseeberg79 closed this Oct 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants