diff --git a/core/site_optimizer.go b/core/site_optimizer.go index 3952062ba..d19de7f86 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -413,8 +413,7 @@ func (site *Site) optimizerRequest(battery []types.Measurement) (optimizer.Optim } if optimizerURI() == OPTIMIZER_URI { - // limit to 2 days for sake of performance - minLen = min(2*96, minLen) + minLen = slotsUntil(grid, optimizerHorizon(time.Now()), minLen) } if expectedSlots := 8; minLen < expectedSlots { @@ -1127,6 +1126,27 @@ func currentRates(tariff api.Tariff) api.Rates { }) } +// optimizerHorizon is the timeframe the hosted optimizer is limited to for sake +// of performance: 48 hours, extended to the end of that day. In the early hours +// the extension would add almost a full day, hence it only applies past 6:00. +func optimizerHorizon(t time.Time) time.Time { + horizon := t.Add(48 * time.Hour) + if t.Hour() < 6 { + return horizon + } + return now.With(horizon).EndOfDay() +} + +// slotsUntil limits maxLen to the slots starting before the given horizon +func slotsUntil(rates api.Rates, horizon time.Time, maxLen int) int { + if i := slices.IndexFunc(rates[:min(maxLen, len(rates))], func(slot api.Rate) bool { + return slot.Start.After(horizon) + }); i >= 0 { + return i + } + return maxLen +} + func timeSteps(minLen int, now time.Time) []int { res := make([]int, 0, minLen) diff --git a/core/site_optimizer_test.go b/core/site_optimizer_test.go index 446e8889c..94c99e743 100644 --- a/core/site_optimizer_test.go +++ b/core/site_optimizer_test.go @@ -7,6 +7,7 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/core/loadpoint" "github.com/evcc-io/evcc/core/types" + "github.com/evcc-io/evcc/tariff" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/config" optimizer "github.com/evcc-io/optimizer/client" @@ -29,6 +30,30 @@ func TestLoadpointProfile(t *testing.T) { require.Equal(t, []float64{250, 250, 250, 250, 250, 250, 250, 50}, loadpointProfile(lp, 8)) } +func TestOptimizerHorizon(t *testing.T) { + ts := time.Date(2025, 1, 1, 10, 30, 0, 0, time.Local) + horizon := optimizerHorizon(ts) + + // 48h plus end of day + assert.Equal(t, time.Date(2025, 1, 3, 23, 59, 59, int(time.Second-time.Nanosecond), time.Local), horizon) + + // before 6:00 the day is not extended + assert.Equal(t, time.Date(2025, 1, 3, 5, 30, 0, 0, time.Local), + optimizerHorizon(time.Date(2025, 1, 1, 5, 30, 0, 0, time.Local))) + + rates := make(api.Rates, 0, 4*96) + for slot := ts.Truncate(tariff.SlotDuration); len(rates) < cap(rates); slot = slot.Add(tariff.SlotDuration) { + rates = append(rates, api.Rate{Start: slot, End: slot.Add(tariff.SlotDuration)}) + } + + // 4 days of slots from 10:30, capped at the last slot of Jan 3rd + assert.Equal(t, 246, slotsUntil(rates, horizon, len(rates))) + assert.Equal(t, time.Date(2025, 1, 3, 23, 45, 0, 0, time.Local), rates[245].Start) + + // shorter forecast is not extended + assert.Equal(t, 8, slotsUntil(rates, horizon, 8)) +} + func TestApplyPrecondition(t *testing.T) { ctrl := gomock.NewController(t)