Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 5 additions & 19 deletions core/planner/planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
iseeberg79 marked this conversation as resolved.
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)

Expand All @@ -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 {
Expand Down
8 changes: 4 additions & 4 deletions core/planner/planner_continuous_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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")
}

Expand Down
34 changes: 26 additions & 8 deletions core/planner/planner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
}
Loading