From 285b22ee6e2021c0573fb331454ef015ebe0e543 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Wed, 15 Jan 2025 15:40:39 +0100 Subject: [PATCH] Planner: improve plan selection (#18211) --- core/loadpoint_effective.go | 47 +++++++++++++++++++++----------- core/loadpoint_effective_test.go | 37 +++++++++++++++++++++++++ tests/plan.spec.js | 1 + 3 files changed, 69 insertions(+), 16 deletions(-) diff --git a/core/loadpoint_effective.go b/core/loadpoint_effective.go index 7f16f5f946..716f7f4e85 100644 --- a/core/loadpoint_effective.go +++ b/core/loadpoint_effective.go @@ -1,7 +1,7 @@ package core import ( - "sort" + "slices" "time" "github.com/evcc-io/evcc/api" @@ -31,20 +31,39 @@ func (lp *Loadpoint) EffectivePriority() int { return lp.GetPriority() } +type plan struct { + Id int + Start time.Time // last possible start time + End time.Time // user-selected finish time + Soc int +} + +func (lp *Loadpoint) nextActivePlan(maxPower float64, plans []plan) *plan { + for i, p := range plans { + requiredDuration := lp.GetPlanRequiredDuration(float64(p.Soc), maxPower) + plans[i].Start = p.End.Add(-requiredDuration) + } + + // sort plans by start time + slices.SortStableFunc(plans, func(i, j plan) int { + return i.Start.Compare(j.Start) + }) + + if len(plans) > 0 { + return &plans[0] + } + + return nil +} + // nextVehiclePlan returns the next vehicle plan time, soc and id func (lp *Loadpoint) nextVehiclePlan() (time.Time, int, int) { if v := lp.GetVehicle(); v != nil { - type plan struct { - Time time.Time - Soc int - Id int - } - var plans []plan // static plan if planTime, soc := vehicle.Settings(lp.log, v).GetPlanSoc(); soc != 0 { - plans = append(plans, plan{Id: 1, Soc: soc, Time: planTime}) + plans = append(plans, plan{Id: 1, Soc: soc, End: planTime}) } // repeating plans @@ -59,16 +78,12 @@ func (lp *Loadpoint) nextVehiclePlan() (time.Time, int, int) { continue } - plans = append(plans, plan{Id: index + 2, Soc: rp.Soc, Time: time}) + plans = append(plans, plan{Id: index + 2, Soc: rp.Soc, End: time}) } - // sort plans by time - sort.Slice(plans, func(i, j int) bool { - return plans[i].Time.Before(plans[j].Time) - }) - - if len(plans) > 0 { - return plans[0].Time, plans[0].Soc, plans[0].Id + // calculate earliest required plan start + if plan := lp.nextActivePlan(lp.EffectiveMaxPower(), plans); plan != nil { + return plan.End, plan.Soc, plan.Id } } return time.Time{}, 0, 0 diff --git a/core/loadpoint_effective_test.go b/core/loadpoint_effective_test.go index f216547824..80c41e70f4 100644 --- a/core/loadpoint_effective_test.go +++ b/core/loadpoint_effective_test.go @@ -2,10 +2,13 @@ package core import ( "testing" + "time" + "github.com/benbjohnson/clock" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" ) @@ -68,3 +71,37 @@ func TestEffectiveMinMaxCurrent(t *testing.T) { assert.Equal(t, tc.effectiveMax, lp.effectiveMaxCurrent(), "max") } } + +func TestNextPlan(t *testing.T) { + clock := clock.NewMock() + + ctrl := gomock.NewController(t) + lp := NewLoadpoint(util.NewLogger("foo"), nil) + lp.charger = api.NewMockCharger(ctrl) + + for _, tc := range []struct { + planId int + plans []plan + }{ + {1, []plan{ + {Id: 1, End: clock.Now().Add(8 * time.Hour), Soc: 10}, + {Id: 2, End: clock.Now().Add(10 * time.Hour), Soc: 10}, + }}, + {1, []plan{ + {Id: 1, End: clock.Now().Add(8 * time.Hour), Soc: 20}, + {Id: 2, End: clock.Now().Add(9 * time.Hour), Soc: 20}, + }}, + {2, []plan{ + {Id: 2, End: clock.Now().Add(8 * time.Hour), Soc: 20}, + {Id: 1, End: clock.Now().Add(9 * time.Hour), Soc: 20}, + }}, + {2, []plan{ + {Id: 1, End: clock.Now().Add(8 * time.Hour), Soc: 10}, + {Id: 2, End: clock.Now().Add(10 * time.Hour), Soc: 60}, + }}, + } { + res := lp.nextActivePlan(1e4, tc.plans) + require.NotNil(t, res) + assert.Equal(t, tc.planId, res.Id) + } +} diff --git a/tests/plan.spec.js b/tests/plan.spec.js index e07ecf4cd7..ffe1eb582d 100644 --- a/tests/plan.spec.js +++ b/tests/plan.spec.js @@ -497,6 +497,7 @@ test.describe("repeating", async () => { const plan1 = modal.getByTestId("plan-entry").nth(0); await plan1.getByTestId("static-plan-day").selectOption({ index: 1 }); await plan1.getByTestId("static-plan-time").fill("09:30"); + await plan1.getByTestId("static-plan-soc").selectOption("80%"); await plan1.getByTestId("static-plan-active").click(); // add repeating plan for tomorrow