Planner: add precondition phase before departure (#20431)
This commit is contained in:
parent
c4be8e918d
commit
832012ef15
41 changed files with 746 additions and 222 deletions
|
|
@ -70,6 +70,7 @@ const (
|
|||
PlanTime = "planTime" // charge plan finish time goal
|
||||
PlanEnergy = "planEnergy" // charge plan energy goal
|
||||
PlanSoc = "planSoc" // charge plan soc goal
|
||||
PlanPrecondition = "planPrecondition" // charge plan precondition duration
|
||||
PlanActive = "planActive" // charge plan has determined current slot to be an active slot
|
||||
PlanProjectedStart = "planProjectedStart" // charge plan start time (earliest slot)
|
||||
PlanProjectedEnd = "planProjectedEnd" // charge plan ends (end of last slot)
|
||||
|
|
|
|||
|
|
@ -140,11 +140,12 @@ type Loadpoint struct {
|
|||
socEstimator *soc.Estimator
|
||||
|
||||
// charge planning
|
||||
planner *planner.Planner
|
||||
planTime time.Time // time goal
|
||||
planEnergy float64 // Plan charge energy in kWh (dumb vehicles)
|
||||
planSlotEnd time.Time // current plan slot end time
|
||||
planActive bool // charge plan exists and has a currently active slot
|
||||
planner *planner.Planner
|
||||
planTime time.Time // time goal
|
||||
planPrecondition time.Duration // precondition duration
|
||||
planEnergy float64 // Plan charge energy in kWh (dumb vehicles)
|
||||
planSlotEnd time.Time // current plan slot end time
|
||||
planActive bool // charge plan exists and has a currently active slot
|
||||
|
||||
// cached state
|
||||
status api.ChargeStatus // Charger status
|
||||
|
|
@ -342,8 +343,9 @@ func (lp *Loadpoint) restoreSettings() {
|
|||
|
||||
t, err1 := lp.settings.Time(keys.PlanTime)
|
||||
v, err2 := lp.settings.Float(keys.PlanEnergy)
|
||||
d, _ := lp.settings.Int(keys.PlanPrecondition)
|
||||
if err1 == nil && err2 == nil {
|
||||
lp.setPlanEnergy(t, v)
|
||||
lp.setPlanEnergy(t, time.Duration(d)*time.Second, v)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -657,6 +659,7 @@ func (lp *Loadpoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
|
|||
// restored settings
|
||||
lp.publish(keys.PlanTime, lp.planTime)
|
||||
lp.publish(keys.PlanEnergy, lp.planEnergy)
|
||||
lp.publish(keys.PlanPrecondition, lp.planPrecondition)
|
||||
lp.publish(keys.LimitSoc, lp.limitSoc)
|
||||
lp.publish(keys.LimitEnergy, lp.limitEnergy)
|
||||
|
||||
|
|
@ -953,7 +956,7 @@ func (lp *Loadpoint) repeatingPlanning() bool {
|
|||
if !lp.socBasedPlanning() {
|
||||
return false
|
||||
}
|
||||
_, _, id := lp.NextVehiclePlan()
|
||||
_, _, _, id := lp.NextVehiclePlan()
|
||||
return id > 1
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -114,17 +114,19 @@ type API interface {
|
|||
//
|
||||
|
||||
// GetPlanEnergy returns the charge plan energy
|
||||
GetPlanEnergy() (time.Time, float64)
|
||||
GetPlanEnergy() (time.Time, time.Duration, float64)
|
||||
// SetPlanEnergy sets the charge plan energy
|
||||
SetPlanEnergy(time.Time, float64) error
|
||||
// GetPlanGoal returns the plan goal and if the goal is soc based
|
||||
SetPlanEnergy(time.Time, time.Duration, float64) error
|
||||
// GetPlanGoal returns the plan goal, precondition duration and if the goal is soc based
|
||||
GetPlanGoal() (float64, bool)
|
||||
// GetPlanRequiredDuration returns required duration of plan to reach the goal from current state
|
||||
GetPlanRequiredDuration(goal, maxPower float64) time.Duration
|
||||
// GetPlanPreCondDuration returns the precondition duration
|
||||
GetPlanPreCondDuration() time.Duration
|
||||
// SocBasedPlanning determines if the planner is soc based
|
||||
SocBasedPlanning() bool
|
||||
// GetPlan creates a charging plan
|
||||
GetPlan(targetTime time.Time, requiredDuration time.Duration) api.Rates
|
||||
GetPlan(targetTime time.Time, requiredDuration, precondition time.Duration) api.Rates
|
||||
|
||||
// GetSocConfig returns the soc poll settings
|
||||
GetSocConfig() SocConfig
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ type DynamicConfig struct {
|
|||
SmartCostLimit *float64 `json:"smartCostLimit"`
|
||||
PlanEnergy float64 `json:"planEnergy"`
|
||||
PlanTime time.Time `json:"planTime"`
|
||||
PlanPrecondition int64 `json:"planPrecondition"`
|
||||
LimitEnergy float64 `json:"limitEnergy"`
|
||||
LimitSoc int `json:"limitSoc"`
|
||||
|
||||
|
|
@ -56,7 +57,7 @@ func (payload DynamicConfig) Apply(lp API) error {
|
|||
lp.SetPriority(payload.Priority)
|
||||
lp.SetSmartCostLimit(payload.SmartCostLimit)
|
||||
lp.SetThresholds(payload.Thresholds)
|
||||
lp.SetPlanEnergy(payload.PlanTime, payload.PlanEnergy)
|
||||
lp.SetPlanEnergy(payload.PlanTime, time.Duration(payload.PlanPrecondition)*time.Second, payload.PlanEnergy)
|
||||
lp.SetLimitEnergy(payload.LimitEnergy)
|
||||
lp.SetLimitSoc(payload.LimitSoc)
|
||||
|
||||
|
|
|
|||
|
|
@ -420,26 +420,27 @@ func (mr *MockAPIMockRecorder) GetPhasesConfigured() *gomock.Call {
|
|||
}
|
||||
|
||||
// GetPlan mocks base method.
|
||||
func (m *MockAPI) GetPlan(targetTime time.Time, requiredDuration time.Duration) api.Rates {
|
||||
func (m *MockAPI) GetPlan(targetTime time.Time, requiredDuration, precondition time.Duration) api.Rates {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetPlan", targetTime, requiredDuration)
|
||||
ret := m.ctrl.Call(m, "GetPlan", targetTime, requiredDuration, precondition)
|
||||
ret0, _ := ret[0].(api.Rates)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetPlan indicates an expected call of GetPlan.
|
||||
func (mr *MockAPIMockRecorder) GetPlan(targetTime, requiredDuration any) *gomock.Call {
|
||||
func (mr *MockAPIMockRecorder) GetPlan(targetTime, requiredDuration, precondition any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlan", reflect.TypeOf((*MockAPI)(nil).GetPlan), targetTime, requiredDuration)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlan", reflect.TypeOf((*MockAPI)(nil).GetPlan), targetTime, requiredDuration, precondition)
|
||||
}
|
||||
|
||||
// GetPlanEnergy mocks base method.
|
||||
func (m *MockAPI) GetPlanEnergy() (time.Time, float64) {
|
||||
func (m *MockAPI) GetPlanEnergy() (time.Time, time.Duration, float64) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetPlanEnergy")
|
||||
ret0, _ := ret[0].(time.Time)
|
||||
ret1, _ := ret[1].(float64)
|
||||
return ret0, ret1
|
||||
ret1, _ := ret[1].(time.Duration)
|
||||
ret2, _ := ret[2].(float64)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// GetPlanEnergy indicates an expected call of GetPlanEnergy.
|
||||
|
|
@ -463,6 +464,20 @@ func (mr *MockAPIMockRecorder) GetPlanGoal() *gomock.Call {
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlanGoal", reflect.TypeOf((*MockAPI)(nil).GetPlanGoal))
|
||||
}
|
||||
|
||||
// GetPlanPreCondDuration mocks base method.
|
||||
func (m *MockAPI) GetPlanPreCondDuration() time.Duration {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetPlanPreCondDuration")
|
||||
ret0, _ := ret[0].(time.Duration)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetPlanPreCondDuration indicates an expected call of GetPlanPreCondDuration.
|
||||
func (mr *MockAPIMockRecorder) GetPlanPreCondDuration() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlanPreCondDuration", reflect.TypeOf((*MockAPI)(nil).GetPlanPreCondDuration))
|
||||
}
|
||||
|
||||
// GetPlanRequiredDuration mocks base method.
|
||||
func (m *MockAPI) GetPlanRequiredDuration(goal, maxPower float64) time.Duration {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
@ -844,17 +859,17 @@ func (mr *MockAPIMockRecorder) SetPhasesConfigured(arg0 any) *gomock.Call {
|
|||
}
|
||||
|
||||
// SetPlanEnergy mocks base method.
|
||||
func (m *MockAPI) SetPlanEnergy(arg0 time.Time, arg1 float64) error {
|
||||
func (m *MockAPI) SetPlanEnergy(arg0 time.Time, arg1 time.Duration, arg2 float64) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SetPlanEnergy", arg0, arg1)
|
||||
ret := m.ctrl.Call(m, "SetPlanEnergy", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SetPlanEnergy indicates an expected call of SetPlanEnergy.
|
||||
func (mr *MockAPIMockRecorder) SetPlanEnergy(arg0, arg1 any) *gomock.Call {
|
||||
func (mr *MockAPIMockRecorder) SetPlanEnergy(arg0, arg1, arg2 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPlanEnergy", reflect.TypeOf((*MockAPI)(nil).SetPlanEnergy), arg0, arg1)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPlanEnergy", reflect.TypeOf((*MockAPI)(nil).SetPlanEnergy), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// SetPriority mocks base method.
|
||||
|
|
|
|||
|
|
@ -327,19 +327,19 @@ func (lp *Loadpoint) SetLimitEnergy(energy float64) {
|
|||
}
|
||||
|
||||
// GetPlanEnergy returns plan target energy
|
||||
func (lp *Loadpoint) GetPlanEnergy() (time.Time, float64) {
|
||||
func (lp *Loadpoint) GetPlanEnergy() (time.Time, time.Duration, float64) {
|
||||
lp.RLock()
|
||||
defer lp.RUnlock()
|
||||
return lp.getPlanEnergy()
|
||||
}
|
||||
|
||||
// getPlanEnergy returns plan target energy
|
||||
func (lp *Loadpoint) getPlanEnergy() (time.Time, float64) {
|
||||
return lp.planTime, lp.planEnergy
|
||||
func (lp *Loadpoint) getPlanEnergy() (time.Time, time.Duration, float64) {
|
||||
return lp.planTime, lp.planPrecondition, lp.planEnergy
|
||||
}
|
||||
|
||||
// setPlanEnergy sets plan target energy (no mutex)
|
||||
func (lp *Loadpoint) setPlanEnergy(finishAt time.Time, energy float64) {
|
||||
func (lp *Loadpoint) setPlanEnergy(finishAt time.Time, precondition time.Duration, energy float64) {
|
||||
lp.planEnergy = energy
|
||||
lp.publish(keys.PlanEnergy, energy)
|
||||
lp.settings.SetFloat(keys.PlanEnergy, energy)
|
||||
|
|
@ -347,11 +347,14 @@ func (lp *Loadpoint) setPlanEnergy(finishAt time.Time, energy float64) {
|
|||
// remove plan
|
||||
if energy == 0 {
|
||||
finishAt = time.Time{}
|
||||
precondition = 0
|
||||
}
|
||||
|
||||
lp.planTime = finishAt
|
||||
lp.publish(keys.PlanTime, finishAt)
|
||||
lp.publish(keys.PlanPrecondition, precondition)
|
||||
lp.settings.SetTime(keys.PlanTime, finishAt)
|
||||
lp.settings.SetInt(keys.PlanPrecondition, int64(precondition.Seconds()))
|
||||
|
||||
if finishAt.IsZero() {
|
||||
lp.setPlanActive(false)
|
||||
|
|
@ -359,7 +362,7 @@ func (lp *Loadpoint) setPlanEnergy(finishAt time.Time, energy float64) {
|
|||
}
|
||||
|
||||
// SetPlanEnergy sets plan target energy
|
||||
func (lp *Loadpoint) SetPlanEnergy(finishAt time.Time, energy float64) error {
|
||||
func (lp *Loadpoint) SetPlanEnergy(finishAt time.Time, precondition time.Duration, energy float64) error {
|
||||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
|
||||
|
|
@ -370,8 +373,8 @@ func (lp *Loadpoint) SetPlanEnergy(finishAt time.Time, energy float64) error {
|
|||
lp.log.DEBUG.Printf("set plan energy: %.3gkWh @ %v", energy, finishAt.Round(time.Second).Local())
|
||||
|
||||
// apply immediately
|
||||
if lp.planEnergy != energy || !lp.planTime.Equal(finishAt) {
|
||||
lp.setPlanEnergy(finishAt, energy)
|
||||
if lp.planEnergy != energy || lp.planPrecondition != precondition || !lp.planTime.Equal(finishAt) {
|
||||
lp.setPlanEnergy(finishAt, precondition, energy)
|
||||
lp.requestUpdate()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,10 +32,11 @@ func (lp *Loadpoint) EffectivePriority() int {
|
|||
}
|
||||
|
||||
type plan struct {
|
||||
Id int
|
||||
Start time.Time // last possible start time
|
||||
End time.Time // user-selected finish time
|
||||
Soc int
|
||||
Id int
|
||||
Start time.Time // last possible start time
|
||||
End time.Time // user-selected finish time
|
||||
Precondition time.Duration
|
||||
Soc int
|
||||
}
|
||||
|
||||
func (lp *Loadpoint) nextActivePlan(maxPower float64, plans []plan) *plan {
|
||||
|
|
@ -59,20 +60,20 @@ func (lp *Loadpoint) nextActivePlan(maxPower float64, plans []plan) *plan {
|
|||
}
|
||||
|
||||
// NextVehiclePlan returns the next vehicle plan time, soc and id
|
||||
func (lp *Loadpoint) NextVehiclePlan() (time.Time, int, int) {
|
||||
func (lp *Loadpoint) NextVehiclePlan() (time.Time, time.Duration, int, int) {
|
||||
lp.RLock()
|
||||
defer lp.RUnlock()
|
||||
return lp.nextVehiclePlan()
|
||||
}
|
||||
|
||||
// nextVehiclePlan returns the next vehicle plan time, soc and id
|
||||
func (lp *Loadpoint) nextVehiclePlan() (time.Time, int, int) {
|
||||
// nextVehiclePlan returns the next vehicle plan time, precondition duration, soc and id
|
||||
func (lp *Loadpoint) nextVehiclePlan() (time.Time, time.Duration, int, int) {
|
||||
if v := lp.GetVehicle(); v != nil {
|
||||
var plans []plan
|
||||
|
||||
// static plan
|
||||
if planTime, soc := vehicle.Settings(lp.log, v).GetPlanSoc(); soc != 0 {
|
||||
plans = append(plans, plan{Id: 1, Soc: soc, End: planTime})
|
||||
if planTime, precondition, soc := vehicle.Settings(lp.log, v).GetPlanSoc(); soc != 0 {
|
||||
plans = append(plans, plan{Id: 1, Precondition: precondition, Soc: soc, End: planTime})
|
||||
}
|
||||
|
||||
// repeating plans
|
||||
|
|
@ -81,33 +82,34 @@ func (lp *Loadpoint) nextVehiclePlan() (time.Time, int, int) {
|
|||
continue
|
||||
}
|
||||
|
||||
time, err := util.GetNextOccurrence(rp.Weekdays, rp.Time, rp.Tz)
|
||||
planTime, err := util.GetNextOccurrence(rp.Weekdays, rp.Time, rp.Tz)
|
||||
if err != nil {
|
||||
lp.log.DEBUG.Printf("invalid repeating plan: weekdays=%v, time=%s, tz=%s, error=%v", rp.Weekdays, rp.Time, rp.Tz, err)
|
||||
continue
|
||||
}
|
||||
|
||||
plans = append(plans, plan{Id: index + 2, Soc: rp.Soc, End: time})
|
||||
precondition := time.Duration(rp.Precondition) * time.Second
|
||||
plans = append(plans, plan{Id: index + 2, Precondition: precondition, Soc: rp.Soc, End: planTime})
|
||||
}
|
||||
|
||||
// calculate earliest required plan start
|
||||
if plan := lp.nextActivePlan(lp.effectiveMaxPower(), plans); plan != nil {
|
||||
return plan.End, plan.Soc, plan.Id
|
||||
return plan.End, plan.Precondition, plan.Soc, plan.Id
|
||||
}
|
||||
}
|
||||
return time.Time{}, 0, 0
|
||||
return time.Time{}, 0, 0, 0
|
||||
}
|
||||
|
||||
// EffectivePlanSoc returns the soc target for the current plan
|
||||
func (lp *Loadpoint) EffectivePlanSoc() int {
|
||||
_, soc, _ := lp.NextVehiclePlan()
|
||||
_, _, soc, _ := lp.NextVehiclePlan()
|
||||
return soc
|
||||
}
|
||||
|
||||
// EffectivePlanId returns the id for the current plan
|
||||
func (lp *Loadpoint) EffectivePlanId() int {
|
||||
if lp.socBasedPlanning() {
|
||||
_, _, id := lp.NextVehiclePlan()
|
||||
_, _, _, id := lp.NextVehiclePlan()
|
||||
return id
|
||||
}
|
||||
if lp.planEnergy > 0 {
|
||||
|
|
@ -120,11 +122,11 @@ func (lp *Loadpoint) EffectivePlanId() int {
|
|||
// EffectivePlanTime returns the effective plan time
|
||||
func (lp *Loadpoint) EffectivePlanTime() time.Time {
|
||||
if lp.socBasedPlanning() {
|
||||
ts, _, _ := lp.NextVehiclePlan()
|
||||
ts, _, _, _ := lp.NextVehiclePlan()
|
||||
return ts
|
||||
}
|
||||
|
||||
ts, _ := lp.GetPlanEnergy()
|
||||
ts, _, _ := lp.GetPlanEnergy()
|
||||
return ts
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ func (lp *Loadpoint) finishPlan() {
|
|||
if lp.repeatingPlanning() {
|
||||
return // noting to do
|
||||
} else if !lp.socBasedPlanning() {
|
||||
lp.setPlanEnergy(time.Time{}, 0)
|
||||
lp.setPlanEnergy(time.Time{}, 0, 0)
|
||||
} else if v := lp.GetVehicle(); v != nil {
|
||||
vehicle.Settings(lp.log, v).SetPlanSoc(time.Time{}, 0)
|
||||
vehicle.Settings(lp.log, v).SetPlanSoc(time.Time{}, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,21 +70,36 @@ func (lp *Loadpoint) GetPlanGoal() (float64, bool) {
|
|||
defer lp.RUnlock()
|
||||
|
||||
if lp.socBasedPlanning() {
|
||||
_, soc, _ := lp.nextVehiclePlan()
|
||||
_, _, soc, _ := lp.nextVehiclePlan()
|
||||
return float64(soc), true
|
||||
}
|
||||
|
||||
_, limit := lp.getPlanEnergy()
|
||||
_, _, limit := lp.getPlanEnergy()
|
||||
return limit, false
|
||||
}
|
||||
|
||||
// GetPlanPreCondDuration returns the plan precondition duration
|
||||
func (lp *Loadpoint) GetPlanPreCondDuration() time.Duration {
|
||||
lp.RLock()
|
||||
defer lp.RUnlock()
|
||||
|
||||
if lp.socBasedPlanning() {
|
||||
_, precondition, _, _ := lp.nextVehiclePlan()
|
||||
return precondition
|
||||
}
|
||||
|
||||
_, precondition, _ := lp.getPlanEnergy()
|
||||
return precondition
|
||||
}
|
||||
|
||||
// GetPlan creates a charging plan for given time and duration
|
||||
func (lp *Loadpoint) GetPlan(targetTime time.Time, requiredDuration time.Duration) api.Rates {
|
||||
// The plan is sorted by time
|
||||
func (lp *Loadpoint) GetPlan(targetTime time.Time, requiredDuration, precondition time.Duration) api.Rates {
|
||||
if lp.planner == nil || targetTime.IsZero() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return lp.planner.Plan(requiredDuration, targetTime)
|
||||
return lp.planner.Plan(requiredDuration, precondition, targetTime)
|
||||
}
|
||||
|
||||
// plannerActive checks if the charging plan has a currently active slot
|
||||
|
|
@ -132,7 +147,7 @@ func (lp *Loadpoint) plannerActive() (active bool) {
|
|||
return false
|
||||
}
|
||||
|
||||
plan := lp.GetPlan(planTime, requiredDuration)
|
||||
plan := lp.GetPlan(planTime, requiredDuration, lp.GetPlanPreCondDuration())
|
||||
if plan == nil {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ func (t *Planner) continuousPlan(rates api.Rates, start, end time.Time) api.Rate
|
|||
return res
|
||||
}
|
||||
|
||||
func (t *Planner) Plan(requiredDuration time.Duration, targetTime time.Time) api.Rates {
|
||||
func (t *Planner) Plan(requiredDuration, precondition time.Duration, targetTime time.Time) api.Rates {
|
||||
if t == nil || requiredDuration <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -178,6 +178,9 @@ func (t *Planner) Plan(requiredDuration time.Duration, targetTime time.Time) api
|
|||
// sort rates by price and time
|
||||
slices.SortStableFunc(rates, sortByCost)
|
||||
|
||||
// for late start ensure that the last slot is the cheapest
|
||||
rates, adjusted := splitPreconditionSlots(rates, precondition, targetTime)
|
||||
|
||||
// reduce planning horizon to available rates
|
||||
if targetTime.After(last) {
|
||||
// there is enough time for charging after end of current rates
|
||||
|
|
@ -194,10 +197,62 @@ func (t *Planner) Plan(requiredDuration time.Duration, targetTime time.Time) api
|
|||
requiredDuration -= durationAfterRates
|
||||
}
|
||||
|
||||
// sort rates by price and time
|
||||
slices.SortStableFunc(rates, sortByCost)
|
||||
|
||||
plan := t.plan(rates, requiredDuration, targetTime)
|
||||
|
||||
// correct plan slots to show original, non-adjusted prices
|
||||
for i, r := range plan {
|
||||
if rr, err := adjusted.At(r.Start); err == nil {
|
||||
plan[i].Value = rr.Value
|
||||
}
|
||||
}
|
||||
|
||||
// sort plan by time
|
||||
plan.Sort()
|
||||
|
||||
return plan
|
||||
}
|
||||
|
||||
func splitPreconditionSlots(rates api.Rates, precondition time.Duration, targetTime time.Time) (api.Rates, api.Rates) {
|
||||
var res, adjusted api.Rates
|
||||
|
||||
for _, r := range slices.Clone(rates) {
|
||||
preCondStart := targetTime.Add(-precondition)
|
||||
|
||||
if !r.End.After(preCondStart) {
|
||||
res = append(res, r)
|
||||
continue
|
||||
}
|
||||
|
||||
// split slot
|
||||
if !r.Start.After(preCondStart) {
|
||||
// keep the first part of the slot
|
||||
res = append(res, api.Rate{
|
||||
Start: r.Start,
|
||||
End: preCondStart,
|
||||
Value: r.Value,
|
||||
})
|
||||
|
||||
// adjust the second part of the slot
|
||||
r = api.Rate{
|
||||
Start: preCondStart,
|
||||
End: r.End,
|
||||
Value: r.Value,
|
||||
}
|
||||
}
|
||||
|
||||
// set the value to 0 to include slot in the plan
|
||||
res = append(res, api.Rate{
|
||||
Start: r.Start,
|
||||
End: r.End,
|
||||
Value: 0,
|
||||
})
|
||||
|
||||
// keep a copy of the adjusted slot
|
||||
adjusted = append(adjusted, r)
|
||||
}
|
||||
|
||||
return res, adjusted
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ func TestNilTariff(t *testing.T) {
|
|||
clock: clock,
|
||||
}
|
||||
|
||||
plan := p.Plan(time.Hour, clock.Now().Add(30*time.Minute))
|
||||
plan := p.Plan(time.Hour, 0, clock.Now().Add(30*time.Minute))
|
||||
assert.Equal(t, api.Rates{
|
||||
{
|
||||
Start: clock.Now(),
|
||||
|
|
@ -155,7 +155,7 @@ func TestRatesError(t *testing.T) {
|
|||
tariff: trf,
|
||||
}
|
||||
|
||||
plan := p.Plan(time.Hour, clock.Now().Add(30*time.Minute))
|
||||
plan := p.Plan(time.Hour, 0, clock.Now().Add(30*time.Minute))
|
||||
assert.Equal(t, api.Rates{
|
||||
{
|
||||
Start: clock.Now(),
|
||||
|
|
@ -184,10 +184,10 @@ func TestFlatTariffTargetInThePast(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
plan := p.Plan(time.Hour, clock.Now().Add(30*time.Minute))
|
||||
plan := p.Plan(time.Hour, 0, clock.Now().Add(30*time.Minute))
|
||||
assert.Equal(t, simplePlan, plan, "expected simple plan")
|
||||
|
||||
plan = p.Plan(time.Hour, clock.Now().Add(-30*time.Minute))
|
||||
plan = p.Plan(time.Hour, 0, clock.Now().Add(-30*time.Minute))
|
||||
assert.Equal(t, simplePlan, plan, "expected simple plan")
|
||||
}
|
||||
|
||||
|
|
@ -208,12 +208,12 @@ func TestFlatTariffLongSlots(t *testing.T) {
|
|||
// that slots are not longer than 1 hour and with that context this is not a problem
|
||||
|
||||
// expect 00:00-01:00 UTC
|
||||
plan := p.Plan(time.Hour, clock.Now().Add(2*time.Hour))
|
||||
plan := p.Plan(time.Hour, 0, clock.Now().Add(2*time.Hour))
|
||||
assert.Equal(t, api.Rate{Start: clock.Now(), End: clock.Now().Add(time.Hour)}, SlotAt(clock.Now(), plan))
|
||||
assert.Equal(t, api.Rate{}, SlotAt(clock.Now().Add(time.Hour), plan))
|
||||
|
||||
// expect 00:00-01:00 UTC
|
||||
plan = p.Plan(time.Hour, clock.Now().Add(time.Hour))
|
||||
plan = p.Plan(time.Hour, 0, clock.Now().Add(time.Hour))
|
||||
assert.Equal(t, api.Rate{Start: clock.Now(), End: clock.Now().Add(time.Hour)}, SlotAt(clock.Now(), plan))
|
||||
}
|
||||
|
||||
|
|
@ -230,10 +230,10 @@ func TestTargetAfterKnownPrices(t *testing.T) {
|
|||
tariff: trf,
|
||||
}
|
||||
|
||||
plan := p.Plan(40*time.Minute, clock.Now().Add(2*time.Hour)) // charge efficiency does not allow to test with 1h
|
||||
plan := p.Plan(40*time.Minute, 0, clock.Now().Add(2*time.Hour)) // charge efficiency does not allow to test with 1h
|
||||
assert.False(t, !SlotAt(clock.Now(), plan).IsZero(), "should not start if car can be charged completely after known prices ")
|
||||
|
||||
plan = p.Plan(2*time.Hour, clock.Now().Add(2*time.Hour))
|
||||
plan = p.Plan(2*time.Hour, 0, clock.Now().Add(2*time.Hour))
|
||||
assert.True(t, !SlotAt(clock.Now(), plan).IsZero(), "should start if car can not be charged completely after known prices ")
|
||||
}
|
||||
|
||||
|
|
@ -257,13 +257,64 @@ func TestChargeAfterTargetTime(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
plan := p.Plan(time.Hour, clock.Now())
|
||||
plan := p.Plan(time.Hour, 0, clock.Now())
|
||||
assert.Equal(t, simplePlan, plan, "expected simple plan")
|
||||
|
||||
plan = p.Plan(time.Hour, clock.Now().Add(-time.Hour))
|
||||
plan = p.Plan(time.Hour, 0, clock.Now().Add(-time.Hour))
|
||||
assert.Equal(t, simplePlan, plan, "expected simple plan")
|
||||
}
|
||||
|
||||
func TestPrecondition(t *testing.T) {
|
||||
clock := clock.NewMock()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
trf := api.NewMockTariff(ctrl)
|
||||
trf.EXPECT().Rates().AnyTimes().Return(rates([]float64{0, 1, 2, 3}, clock.Now(), time.Hour), nil)
|
||||
|
||||
p := &Planner{
|
||||
log: util.NewLogger("foo"),
|
||||
clock: clock,
|
||||
tariff: trf,
|
||||
}
|
||||
|
||||
plan := p.Plan(time.Hour, time.Hour, clock.Now().Add(4*time.Hour))
|
||||
assert.Equal(t, api.Rates{
|
||||
{
|
||||
Start: clock.Now().Add(3 * time.Hour),
|
||||
End: clock.Now().Add(4 * time.Hour),
|
||||
Value: 3,
|
||||
},
|
||||
}, plan, "expected last slot")
|
||||
|
||||
plan = p.Plan(2*time.Hour, time.Hour, clock.Now().Add(4*time.Hour))
|
||||
assert.Equal(t, api.Rates{
|
||||
{
|
||||
Start: clock.Now(),
|
||||
End: clock.Now().Add(1 * time.Hour),
|
||||
Value: 0,
|
||||
},
|
||||
{
|
||||
Start: clock.Now().Add(3 * time.Hour),
|
||||
End: clock.Now().Add(4 * time.Hour),
|
||||
Value: 3,
|
||||
},
|
||||
}, plan, "expected two slots")
|
||||
|
||||
plan = p.Plan(time.Hour, 30*time.Minute, clock.Now().Add(4*time.Hour))
|
||||
assert.Equal(t, api.Rates{
|
||||
{
|
||||
Start: clock.Now().Add(30 * time.Minute),
|
||||
End: clock.Now().Add(time.Hour),
|
||||
Value: 0,
|
||||
},
|
||||
{
|
||||
Start: clock.Now().Add(210 * time.Minute),
|
||||
End: clock.Now().Add(4 * time.Hour),
|
||||
Value: 3,
|
||||
},
|
||||
}, plan, "expected short early and split late slot")
|
||||
}
|
||||
|
||||
func TestContinuousPlanNoTariff(t *testing.T) {
|
||||
clock := clock.NewMock()
|
||||
|
||||
|
|
@ -272,7 +323,7 @@ func TestContinuousPlanNoTariff(t *testing.T) {
|
|||
clock: clock,
|
||||
}
|
||||
|
||||
plan := p.Plan(time.Hour, clock.Now())
|
||||
plan := p.Plan(time.Hour, 0, clock.Now())
|
||||
|
||||
// single-slot plan
|
||||
assert.Len(t, plan, 1)
|
||||
|
|
@ -293,7 +344,7 @@ func TestContinuousPlan(t *testing.T) {
|
|||
tariff: trf,
|
||||
}
|
||||
|
||||
plan := p.Plan(150*time.Minute, clock.Now())
|
||||
plan := p.Plan(150*time.Minute, 0, clock.Now())
|
||||
|
||||
// 3-slot plan
|
||||
assert.Len(t, plan, 3)
|
||||
|
|
@ -312,7 +363,7 @@ func TestContinuousPlanOutsideRates(t *testing.T) {
|
|||
tariff: trf,
|
||||
}
|
||||
|
||||
plan := p.Plan(30*time.Minute, clock.Now())
|
||||
plan := p.Plan(30*time.Minute, 0, clock.Now())
|
||||
|
||||
// 3-slot plan
|
||||
assert.Len(t, plan, 1)
|
||||
|
|
|
|||
|
|
@ -99,14 +99,16 @@ func (site *Site) publishTariffs(greenShareHome float64, greenShareLoadpoints fl
|
|||
}
|
||||
|
||||
fc := struct {
|
||||
Co2 api.Rates `json:"co2,omitempty"`
|
||||
FeedIn api.Rates `json:"feedin,omitempty"`
|
||||
Grid api.Rates `json:"grid,omitempty"`
|
||||
Solar *solarDetails `json:"solar,omitempty"`
|
||||
Co2 api.Rates `json:"co2,omitempty"`
|
||||
FeedIn api.Rates `json:"feedin,omitempty"`
|
||||
Grid api.Rates `json:"grid,omitempty"`
|
||||
Planner api.Rates `json:"planner,omitempty"`
|
||||
Solar *solarDetails `json:"solar,omitempty"`
|
||||
}{
|
||||
Co2: tariff.Forecast(site.GetTariff(api.TariffUsageCo2)),
|
||||
FeedIn: tariff.Forecast(site.GetTariff(api.TariffUsageFeedIn)),
|
||||
Grid: tariff.Forecast(site.GetTariff(api.TariffUsageGrid)),
|
||||
Co2: tariff.Forecast(site.GetTariff(api.TariffUsageCo2)),
|
||||
FeedIn: tariff.Forecast(site.GetTariff(api.TariffUsageFeedIn)),
|
||||
Planner: tariff.Forecast(site.GetTariff(api.TariffUsagePlanner)),
|
||||
Grid: tariff.Forecast(site.GetTariff(api.TariffUsageGrid)),
|
||||
}
|
||||
|
||||
// calculate adjusted solar forecast
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ import (
|
|||
)
|
||||
|
||||
type planStruct struct {
|
||||
Soc int `json:"soc"`
|
||||
Time time.Time `json:"time"`
|
||||
Soc int `json:"soc"`
|
||||
Precondition int64 `json:"precondition"`
|
||||
Time time.Time `json:"time"`
|
||||
}
|
||||
|
||||
type vehicleStruct struct {
|
||||
|
|
@ -40,8 +41,8 @@ func (site *Site) publishVehicles() {
|
|||
for _, v := range vv {
|
||||
var plan *planStruct
|
||||
|
||||
if time, soc := v.GetPlanSoc(); !time.IsZero() {
|
||||
plan = &planStruct{Soc: soc, Time: time}
|
||||
if time, precondition, soc := v.GetPlanSoc(); !time.IsZero() {
|
||||
plan = &planStruct{Soc: soc, Precondition: int64(precondition.Seconds()), Time: time}
|
||||
}
|
||||
|
||||
instance := v.Instance()
|
||||
|
|
|
|||
|
|
@ -71,20 +71,24 @@ func (v *adapter) SetLimitSoc(soc int) {
|
|||
}
|
||||
|
||||
// GetPlanSoc returns the charge plan soc
|
||||
func (v *adapter) GetPlanSoc() (time.Time, int) {
|
||||
func (v *adapter) GetPlanSoc() (time.Time, time.Duration, int) {
|
||||
var ts time.Time
|
||||
if v, err := settings.Time(v.key() + keys.PlanTime); err == nil {
|
||||
ts = v
|
||||
}
|
||||
var precondition time.Duration
|
||||
if v, err := settings.Int(v.key() + keys.PlanPrecondition); err == nil {
|
||||
precondition = time.Duration(v) * time.Second
|
||||
}
|
||||
var soc int
|
||||
if v, err := settings.Int(v.key() + keys.PlanSoc); err == nil {
|
||||
soc = int(v)
|
||||
}
|
||||
return ts, soc
|
||||
return ts, precondition, soc
|
||||
}
|
||||
|
||||
// SetPlanSoc sets the charge plan soc
|
||||
func (v *adapter) SetPlanSoc(ts time.Time, soc int) error {
|
||||
func (v *adapter) SetPlanSoc(ts time.Time, precondition time.Duration, soc int) error {
|
||||
if !ts.IsZero() && ts.Before(time.Now()) {
|
||||
return errors.New("timestamp is in the past")
|
||||
}
|
||||
|
|
@ -94,10 +98,11 @@ func (v *adapter) SetPlanSoc(ts time.Time, soc int) error {
|
|||
ts = time.Time{}
|
||||
v.log.DEBUG.Printf("delete %s plan", v.name)
|
||||
} else {
|
||||
v.log.DEBUG.Printf("set %s plan soc: %d @ %v", v.name, soc, ts.Round(time.Second).Local())
|
||||
v.log.DEBUG.Printf("set %s plan soc: %d @ %v (precondition: %v)", v.name, soc, ts.Round(time.Second).Local(), precondition)
|
||||
}
|
||||
|
||||
settings.SetTime(v.key()+keys.PlanTime, ts)
|
||||
settings.SetInt(v.key()+keys.PlanPrecondition, int64(precondition.Seconds()))
|
||||
settings.SetInt(v.key()+keys.PlanSoc, int64(soc))
|
||||
|
||||
v.publish()
|
||||
|
|
|
|||
|
|
@ -39,9 +39,9 @@ type API interface {
|
|||
SetLimitSoc(soc int)
|
||||
|
||||
// GetPlanSoc returns the charge plan soc
|
||||
GetPlanSoc() (time.Time, int)
|
||||
GetPlanSoc() (time.Time, time.Duration, int)
|
||||
// SetPlanSoc sets the charge plan time and soc
|
||||
SetPlanSoc(time.Time, int) error
|
||||
SetPlanSoc(time.Time, time.Duration, int) error
|
||||
|
||||
// GetRepeatingPlans returns every repeating plan
|
||||
GetRepeatingPlans() []api.RepeatingPlanStruct
|
||||
|
|
|
|||
|
|
@ -39,12 +39,12 @@ func (v *dummy) SetLimitSoc(soc int) {
|
|||
}
|
||||
|
||||
// GetPlanSoc returns the charge plan soc
|
||||
func (v *dummy) GetPlanSoc() (time.Time, int) {
|
||||
return time.Time{}, 0
|
||||
func (v *dummy) GetPlanSoc() (time.Time, time.Duration, int) {
|
||||
return time.Time{}, 0, 0
|
||||
}
|
||||
|
||||
// SetPlanSoc sets the charge plan soc
|
||||
func (v *dummy) SetPlanSoc(ts time.Time, soc int) error {
|
||||
func (v *dummy) SetPlanSoc(ts time.Time, precondition time.Duration, soc int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,12 +70,13 @@ func (mr *MockAPIMockRecorder) GetMinSoc() *gomock.Call {
|
|||
}
|
||||
|
||||
// GetPlanSoc mocks base method.
|
||||
func (m *MockAPI) GetPlanSoc() (time.Time, int) {
|
||||
func (m *MockAPI) GetPlanSoc() (time.Time, time.Duration, int) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetPlanSoc")
|
||||
ret0, _ := ret[0].(time.Time)
|
||||
ret1, _ := ret[1].(int)
|
||||
return ret0, ret1
|
||||
ret1, _ := ret[1].(time.Duration)
|
||||
ret2, _ := ret[2].(int)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// GetPlanSoc indicates an expected call of GetPlanSoc.
|
||||
|
|
@ -151,17 +152,17 @@ func (mr *MockAPIMockRecorder) SetMinSoc(soc any) *gomock.Call {
|
|||
}
|
||||
|
||||
// SetPlanSoc mocks base method.
|
||||
func (m *MockAPI) SetPlanSoc(arg0 time.Time, arg1 int) error {
|
||||
func (m *MockAPI) SetPlanSoc(arg0 time.Time, arg1 time.Duration, arg2 int) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SetPlanSoc", arg0, arg1)
|
||||
ret := m.ctrl.Call(m, "SetPlanSoc", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SetPlanSoc indicates an expected call of SetPlanSoc.
|
||||
func (mr *MockAPIMockRecorder) SetPlanSoc(arg0, arg1 any) *gomock.Call {
|
||||
func (mr *MockAPIMockRecorder) SetPlanSoc(arg0, arg1, arg2 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPlanSoc", reflect.TypeOf((*MockAPI)(nil).SetPlanSoc), arg0, arg1)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPlanSoc", reflect.TypeOf((*MockAPI)(nil).SetPlanSoc), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// SetRepeatingPlans mocks base method.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue