Planner: fix repeating overrun (#26529)
This commit is contained in:
parent
33ce3a494f
commit
2671732c32
9 changed files with 139 additions and 16 deletions
|
|
@ -152,6 +152,7 @@ type Loadpoint struct {
|
|||
planSlotEnd time.Time // current plan slot end time
|
||||
planActive bool // charge plan exists and has a currently active slot
|
||||
planOverrunSent bool // notification has been sent already
|
||||
planLocked PlanLock // locked plan
|
||||
|
||||
// cached state
|
||||
status api.ChargeStatus // Charger status
|
||||
|
|
@ -528,6 +529,9 @@ func (lp *Loadpoint) evVehicleDisconnectHandler() {
|
|||
// session is persisted during evChargeStopHandler which runs before
|
||||
lp.clearSession()
|
||||
|
||||
// clear locked plan goal on disconnect
|
||||
lp.clearPlanLock()
|
||||
|
||||
// phases are unknown when vehicle disconnects
|
||||
lp.ResetMeasuredPhases()
|
||||
|
||||
|
|
@ -984,8 +988,7 @@ func (lp *Loadpoint) repeatingPlanning() bool {
|
|||
if !lp.socBasedPlanning() {
|
||||
return false
|
||||
}
|
||||
_, _, id := lp.NextVehiclePlan()
|
||||
return id > 1
|
||||
return lp.getPlanId() > 1
|
||||
}
|
||||
|
||||
// vehicleHasSoc returns true if active vehicle supports returning soc, i.e. it is not an offline vehicle
|
||||
|
|
|
|||
|
|
@ -121,6 +121,8 @@ type API interface {
|
|||
GetPlanEnergy() (time.Time, float64)
|
||||
// SetPlanEnergy sets the charge plan energy
|
||||
SetPlanEnergy(time.Time, float64) error
|
||||
// ClearPlanLock clears the locked plan goal
|
||||
ClearPlanLock()
|
||||
// GetPlanGoal returns the plan goal and if the goal is soc based
|
||||
GetPlanGoal() (float64, bool)
|
||||
// GetPlanRequiredDuration returns required duration of plan to reach the goal from current state
|
||||
|
|
|
|||
|
|
@ -55,6 +55,18 @@ func (mr *MockAPIMockRecorder) ActivePhases() *gomock.Call {
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActivePhases", reflect.TypeOf((*MockAPI)(nil).ActivePhases))
|
||||
}
|
||||
|
||||
// ClearPlanLock mocks base method.
|
||||
func (m *MockAPI) ClearPlanLock() {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "ClearPlanLock")
|
||||
}
|
||||
|
||||
// ClearPlanLock indicates an expected call of ClearPlanLock.
|
||||
func (mr *MockAPIMockRecorder) ClearPlanLock() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearPlanLock", reflect.TypeOf((*MockAPI)(nil).ClearPlanLock))
|
||||
}
|
||||
|
||||
// EffectiveLimitSoc mocks base method.
|
||||
func (m *MockAPI) EffectiveLimitSoc() int {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
|||
|
|
@ -356,6 +356,9 @@ func (lp *Loadpoint) getPlanEnergy() (time.Time, float64) {
|
|||
|
||||
// setPlanEnergy sets plan target energy (no mutex)
|
||||
func (lp *Loadpoint) setPlanEnergy(finishAt time.Time, energy float64) {
|
||||
// clear locked goal when energy plan changes
|
||||
lp.clearPlanLock()
|
||||
|
||||
lp.planEnergy = energy
|
||||
lp.publish(keys.PlanEnergy, energy)
|
||||
lp.settings.SetFloat(keys.PlanEnergy, energy)
|
||||
|
|
|
|||
|
|
@ -61,15 +61,15 @@ func (lp *Loadpoint) nextActivePlan(maxPower float64, plans []plan) *plan {
|
|||
return nil
|
||||
}
|
||||
|
||||
// NextVehiclePlan returns the next vehicle plan time, soc and id
|
||||
func (lp *Loadpoint) NextVehiclePlan() (time.Time, int, int) {
|
||||
lp.RLock()
|
||||
defer lp.RUnlock()
|
||||
return lp.nextVehiclePlan()
|
||||
}
|
||||
|
||||
// nextVehiclePlan returns the next vehicle plan time, soc, id
|
||||
// Returns locked plan if available, otherwise calculates fresh
|
||||
func (lp *Loadpoint) nextVehiclePlan() (time.Time, int, int) {
|
||||
// return locked plan if available
|
||||
if p := lp.planLocked; p.Id > 0 {
|
||||
return p.Time, p.Soc, p.Id
|
||||
}
|
||||
|
||||
// calculate fresh plan
|
||||
if v := lp.GetVehicle(); v != nil {
|
||||
var plans []plan
|
||||
|
||||
|
|
@ -103,31 +103,41 @@ func (lp *Loadpoint) nextVehiclePlan() (time.Time, int, int) {
|
|||
|
||||
// EffectivePlanSoc returns the soc target for the current plan
|
||||
func (lp *Loadpoint) EffectivePlanSoc() int {
|
||||
_, soc, _ := lp.NextVehiclePlan()
|
||||
lp.RLock()
|
||||
defer lp.RUnlock()
|
||||
_, soc, _ := lp.nextVehiclePlan()
|
||||
return soc
|
||||
}
|
||||
|
||||
// EffectivePlanId returns the id for the current plan
|
||||
func (lp *Loadpoint) EffectivePlanId() int {
|
||||
// getPlanId returns the plan id of the current/next plan
|
||||
func (lp *Loadpoint) getPlanId() int {
|
||||
if lp.socBasedPlanning() {
|
||||
_, _, id := lp.NextVehiclePlan()
|
||||
_, _, id := lp.nextVehiclePlan()
|
||||
return id
|
||||
}
|
||||
if lp.planEnergy > 0 {
|
||||
return 1
|
||||
}
|
||||
// no plan
|
||||
return 0
|
||||
}
|
||||
|
||||
// EffectivePlanId returns the id for the current plan
|
||||
func (lp *Loadpoint) EffectivePlanId() int {
|
||||
lp.RLock()
|
||||
defer lp.RUnlock()
|
||||
return lp.getPlanId()
|
||||
}
|
||||
|
||||
// EffectivePlanTime returns the effective plan time
|
||||
func (lp *Loadpoint) EffectivePlanTime() time.Time {
|
||||
lp.RLock()
|
||||
defer lp.RUnlock()
|
||||
if lp.socBasedPlanning() {
|
||||
ts, _, _ := lp.NextVehiclePlan()
|
||||
ts, _, _ := lp.nextVehiclePlan()
|
||||
return ts
|
||||
}
|
||||
|
||||
ts, _ := lp.GetPlanEnergy()
|
||||
ts, _ := lp.getPlanEnergy()
|
||||
return ts
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -126,3 +126,38 @@ func TestNextPlan(t *testing.T) {
|
|||
assert.Equal(t, tc.planId, res.Id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanLocking(t *testing.T) {
|
||||
clk := clock.NewMock()
|
||||
now := clk.Now()
|
||||
|
||||
lp := NewLoadpoint(util.NewLogger("foo"), nil)
|
||||
lp.clock = clk
|
||||
|
||||
planTime := now.Add(2 * time.Hour)
|
||||
|
||||
t.Run("lock and unlock", func(t *testing.T) {
|
||||
lp.lockPlanGoal(planTime, 80, 2)
|
||||
|
||||
// locked values returned before plan target
|
||||
ts, soc, id := lp.nextVehiclePlan()
|
||||
assert.Equal(t, planTime, ts)
|
||||
assert.Equal(t, 80, soc)
|
||||
assert.Equal(t, 2, id)
|
||||
|
||||
clk.Add(3 * time.Hour) // advance past plan target
|
||||
|
||||
// locked values persist during overrun
|
||||
ts, soc, id = lp.nextVehiclePlan()
|
||||
assert.Equal(t, planTime, ts)
|
||||
assert.Equal(t, 80, soc)
|
||||
assert.Equal(t, 2, id)
|
||||
|
||||
// after clearing, lock is not returned
|
||||
lp.clearPlanLock()
|
||||
ts, soc, id = lp.nextVehiclePlan()
|
||||
assert.True(t, ts.IsZero())
|
||||
assert.Equal(t, 0, soc)
|
||||
assert.Equal(t, 0, id)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,40 @@ import (
|
|||
|
||||
// TODO planActive is not guarded by mutex
|
||||
|
||||
// PlanLock contains information about a locked plan
|
||||
type PlanLock struct {
|
||||
Time time.Time // target time (committed goal, persists during overrun)
|
||||
Soc int // target soc
|
||||
Id int // id (0=none, 1=static, 2+=repeating), needed to highlight the plan in ui
|
||||
}
|
||||
|
||||
// clearPlanLock clears the locked plan goal
|
||||
func (lp *Loadpoint) clearPlanLock() {
|
||||
lp.planLocked = PlanLock{}
|
||||
}
|
||||
|
||||
// ClearPlanLock clears the locked plan goal
|
||||
func (lp *Loadpoint) ClearPlanLock() {
|
||||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
lp.clearPlanLock()
|
||||
}
|
||||
|
||||
// lockPlanGoal locks the current plan goal to handle overruns (soc-based plans)
|
||||
func (lp *Loadpoint) lockPlanGoal(planTime time.Time, soc int, id int) {
|
||||
lp.planLocked = PlanLock{
|
||||
Time: planTime,
|
||||
Soc: soc,
|
||||
Id: id,
|
||||
}
|
||||
}
|
||||
|
||||
// setPlanActive updates plan active flag
|
||||
func (lp *Loadpoint) setPlanActive(active bool) {
|
||||
if !active {
|
||||
lp.planOverrunSent = false
|
||||
lp.planSlotEnd = time.Time{}
|
||||
lp.clearPlanLock()
|
||||
}
|
||||
if lp.planActive != active {
|
||||
lp.planActive = active
|
||||
|
|
@ -177,6 +206,11 @@ func (lp *Loadpoint) plannerActive() (active bool) {
|
|||
return false
|
||||
}
|
||||
|
||||
// lock the goal when soc-based plan becomes active for the first time
|
||||
if lp.planLocked.Id == 0 && isSocBased {
|
||||
lp.lockPlanGoal(planTime, int(goal), lp.getPlanId())
|
||||
}
|
||||
|
||||
// remember last active plan's slot end time
|
||||
lp.planSlotEnd = activeSlot.End
|
||||
} else if lp.planActive {
|
||||
|
|
|
|||
|
|
@ -495,6 +495,13 @@ func (site *Site) Publish(key string, val any) {
|
|||
site.publish(key, val)
|
||||
}
|
||||
|
||||
// clearPlanLocks clears locked plan goals for all loadpoints
|
||||
func (site *Site) clearPlanLocks() {
|
||||
for _, lp := range site.Loadpoints() {
|
||||
lp.ClearPlanLock()
|
||||
}
|
||||
}
|
||||
|
||||
func (site *Site) collectMeters(key string, meters []config.Device[api.Meter]) []measurement {
|
||||
mm := make([]measurement, len(meters))
|
||||
|
||||
|
|
@ -1049,6 +1056,7 @@ func (site *Site) prepare() {
|
|||
site.publishVehicles()
|
||||
site.publishTariffs(0, 0)
|
||||
vehicle.Publish = site.publishVehicles
|
||||
vehicle.ClearPlanLocks = site.clearPlanLocks
|
||||
}
|
||||
|
||||
// Prepare attaches communication channels to site and loadpoints
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ var _ API = (*adapter)(nil)
|
|||
// Publish publishes vehicle updates at site level
|
||||
var Publish func()
|
||||
|
||||
// ClearPlanLocks clears locked plan goals across all loadpoints
|
||||
var ClearPlanLocks func()
|
||||
|
||||
type adapter struct {
|
||||
log *util.Logger
|
||||
name string
|
||||
|
|
@ -32,6 +35,12 @@ func (v *adapter) publish() {
|
|||
}
|
||||
}
|
||||
|
||||
func (v *adapter) clearPlanLocks() {
|
||||
if ClearPlanLocks != nil {
|
||||
ClearPlanLocks()
|
||||
}
|
||||
}
|
||||
|
||||
func (v *adapter) Instance() api.Vehicle {
|
||||
return v.Vehicle
|
||||
}
|
||||
|
|
@ -100,6 +109,9 @@ func (v *adapter) SetPlanSoc(ts time.Time, soc int) error {
|
|||
settings.SetTime(v.key()+keys.PlanTime, ts)
|
||||
settings.SetInt(v.key()+keys.PlanSoc, int64(soc))
|
||||
|
||||
// note: could be optimized by only clearing plan lock of the relevant loadpoint
|
||||
v.clearPlanLocks()
|
||||
|
||||
v.publish()
|
||||
|
||||
return nil
|
||||
|
|
@ -125,6 +137,10 @@ func (v *adapter) SetRepeatingPlans(plans []api.RepeatingPlan) error {
|
|||
}
|
||||
|
||||
v.log.DEBUG.Printf("update repeating plans for %s to: %v", v.name, plans)
|
||||
|
||||
// note: could be optimized by only clearing plan lock of the relevant loadpoint
|
||||
v.clearPlanLocks()
|
||||
|
||||
v.publish()
|
||||
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue