forked from evcc-io/evcc
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathloadpoint_plan.go
153 lines (125 loc) Β· 4.94 KB
/
loadpoint_plan.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package core
import (
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/core/planner"
"github.com/evcc-io/evcc/core/soc"
"golang.org/x/exp/slices"
)
const (
smallSlotDuration = 10 * time.Minute // small planner slot duration we might ignore
smallGapDuration = 30 * time.Minute // small gap duration between planner slots we might ignore
)
// setPlanActive updates plan active flag
func (lp *Loadpoint) setPlanActive(active bool) {
if !active {
lp.planSlotEnd = time.Time{}
}
lp.planActive = active
lp.publish(planActive, lp.planActive)
}
// planRequiredDuration is the estimated total charging duration
func (lp *Loadpoint) planRequiredDuration(maxPower float64) time.Duration {
var requiredDuration time.Duration
if energy, ok := lp.remainingChargeEnergy(); ok {
requiredDuration = time.Duration(energy * 1e3 / maxPower * float64(time.Hour))
} else if lp.socEstimator != nil {
// TODO vehicle soc limit
targetSoc := lp.Soc.target
if targetSoc == 0 {
targetSoc = 100
}
requiredDuration = lp.socEstimator.RemainingChargeDuration(targetSoc, maxPower)
// anticipate lower charge rates at end of charging curve
var additionalDuration time.Duration
if targetSoc > 80 && maxPower > 15000 {
additionalDuration = 5 * time.Duration(float64(targetSoc-80)/(float64(targetSoc)-lp.vehicleSoc)*float64(requiredDuration))
lp.log.DEBUG.Printf("add additional charging time %v for soc > 80%%", additionalDuration.Round(time.Minute))
} else if targetSoc > 90 && maxPower > 4000 {
additionalDuration = 3 * time.Duration(float64(targetSoc-90)/(float64(targetSoc)-lp.vehicleSoc)*float64(requiredDuration))
lp.log.DEBUG.Printf("add additional charging time %v for soc > 90%%", additionalDuration.Round(time.Minute))
}
requiredDuration += additionalDuration
}
requiredDuration = time.Duration(float64(requiredDuration) / soc.ChargeEfficiency)
return requiredDuration
}
func (lp *Loadpoint) GetPlannerUnit() string {
return lp.planner.Unit()
}
// GetPlan creates a charging plan
//
// Results:
// - required total charging duration
// - actual charging plan as rate table
func (lp *Loadpoint) GetPlan(targetTime time.Time, maxPower float64) (time.Duration, api.Rates, error) {
if lp.planner == nil || targetTime.IsZero() {
return 0, nil, nil
}
// don't start planning into the past
if targetTime.Before(lp.clock.Now()) && !lp.planActive {
return 0, nil, nil
}
requiredDuration := lp.planRequiredDuration(maxPower)
plan, err := lp.planner.Plan(requiredDuration, targetTime)
// sort plan by time
slices.SortStableFunc(plan, planner.SortByTime)
return requiredDuration, plan, err
}
// plannerActive checks if charging plan is active
func (lp *Loadpoint) plannerActive() (active bool) {
defer func() {
lp.setPlanActive(active)
}()
maxPower := lp.GetMaxPower()
requiredDuration, plan, err := lp.GetPlan(lp.GetTargetTime(), maxPower)
if err != nil {
lp.log.ERROR.Println("planner:", err)
return false
}
// nothing to do
if requiredDuration == 0 {
return false
}
planStart := planner.Start(plan)
lp.publish(planProjectedStart, planStart)
lp.log.DEBUG.Printf("planned %v until %v at %.0fW: total plan duration: %v, avg cost: %.3f",
requiredDuration.Round(time.Second), lp.targetTime.Round(time.Second).Local(), maxPower,
planner.Duration(plan).Round(time.Second), planner.AverageCost(plan))
// sort plan by time
for _, slot := range plan {
lp.log.TRACE.Printf(" slot from: %v to %v cost %.3f", slot.Start.Round(time.Second).Local(), slot.End.Round(time.Second).Local(), slot.Price)
}
activeSlot := planner.ActiveSlot(lp.clock, plan)
active = !activeSlot.End.IsZero()
if active {
// ignore short plans if not already active
if !lp.planActive && lp.clock.Until(activeSlot.End) < smallSlotDuration {
lp.log.DEBUG.Printf("plan too short- ignoring remaining %v", requiredDuration.Round(time.Second))
return false
}
// remember last active plan's end time
lp.setPlanActive(true)
lp.planSlotEnd = activeSlot.End
} else if lp.planActive {
// planner was active (any slot, not necessarily previous slot) and charge goal has not yet been met
switch {
case lp.clock.Now().After(lp.targetTime) && !lp.targetTime.IsZero():
// if the plan did not (entirely) work, we may still be charging beyond plan end- in that case, continue charging
// TODO check when schedule is implemented
lp.log.DEBUG.Println("continuing after target time")
return true
case lp.clock.Now().Before(lp.planSlotEnd) && !lp.planSlotEnd.IsZero():
// don't stop an already running slot if goal was not met
lp.log.DEBUG.Println("continuing until end of slot")
return true
case requiredDuration < 30*time.Minute:
lp.log.DEBUG.Printf("continuing for remaining %v", requiredDuration.Round(time.Second))
return true
case lp.clock.Until(planStart) < smallGapDuration:
lp.log.DEBUG.Printf("plan will re-start shortly, continuing for remaining %v", lp.clock.Until(planStart).Round(time.Second))
return true
}
}
return active
}