From f3a2b667ca3161cbcc659e7c02e487d66e13f5df Mon Sep 17 00:00:00 2001 From: Ingo <71161062+iseeberg79@users.noreply.github.com> Date: Wed, 31 Dec 2025 00:13:38 +0100 Subject: [PATCH 1/5] on missing rates start latest --- core/planner/planner.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/planner/planner.go b/core/planner/planner.go index d27a2f88bf8..991b1a4a4c7 100644 --- a/core/planner/planner.go +++ b/core/planner/planner.go @@ -187,9 +187,9 @@ func (t *Planner) Plan(requiredDuration, precondition time.Duration, targetTime end = targetTime } - // available window too small for sliding window - charge continuously from now to target + // available window too small for sliding window - charge continuously to target if end.Sub(start) < requiredDuration { - return continuousPlan(append(rates, precond...), now, targetTime.Add(precondition)) + return continuousPlan(append(rates, precond...), latestStart, targetTime.Add(precondition)) } } From 038d8d3c2f1d8c9b53ed8aea8712858e80f66bf5 Mon Sep 17 00:00:00 2001 From: Ingo <71161062+iseeberg79@users.noreply.github.com> Date: Thu, 1 Jan 2026 13:42:07 +0100 Subject: [PATCH 2/5] adapt test --- core/planner/planner_continuous_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/planner/planner_continuous_test.go b/core/planner/planner_continuous_test.go index ff383ea874c..f59642bf653 100644 --- a/core/planner/planner_continuous_test.go +++ b/core/planner/planner_continuous_test.go @@ -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") } From b89653f384b4fd245963d5444eed1e9997150274 Mon Sep 17 00:00:00 2001 From: Ingo <71161062+iseeberg79@users.noreply.github.com> Date: Thu, 1 Jan 2026 13:59:55 +0100 Subject: [PATCH 3/5] fix comment --- core/planner/planner_continuous_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/planner/planner_continuous_test.go b/core/planner/planner_continuous_test.go index f59642bf653..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() From ed4f3c002bdd527304d489cc23011af0ca78b608 Mon Sep 17 00:00:00 2001 From: Ingo <71161062+iseeberg79@users.noreply.github.com> Date: Thu, 1 Jan 2026 14:45:18 +0100 Subject: [PATCH 4/5] handle incomplete rates case to reach target --- core/planner/planner.go | 24 +++++------------------- core/planner/planner_test.go | 15 +++++++-------- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/core/planner/planner.go b/core/planner/planner.go index 991b1a4a4c7..499ab5f79ef 100644 --- a/core/planner/planner.go +++ b/core/planner/planner.go @@ -153,6 +153,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) @@ -174,25 +179,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 to target - if end.Sub(start) < requiredDuration { - return continuousPlan(append(rates, precond...), latestStart, targetTime.Add(precondition)) - } - } - // find cheapest continuous window plan = findContinuousWindow(rates, requiredDuration, targetTime) } else { diff --git a/core/planner/planner_test.go b/core/planner/planner_test.go index e3825e21b68..04f14d1208f 100644 --- a/core/planner/planner_test.go +++ b/core/planner/planner_test.go @@ -502,9 +502,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() @@ -530,13 +529,13 @@ 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") - // 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") + // 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") } From b5cda20d788652339e27c84117e91e868b4987b9 Mon Sep 17 00:00:00 2001 From: Ingo <71161062+iseeberg79@users.noreply.github.com> Date: Fri, 2 Jan 2026 00:45:46 +0100 Subject: [PATCH 5/5] check --- core/planner/planner.go | 2 +- core/planner/planner_test.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/core/planner/planner.go b/core/planner/planner.go index fd6a61968c8..fe34af7f71e 100644 --- a/core/planner/planner.go +++ b/core/planner/planner.go @@ -155,7 +155,7 @@ 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 { + if len(rates) == 0 || rates[len(rates)-1].End.Sub(rates[0].Start) < requiredDuration { return simplePlan } diff --git a/core/planner/planner_test.go b/core/planner/planner_test.go index 5d508e44fb7..e6db1640e01 100644 --- a/core/planner/planner_test.go +++ b/core/planner/planner_test.go @@ -534,3 +534,22 @@ func TestStartBeforeRatesInsufficientTime(t *testing.T) { 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, + } + + 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) +}