diff --git a/core/planner/planner.go b/core/planner/planner.go index c0bbe7ecb9c..fe34af7f71e 100644 --- a/core/planner/planner.go +++ b/core/planner/planner.go @@ -154,6 +154,11 @@ func (t *Planner) Plan(requiredDuration, precondition time.Duration, targetTime rates = clampRates(rates, now, targetTime) + // check if rate coverage is sufficient for planning + if len(rates) == 0 || rates[len(rates)-1].End.Sub(rates[0].Start) < requiredDuration { + return simplePlan + } + // don't precondition longer than charging duration precondition = min(precondition, requiredDuration) @@ -175,25 +180,6 @@ func (t *Planner) Plan(requiredDuration, precondition time.Duration, targetTime // create plan unless only precond slots remaining var plan api.Rates if continuous { - // check if available tariff slots span is sufficient for sliding window algorithm - // verify that actual tariff data covers enough duration (may have gaps or start late) - if len(rates) > 0 { - start := rates[0].Start - if start.Before(now) { - start = now - } - - end := rates[len(rates)-1].End - if end.After(targetTime) { - end = targetTime - } - - // available window too small for sliding window - charge continuously from now to target - if end.Sub(start) < requiredDuration { - return continuousPlan(append(rates, precond...), now, targetTime.Add(precondition)) - } - } - // find cheapest continuous window plan = findContinuousWindow(rates, requiredDuration, targetTime) } else { diff --git a/core/planner/planner_continuous_test.go b/core/planner/planner_continuous_test.go index ff383ea874c..4f9c97fc28a 100644 --- a/core/planner/planner_continuous_test.go +++ b/core/planner/planner_continuous_test.go @@ -548,8 +548,8 @@ func TestContinuous_StartBeforeRates(t *testing.T) { // TestContinuous_StartBeforeRatesInsufficientTime tests that when current time // is before the first available rate AND there's not enough time after rates -// start to complete charging before target, the planner starts charging as soon -// as rates become available (best effort approach) +// start to complete charging before target, the planner starts at the latest +// possible time to reach target (best effort approach) func TestContinuous_StartBeforeRatesInsufficientTime(t *testing.T) { now := time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) c := clock.NewMock() @@ -581,8 +581,8 @@ func TestContinuous_StartBeforeRatesInsufficientTime(t *testing.T) { require.NotEmpty(t, plan, "plan should not be empty") - // Best effort: start immediately to maximize charging time - assert.Equal(t, now, plan[0].Start, "should start immediately") + // Best effort: start at latest possible time to maximize charging time + assert.Equal(t, now.Add(1*time.Hour), plan[0].Start, "should start at latest possible time") assert.Equal(t, 0.0, plan[0].Value, "gap-filling slot before rates has no price") } diff --git a/core/planner/planner_test.go b/core/planner/planner_test.go index 011d7a4734c..e6db1640e01 100644 --- a/core/planner/planner_test.go +++ b/core/planner/planner_test.go @@ -497,9 +497,8 @@ func TestStartBeforeRates(t *testing.T) { } // TestStartBeforeRatesInsufficientTime tests that when current time -// is before the first available rate AND there's not enough time after rates -// start to complete charging before target, the planner starts charging as soon -// as rates become available (best effort approach) +// is before the first available rate AND there's not enough rate coverage +// to complete charging, the planner falls back to simplePlan (ignoring rates) func TestStartBeforeRatesInsufficientTime(t *testing.T) { now := time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) c := clock.NewMock() @@ -525,13 +524,32 @@ func TestStartBeforeRatesInsufficientTime(t *testing.T) { } targetTime := now.Add(4 * time.Hour) - requiredDuration := 3 * time.Hour // Need 3h but only 2h available after rates start + requiredDuration := 3 * time.Hour // Need 3h but only 2h rate coverage plan := planner.Plan(requiredDuration, 0, targetTime, false) // dispersed mode - require.NotEmpty(t, plan, "plan should not be empty - starts when rates become available") + require.NotEmpty(t, plan, "plan should not be empty") + + // Insufficient rate coverage: fall back to simplePlan starting at latestStart + assert.Equal(t, now.Add(1*time.Hour), plan[0].Start, "should start at latestStart (target - required)") + assert.Equal(t, 0.0, plan[0].Value, "simplePlan has no price info") +} + +// TestEmptyRatesAfterClamping tests fallback to simplePlan when no rates cover [now, targetTime] +func TestEmptyRatesAfterClamping(t *testing.T) { + c := clock.NewMock() + ctrl := gomock.NewController(t) + + trf := api.NewMockTariff(ctrl) + trf.EXPECT().Rates().AnyTimes().Return(rates([]float64{0.20}, c.Now().Add(3*time.Hour), time.Hour), nil) + + p := &Planner{ + log: util.NewLogger("test"), + clock: c, + tariff: trf, + } - // Best effort: start as soon as rates are available - assert.Equal(t, now.Add(2*time.Hour), plan[0].Start, "should start at first available rate") - assert.Equal(t, 0.10, plan[0].Value, "should use first available rate price") + plan := p.Plan(time.Hour, 0, c.Now().Add(90*time.Minute), false) + require.Len(t, plan, 1) + assert.Equal(t, c.Now().Add(30*time.Minute), plan[0].Start) }