Skip to content

Commit f268091

Browse files
authored
chore: remove dedicated timeseries type (#22692)
1 parent 2bd410a commit f268091

File tree

4 files changed

+106
-144
lines changed

4 files changed

+106
-144
lines changed

core/site_tariffs.go

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type solarDetails struct {
1919
Today dailyDetails `json:"today,omitempty"` // tomorrow
2020
Tomorrow dailyDetails `json:"tomorrow,omitempty"` // tomorrow
2121
DayAfterTomorrow dailyDetails `json:"dayAfterTomorrow,omitempty"` // day after tomorrow
22-
Timeseries timeseries `json:"timeseries,omitempty"` // timeseries of forecasted energy
22+
Timeseries []tsEntry `json:"timeseries,omitempty"` // timeseries of forecasted energy
2323
}
2424

2525
type dailyDetails struct {
@@ -113,27 +113,27 @@ func (site *Site) publishTariffs(greenShareHome float64, greenShareLoadpoints fl
113113
}
114114

115115
// calculate adjusted solar forecast
116-
if solar := timestampSeries(tariff.Forecast(site.GetTariff(api.TariffUsageSolar))); len(solar) > 0 {
116+
if solar := tariff.Forecast(site.GetTariff(api.TariffUsageSolar)); len(solar) > 0 {
117117
fc.Solar = lo.ToPtr(site.solarDetails(solar))
118118
}
119119

120120
site.publish(keys.Forecast, fc)
121121
}
122122

123-
func (site *Site) solarDetails(solar timeseries) solarDetails {
123+
func (site *Site) solarDetails(solar api.Rates) solarDetails {
124124
res := solarDetails{
125-
Timeseries: solar,
125+
Timeseries: solarTimeseries(solar),
126126
}
127127

128-
last := solar[len(solar)-1].Timestamp
128+
last := solar[len(solar)-1].Start
129129

130-
bod := beginningOfDay(time.Now())
130+
bod := now.BeginningOfDay()
131131
eod := bod.AddDate(0, 0, 1)
132132
eot := eod.AddDate(0, 0, 1)
133133

134-
remainingToday := solar.energy(time.Now(), eod)
135-
tomorrow := solar.energy(eod, eot)
136-
dayAfterTomorrow := solar.energy(eot, eot.AddDate(0, 0, 1))
134+
remainingToday := solarEnergy(solar, time.Now(), eod)
135+
tomorrow := solarEnergy(solar, eod, eot)
136+
dayAfterTomorrow := solarEnergy(solar, eot, eot.AddDate(0, 0, 1))
137137

138138
res.Today = dailyDetails{
139139
Yield: remainingToday,
@@ -149,7 +149,7 @@ func (site *Site) solarDetails(solar timeseries) solarDetails {
149149
}
150150

151151
// accumulate forecasted energy since last update
152-
site.fcstEnergy.AddEnergy(solar.energy(site.fcstEnergy.updated, time.Now()) / 1e3)
152+
site.fcstEnergy.AddEnergy(solarEnergy(solar, site.fcstEnergy.updated, time.Now()) / 1e3)
153153
settings.SetFloat(keys.SolarAccForecast, site.fcstEnergy.Accumulated)
154154

155155
produced := lo.SumBy(slices.Collect(maps.Values(site.pvEnergy)), func(v *meterEnergy) float64 {
@@ -173,7 +173,3 @@ func (site *Site) isDynamicTariff(usage api.TariffUsage) bool {
173173
tariff := site.GetTariff(usage)
174174
return tariff != nil && tariff.Type() != api.TariffTypePriceStatic
175175
}
176-
177-
func beginningOfDay(t time.Time) time.Time {
178-
return now.With(t).BeginningOfDay()
179-
}

core/solar.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package core
2+
3+
import (
4+
"slices"
5+
"time"
6+
7+
"github.com/evcc-io/evcc/api"
8+
"github.com/samber/lo"
9+
)
10+
11+
type tsEntry struct {
12+
Timestamp time.Time `json:"ts"`
13+
Value float64 `json:"val"`
14+
}
15+
16+
func solarTimeseries(rr api.Rates) []tsEntry {
17+
return lo.Map(rr, func(r api.Rate, _ int) tsEntry {
18+
return tsEntry{Timestamp: r.Start, Value: r.Value}
19+
})
20+
}
21+
22+
func search(rr api.Rates, ts time.Time) (int, bool) {
23+
return slices.BinarySearchFunc(rr, ts, func(v api.Rate, ts time.Time) int {
24+
return v.Start.Compare(ts)
25+
})
26+
}
27+
28+
// interpolate returns the interpolated value where ts is between two entries and i is the index of the rate after ts
29+
func interpolate(rr api.Rates, i int, ts time.Time) float64 {
30+
rp := &rr[i-1]
31+
r := &rr[i]
32+
return rp.Value + float64(ts.Sub(rp.Start))*(r.Value-rp.Value)/float64(r.Start.Sub(rp.Start))
33+
}
34+
35+
// solarEnergy calculates the energy consumption between from and to,
36+
// assuming the rates containing the power at given timestamp.
37+
// Result is in Wh
38+
func solarEnergy(rr api.Rates, from, to time.Time) float64 {
39+
var energy float64
40+
41+
idx, ok := search(rr, from)
42+
if !ok {
43+
switch {
44+
case idx >= len(rr):
45+
// from is just before or after last entry
46+
return 0
47+
case idx == 0:
48+
// from is before first entry
49+
// do nothing- we ignore anything before the first entry
50+
default:
51+
// from is between two entries
52+
r := &rr[idx]
53+
vp := interpolate(rr, idx, from)
54+
55+
// to is before same entry as from
56+
if r.Start.After(to) {
57+
return (vp + interpolate(rr, idx, to)) / 2 * to.Sub(from).Hours()
58+
}
59+
60+
energy += (vp + r.Value) / 2 * r.Start.Sub(from).Hours()
61+
}
62+
}
63+
64+
for ; idx < len(rr)-1; idx++ {
65+
r := &rr[idx]
66+
rn := &rr[idx+1]
67+
68+
if rn.Start.After(to) {
69+
energy += (r.Value + interpolate(rr, idx+1, to)) / 2 * to.Sub(r.Start).Hours()
70+
break
71+
}
72+
73+
energy += (r.Value + rn.Value) / 2 * rn.Start.Sub(r.Start).Hours()
74+
}
75+
76+
return energy
77+
}

core/timeseries_test.go renamed to core/solar_test.go

Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,36 @@ import (
55
"time"
66

77
"github.com/benbjohnson/clock"
8+
"github.com/evcc-io/evcc/api"
89
"github.com/jinzhu/now"
910
"github.com/stretchr/testify/suite"
1011
)
1112

12-
func TestTimeseries(t *testing.T) {
13-
suite.Run(t, new(timeseriesTestSuite))
13+
func TestSolarRates(t *testing.T) {
14+
suite.Run(t, new(solarTestSuite))
1415
}
1516

16-
type timeseriesTestSuite struct {
17+
type solarTestSuite struct {
1718
suite.Suite
1819
clock *clock.Mock
19-
rr timeseries
20+
rr api.Rates
2021
}
2122

22-
func (t *timeseriesTestSuite) rate(start int, val float64) tsval {
23-
return tsval{
24-
Timestamp: t.clock.Now().Add(time.Duration(start) * time.Hour),
25-
Value: val,
23+
func (t *solarTestSuite) rate(start int, val float64) api.Rate {
24+
return api.Rate{
25+
Start: t.clock.Now().Add(time.Duration(start) * time.Hour),
26+
End: t.clock.Now().Add(time.Duration(start+1) * time.Hour),
27+
Value: val,
2628
}
2729
}
2830

29-
func (t *timeseriesTestSuite) SetupSuite() {
31+
func (t *solarTestSuite) SetupSuite() {
3032
t.clock = clock.NewMock()
3133
t.clock.Set(now.BeginningOfDay())
32-
t.rr = timeseries{t.rate(0, 0), t.rate(1, 1), t.rate(2, 2), t.rate(3, 3), t.rate(4, 4)}
34+
t.rr = api.Rates{t.rate(0, 0), t.rate(1, 1), t.rate(2, 2), t.rate(3, 3), t.rate(4, 4)}
3335
}
3436

35-
func (t *timeseriesTestSuite) TestIndex() {
37+
func (t *solarTestSuite) TestIndex() {
3638
for i, tc := range []struct {
3739
ts float64
3840
idx int
@@ -45,30 +47,13 @@ func (t *timeseriesTestSuite) TestIndex() {
4547
{99, len(t.rr), false},
4648
} {
4749
ts := t.clock.Now().Add(time.Duration(float64(time.Hour) * tc.ts))
48-
res, ok := t.rr.search(ts)
50+
res, ok := search(t.rr, ts)
4951
t.Equal(tc.idx, res, "%d. idx %+v", i+1, tc)
5052
t.Equal(tc.ok, ok, "%d. ok %+v", i+1, tc)
5153
}
5254
}
5355

54-
func (t *timeseriesTestSuite) TestValue() {
55-
for i, tc := range []struct {
56-
ts, val float64
57-
}{
58-
{-1, 0},
59-
{0, 0},
60-
{0.5, 0.5},
61-
{1, 1},
62-
{4, 4},
63-
{99, 0},
64-
} {
65-
ts := t.clock.Now().Add(time.Duration(float64(time.Hour) * tc.ts))
66-
res := t.rr.value(ts)
67-
t.Equal(tc.val, res, "%d. %+v", i+1, tc)
68-
}
69-
}
70-
71-
func (t *timeseriesTestSuite) TestEnergy() {
56+
func (t *solarTestSuite) TestEnergy() {
7257
for i, tc := range []struct {
7358
from, to float64
7459
expected float64
@@ -90,14 +75,14 @@ func (t *timeseriesTestSuite) TestEnergy() {
9075
from := t.clock.Now().Add(time.Duration(float64(time.Hour) * tc.from))
9176
to := t.clock.Now().Add(time.Duration(float64(time.Hour) * tc.to))
9277

93-
res := t.rr.energy(from, to)
78+
res := solarEnergy(t.rr, from, to)
9479
t.Equal(tc.expected, res, "%d. %+v", i+1, tc)
9580
}
9681
}
9782

98-
func (t *timeseriesTestSuite) TestShort() {
83+
func (t *solarTestSuite) TestShort() {
9984
t.clock.Set(now.BeginningOfDay())
100-
rr := timeseries{t.rate(0, 0), t.rate(1, 1)}
85+
rr := api.Rates{t.rate(0, 0), t.rate(1, 1)}
10186

10287
for i, tc := range []struct {
10388
from, to, energy, value float64
@@ -114,7 +99,6 @@ func (t *timeseriesTestSuite) TestShort() {
11499
from := t.clock.Now().Add(time.Duration(float64(time.Hour) * tc.from))
115100
to := t.clock.Now().Add(time.Duration(float64(time.Hour) * tc.to))
116101

117-
t.Equal(tc.energy, rr.energy(from, to), "%d. energy %+v", i+1, tc)
118-
t.Equal(tc.value, rr.value(to), "%d. value %+v", i+1, tc)
102+
t.Equal(tc.energy, solarEnergy(rr, from, to), "%d. energy %+v", i+1, tc)
119103
}
120104
}

core/timeseries.go

Lines changed: 0 additions & 95 deletions
This file was deleted.

0 commit comments

Comments
 (0)