evcc-io/core/solar_test.go

152 lines
3.9 KiB
Go

package core
import (
"testing"
"time"
"github.com/benbjohnson/clock"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/tariff"
"github.com/jinzhu/now"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
func TestSolarRates(t *testing.T) {
suite.Run(t, new(solarTestSuite))
}
type solarTestSuite struct {
suite.Suite
clock *clock.Mock
rr api.Rates
}
func (t *solarTestSuite) rate(start int, val float64) api.Rate {
return api.Rate{
Start: t.clock.Now().Add(time.Duration(start) * time.Hour),
End: t.clock.Now().Add(time.Duration(start+1) * time.Hour),
Value: val,
}
}
func (t *solarTestSuite) SetupSuite() {
t.clock = clock.NewMock()
t.clock.Set(now.BeginningOfDay())
t.rr = api.Rates{t.rate(0, 0), t.rate(1, 1), t.rate(2, 2), t.rate(3, 3), t.rate(4, 4)}
}
func (t *solarTestSuite) TestIndex() {
for i, tc := range []struct {
ts float64
idx int
ok bool
}{
{-1, 0, false},
{0, 0, true},
{0.5, 1, false},
{1, 1, true},
{99, len(t.rr), false},
} {
ts := t.clock.Now().Add(time.Duration(float64(time.Hour) * tc.ts))
res, ok := search(t.rr, ts)
t.Equal(tc.idx, res, "%d. idx %+v", i+1, tc)
t.Equal(tc.ok, ok, "%d. ok %+v", i+1, tc)
}
}
func (t *solarTestSuite) TestEnergy() {
for i, tc := range []struct {
from, to float64
expected float64
}{
{-1, 0, 0},
{-2, -1, 0}, // whole interval before first entry
{-1, -0.5, 0}, // whole interval before first entry
{-1, 1, 0.5},
{-1, 90, 8},
{0, 0, 0},
{0, 0.5, 0.125},
{0, 1, 0.5},
{0, 1.5, 1.125},
{0, 2, 2},
{1, 2, 1.5},
{0.25, 0.75, 0.25},
{0.5, 1, 0.375},
{0.5, 3.5, 6},
{80, 90, 0},
} {
from := t.clock.Now().Add(time.Duration(float64(time.Hour) * tc.from))
to := t.clock.Now().Add(time.Duration(float64(time.Hour) * tc.to))
res := solarEnergy(t.rr, from, to)
t.Equal(tc.expected, res, "%d. %+v", i+1, tc)
}
}
func (t *solarTestSuite) TestShort() {
t.clock.Set(now.BeginningOfDay())
rr := api.Rates{t.rate(0, 0), t.rate(1, 1)}
for i, tc := range []struct {
from, to, energy, value float64
}{
{-1, 0, 0, 0},
// {-1, 0.5, 0.125, 0.5},
// {-1, 2, 0.5, 0},
{0, 0, 0, 0},
{0, 0.5, 0.125, 0.5},
{0, 1, 0.5, 1},
{0, 1.5, 0.5, 0},
{1.5, 2, 0, 0},
} {
from := t.clock.Now().Add(time.Duration(float64(time.Hour) * tc.from))
to := t.clock.Now().Add(time.Duration(float64(time.Hour) * tc.to))
t.Equal(tc.energy, solarEnergy(rr, from, to), "%d. energy %+v", i+1, tc)
}
}
func TestSolarEnergyNoRates(t *testing.T) {
// empty rate series must not panic and yields no energy
now := time.Now()
assert.Equal(t, 0.0, solarEnergy(api.Rates{}, now, now.Add(time.Hour)))
assert.Equal(t, 0.0, solarEnergy(nil, now, now.Add(time.Hour)))
}
type solarTariff struct {
rates api.Rates
}
func (t *solarTariff) Rates() (api.Rates, error) { return t.rates, nil }
func (t *solarTariff) Type() api.TariffType { return api.TariffTypeSolar }
// TestSolarSlotSplitPreservesEnergy asserts that splitting hourly solar rates into
// 15min slots leaves the integrated energy untouched- the wrapper resamples the
// curve, it does not change it
func TestSolarSlotSplitPreservesEnergy(t *testing.T) {
start := time.Now().Truncate(tariff.SlotDuration)
var rr api.Rates
for i, v := range []float64{0, 1000, 2500, 3000, 1500, 0} {
s := start.Add(time.Duration(i) * time.Hour)
rr = append(rr, api.Rate{Start: s, End: s.Add(time.Hour), Value: v})
}
w := &tariff.SlotWrapper{Tariff: &solarTariff{rates: rr}}
res, err := w.Rates()
require.NoError(t, err)
require.Len(t, res, 6*4)
to := rr[len(rr)-1].Start
assert.InDelta(t, solarEnergy(rr, start, to), solarEnergy(res, start, to), 1e-9)
// every sub-interval integrates identically, not just the total
for i := range res {
assert.InDelta(t,
solarEnergy(rr, start, res[i].Start),
solarEnergy(res, start, res[i].Start),
1e-9, "slot %d", i)
}
}