evcc-io/core/site_circuits_test.go

177 lines
4.5 KiB
Go

package core
import (
"errors"
"testing"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// curtailableMeter counts device interactions to verify caching.
type curtailableMeter struct {
api.Meter
percent int
getErr error
setErr error
gets int
setCalls []int
}
func (m *curtailableMeter) CurtailedPercent() (int, error) {
m.gets++
return m.percent, m.getErr
}
func (m *curtailableMeter) SetCurtailPercent(percent int) error {
m.setCalls = append(m.setCalls, percent)
if m.setErr != nil {
return m.setErr
}
m.percent = percent
return nil
}
func curtailSite(m api.Meter) *Site {
return &Site{
log: util.NewLogger("foo"),
pvMeters: []config.Device[api.Meter]{config.NewStaticDevice(config.Named{}, m)},
}
}
// The HEMS percent is applied once and not re-evaluated while it stays unchanged.
func TestCurtailPVCache(t *testing.T) {
m := &curtailableMeter{percent: 100}
site := curtailSite(m)
require.NoError(t, site.curtailPV(new(60)))
assert.Equal(t, []int{60}, m.setCalls)
assert.Equal(t, 1, m.gets)
// unchanged: device is not queried again
for range 3 {
require.NoError(t, site.curtailPV(new(60)))
}
assert.Equal(t, []int{60}, m.setCalls)
assert.Equal(t, 1, m.gets)
// changed: applied again. A bool device state could not distinguish 60 from 30
require.NoError(t, site.curtailPV(new(30)))
assert.Equal(t, []int{60, 30}, m.setCalls)
require.NoError(t, site.curtailPV(new(100)))
assert.Equal(t, []int{60, 30, 100}, m.setCalls)
}
// A failed write must be retried on the next cycle instead of being cached.
func TestCurtailPVCacheRetriesAfterError(t *testing.T) {
m := &curtailableMeter{percent: 100, setErr: errors.New("nope")}
site := curtailSite(m)
require.Error(t, site.curtailPV(new(60)))
require.Error(t, site.curtailPV(new(60)))
assert.Equal(t, []int{60, 60}, m.setCalls)
m.setErr = nil
require.NoError(t, site.curtailPV(new(60)))
require.NoError(t, site.curtailPV(new(60)))
assert.Equal(t, []int{60, 60, 60}, m.setCalls)
}
// nil percent means the HEMS makes no statement and must not touch the cache.
func TestCurtailPVNoStatement(t *testing.T) {
m := &curtailableMeter{percent: 100}
site := curtailSite(m)
require.NoError(t, site.curtailPV(new(60)))
require.NoError(t, site.curtailPV(nil))
require.NoError(t, site.curtailPV(new(60)))
assert.Equal(t, []int{60}, m.setCalls)
}
// A device that cannot report its state is written once, not on every cycle.
func TestCurtailPVNotAvailable(t *testing.T) {
m := &curtailableMeter{getErr: api.ErrNotAvailable}
site := curtailSite(m)
for range 3 {
require.NoError(t, site.curtailPV(new(60)))
}
assert.Equal(t, []int{60}, m.setCalls)
// changed: applied again
require.NoError(t, site.curtailPV(new(30)))
assert.Equal(t, []int{60, 30}, m.setCalls)
}
// dimmableMeter counts device interactions to verify caching.
type dimmableMeter struct {
api.Meter
dimmed bool
getErr error
dimErr error
gets int
dimCalls []bool
}
func (m *dimmableMeter) Dimmed() (bool, error) {
m.gets++
return m.dimmed, m.getErr
}
func (m *dimmableMeter) Dim(dim bool) error {
m.dimCalls = append(m.dimCalls, dim)
if m.dimErr != nil {
return m.dimErr
}
m.dimmed = dim
return nil
}
func dimSite(m api.Meter) *Site {
return &Site{
log: util.NewLogger("foo"),
auxMeters: []config.Device[api.Meter]{config.NewStaticDevice(config.Named{}, m)},
}
}
func TestDimMetersCache(t *testing.T) {
m := &dimmableMeter{}
site := dimSite(m)
require.NoError(t, site.dimMeters(true))
assert.Equal(t, []bool{true}, m.dimCalls)
assert.Equal(t, 1, m.gets)
for range 3 {
require.NoError(t, site.dimMeters(true))
}
assert.Equal(t, []bool{true}, m.dimCalls)
assert.Equal(t, 1, m.gets)
require.NoError(t, site.dimMeters(false))
assert.Equal(t, []bool{true, false}, m.dimCalls)
// failed write is retried
m.dimErr = errors.New("nope")
require.Error(t, site.dimMeters(true))
require.Error(t, site.dimMeters(true))
assert.Equal(t, []bool{true, false, true, true}, m.dimCalls)
}
// A device that cannot report its state is written once, not on every cycle.
func TestDimMetersNotAvailable(t *testing.T) {
m := &dimmableMeter{getErr: api.ErrNotAvailable}
site := dimSite(m)
for range 3 {
require.NoError(t, site.dimMeters(true))
}
assert.Equal(t, []bool{true}, m.dimCalls)
require.NoError(t, site.dimMeters(false))
assert.Equal(t, []bool{true, false}, m.dimCalls)
}