evcc-io/core/timeseries.go
2025-03-08 11:45:21 +01:00

95 lines
2.2 KiB
Go

package core
import (
"slices"
"time"
"github.com/evcc-io/evcc/api"
"github.com/samber/lo"
)
// timeseries is a sorted list of timestamped values
// methods are optimized for fast searching and interpolation
type timeseries []tsval
type tsval struct {
Timestamp time.Time `json:"ts"`
Value float64 `json:"val"`
}
func (rr timeseries) search(ts time.Time) (int, bool) {
return slices.BinarySearchFunc(rr, ts, func(v tsval, ts time.Time) int {
return v.Timestamp.Compare(ts)
})
}
// interpolate returns the interpolated value where ts is between two entries and i is the index of the rate after ts
func (rr timeseries) interpolate(i int, ts time.Time) float64 {
rp := &rr[i-1]
r := &rr[i]
return rp.Value + float64(ts.Sub(rp.Timestamp))*(r.Value-rp.Value)/float64(r.Timestamp.Sub(rp.Timestamp))
}
func (rr timeseries) value(ts time.Time) float64 {
idx, ok := rr.search(ts)
if ok {
return rr[idx].Value
}
if idx == 0 || idx >= len(rr) {
return 0
}
return rr.interpolate(idx, ts)
}
// energy calculates the energy consumption between from and to,
// assuming the rates containing the power at given timestamp.
// Result is in Wh
func (rr timeseries) energy(from, to time.Time) float64 {
var energy float64
idx, ok := rr.search(from)
if !ok {
switch {
case idx >= len(rr):
// from is just before or after last entry
return 0
case idx == 0:
// from is before first entry
// do nothing- we ignore anything before the first entry
default:
// from is between two entries
r := &rr[idx]
vp := rr.interpolate(idx, from)
// to is before same entry as from
if r.Timestamp.After(to) {
return (vp + rr.interpolate(idx, to)) / 2 * to.Sub(from).Hours()
}
energy += (vp + r.Value) / 2 * r.Timestamp.Sub(from).Hours()
}
}
for ; idx < len(rr)-1; idx++ {
r := &rr[idx]
rn := &rr[idx+1]
if rn.Timestamp.After(to) {
energy += (r.Value + rr.interpolate(idx+1, to)) / 2 * to.Sub(r.Timestamp).Hours()
break
}
energy += (r.Value + rn.Value) / 2 * rn.Timestamp.Sub(r.Timestamp).Hours()
}
return energy
}
func timestampSeries(rr api.Rates) timeseries {
return lo.Map(rr, func(r api.Rate, _ int) tsval {
return tsval{
Timestamp: r.Start,
Value: r.Price,
}
})
}