forked from evcc-io/evcc
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathplanner.go
203 lines (165 loc) Β· 4.53 KB
/
planner.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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package planner
import (
"slices"
"time"
"github.com/benbjohnson/clock"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
)
// Planner plans a series of charging slots for a given (variable) tariff
type Planner struct {
log *util.Logger
clock clock.Clock // mockable time
tariff api.Tariff
}
// New creates a price planner
func New(log *util.Logger, tariff api.Tariff, opt ...func(t *Planner)) *Planner {
p := &Planner{
log: log,
clock: clock.New(),
tariff: tariff,
}
for _, o := range opt {
o(p)
}
return p
}
// plan creates a lowest-cost plan or required duration.
// It MUST already established that
// - rates are sorted in ascending order by cost and descending order by start time (prefer late slots)
// - target time and required duration are before end of rates
func (t *Planner) plan(rates api.Rates, requiredDuration time.Duration, targetTime time.Time) api.Rates {
var plan api.Rates
for _, source := range rates {
// slot not relevant
if source.Start.After(targetTime) || source.Start.Equal(targetTime) || source.End.Before(t.clock.Now()) {
continue
}
// adjust slot start and end
slot := source
if slot.Start.Before(t.clock.Now()) {
slot.Start = t.clock.Now()
}
if slot.End.After(targetTime) {
slot.End = targetTime
}
slotDuration := slot.End.Sub(slot.Start)
requiredDuration -= slotDuration
// slot covers more than we need, so shorten it
if requiredDuration < 0 {
// the first (if not single) slot should start as late as possible
if IsFirst(slot, plan) && len(plan) > 0 {
slot.Start = slot.Start.Add(-requiredDuration)
} else {
slot.End = slot.End.Add(requiredDuration)
}
requiredDuration = 0
if slot.End.Before(slot.Start) {
panic("slot end before start")
}
}
plan = append(plan, slot)
// we found all necessary slots
if requiredDuration == 0 {
break
}
}
return plan
}
// Plan creates a continuous emergency charging plan
func (t *Planner) continuousPlan(rates api.Rates, start, end time.Time) api.Rates {
rates.Sort()
res := make(api.Rates, 0, len(rates)+2)
for _, r := range rates {
// slot before continuous plan
if !r.End.After(start) {
continue
}
// slot after continuous plan
if !r.Start.Before(end) {
continue
}
// adjust first slot
if r.Start.Before(start) && r.End.After(start) {
r.Start = start
}
// adjust last slot
if r.Start.Before(end) && r.End.After(end) {
r.End = end
}
res = append(res, r)
}
if len(res) == 0 {
res = append(res, api.Rate{
Start: start,
End: end,
})
} else {
// prepend missing slot
if res[0].Start.After(start) {
res = slices.Insert(res, 0, api.Rate{
Start: start,
End: res[0].Start,
})
}
// append missing slot
if last := res[len(res)-1]; last.End.Before(end) {
res = append(res, api.Rate{
Start: last.End,
End: end,
})
}
}
return res
}
func (t *Planner) Plan(requiredDuration time.Duration, targetTime time.Time) (api.Rates, error) {
if t == nil || requiredDuration <= 0 {
return nil, nil
}
latestStart := targetTime.Add(-requiredDuration)
if latestStart.Before(t.clock.Now()) {
latestStart = t.clock.Now()
targetTime = latestStart.Add(requiredDuration)
}
// simplePlan only considers time, but not cost
simplePlan := api.Rates{
api.Rate{
Start: latestStart,
End: targetTime,
},
}
// target charging without tariff or late start
if t.tariff == nil {
return simplePlan, nil
}
rates, err := t.tariff.Rates()
// treat like normal target charging if we don't have rates
if len(rates) == 0 || err != nil {
return simplePlan, err
}
// consume remaining time
if t.clock.Until(targetTime) <= requiredDuration {
return t.continuousPlan(rates, latestStart, targetTime), nil
}
// rates are by default sorted by date, oldest to newest
last := rates[len(rates)-1].End
// sort rates by price and time
slices.SortStableFunc(rates, sortByCost)
// reduce planning horizon to available rates
if targetTime.After(last) {
// there is enough time for charging after end of current rates
durationAfterRates := targetTime.Sub(last)
if durationAfterRates >= requiredDuration {
return nil, nil
}
// need to use some of the available slots
t.log.DEBUG.Printf("target time beyond available slots- reducing plan horizon from %v to %v",
requiredDuration.Round(time.Second), durationAfterRates.Round(time.Second))
targetTime = last
requiredDuration -= durationAfterRates
}
plan := t.plan(rates, requiredDuration, targetTime)
// sort plan by time
plan.Sort()
return plan, nil
}