Planner: fix continuous plan selecting the current slot (#32041)
This commit is contained in:
parent
1b16f23da3
commit
ea6cc99094
2 changed files with 58 additions and 4 deletions
|
|
@ -1,12 +1,12 @@
|
|||
package planner
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"iter"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/samber/lo/it"
|
||||
)
|
||||
|
||||
// Start returns the earliest slot's start time
|
||||
|
|
@ -134,6 +134,36 @@ func clampRatesSeq(rates api.Rates, start, end time.Time) iter.Seq[api.Rate] {
|
|||
}
|
||||
}
|
||||
|
||||
// windowCost returns the cost of the given window. Durations are summed per price before
|
||||
// multiplication, hence the result does not depend on how the window's slots are clamped.
|
||||
func windowCost(rates api.Rates, start, end time.Time) float64 {
|
||||
type pricedDuration struct {
|
||||
value float64
|
||||
dur time.Duration
|
||||
}
|
||||
|
||||
var acc []pricedDuration
|
||||
for r := range clampRatesSeq(rates, start, end) {
|
||||
dur := r.End.Sub(r.Start)
|
||||
|
||||
if idx := slices.IndexFunc(acc, func(p pricedDuration) bool { return p.value == r.Value }); idx >= 0 {
|
||||
acc[idx].dur += dur
|
||||
continue
|
||||
}
|
||||
|
||||
acc = append(acc, pricedDuration{r.Value, dur})
|
||||
}
|
||||
|
||||
slices.SortFunc(acc, func(i, j pricedDuration) int { return cmp.Compare(i.value, j.value) })
|
||||
|
||||
var cost float64
|
||||
for _, p := range acc {
|
||||
cost += p.value * float64(p.dur)
|
||||
}
|
||||
|
||||
return cost
|
||||
}
|
||||
|
||||
// findContinuousWindow finds the cheapest continuous window of slots for the given duration.
|
||||
// - rates are filtered to [now, targetTime] window by caller
|
||||
// Returns the selected rates.
|
||||
|
|
@ -147,9 +177,7 @@ func findContinuousWindow(rates api.Rates, effectiveDuration time.Duration, targ
|
|||
break
|
||||
}
|
||||
|
||||
cost := it.SumBy(clampRatesSeq(rates[i:], rates[i].Start, windowEnd), func(r api.Rate) float64 {
|
||||
return float64(r.End.Sub(r.Start)) * r.Value
|
||||
})
|
||||
cost := windowCost(rates[i:], rates[i].Start, windowEnd)
|
||||
|
||||
// Prefer later start if equal cost
|
||||
if bestCost == nil || cost <= *bestCost {
|
||||
|
|
|
|||
|
|
@ -105,6 +105,32 @@ func TestSlotAt(t *testing.T) {
|
|||
require.True(t, SlotAt(now.Add(3*time.Hour), plan).IsZero())
|
||||
}
|
||||
|
||||
// TestFindContinuousWindowFlatRates ensures that equal-cost windows always select the
|
||||
// latest slot. The current slot is clamped to now, hence its cost is summed from
|
||||
// different addends than the aligned slots and must not decide the comparison.
|
||||
func TestFindContinuousWindowFlatRates(t *testing.T) {
|
||||
day := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
rr := rates(lo.RepeatBy(3*96, func(int) float64 { return 0.291 }), day, tariff.SlotDuration)
|
||||
target := day.Add(2 * 24 * time.Hour).Add(6*time.Hour + 45*time.Minute)
|
||||
|
||||
for _, tc := range []struct {
|
||||
now time.Duration
|
||||
duration time.Duration
|
||||
}{
|
||||
{16*time.Hour + 45*time.Minute + 35*time.Second, 15*time.Minute + 30*time.Second + 464868247},
|
||||
{16*time.Hour + 45*time.Minute + 37*time.Second, 15*time.Minute + 30*time.Second + 239819456},
|
||||
{16*time.Hour + 46*time.Minute + 2*time.Second, 26*time.Minute + 39*time.Second + 438201128},
|
||||
} {
|
||||
now := day.Add(tc.now)
|
||||
|
||||
plan := findContinuousWindow(clampRates(rr, now, target), tc.duration, target)
|
||||
require.NotEmpty(t, plan)
|
||||
|
||||
latest := target.Add(-tc.duration).Truncate(tariff.SlotDuration)
|
||||
assert.Equal(t, latest, Start(plan), "now %v, duration %v", now, tc.duration)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFindContinuousWindow(b *testing.B) {
|
||||
rr := rates(lo.RepeatBy(96, func(i int) float64 {
|
||||
return float64(i)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue