164 lines
6 KiB
Go
164 lines
6 KiB
Go
package eebus
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
ucapi "github.com/enbility/eebus-go/usecases/api"
|
|
"github.com/evcc-io/evcc/core/site"
|
|
"github.com/evcc-io/evcc/server/db"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
testFailsafeConsumption = 4200.0
|
|
testFailsafeProduction = 1000.0
|
|
testFailsafeDuration = 2 * time.Hour
|
|
)
|
|
|
|
// stubSite implements site.API for testing — only GetGridPower is exercised;
|
|
// any other call would dereference the nil embedded interface and panic.
|
|
type stubSite struct {
|
|
site.API
|
|
gridPower float64
|
|
}
|
|
|
|
func (s *stubSite) GetGridPower() float64 { return s.gridPower }
|
|
|
|
// newTestEEBus builds a minimally-wired EEBus suitable for exercising run().
|
|
// The CS interfaces are nil — the failsafe-exit path under test does not call
|
|
// them — and smartgrid persistence is backed by an in-memory SQLite database.
|
|
func newTestEEBus(t *testing.T) *EEBus {
|
|
t.Helper()
|
|
|
|
require.NoError(t, db.NewInstance("sqlite", ":memory:"))
|
|
|
|
failsafeProduction := testFailsafeProduction
|
|
return &EEBus{
|
|
log: util.NewLogger("test"),
|
|
site: &stubSite{},
|
|
heartbeat: util.NewValue[struct{}](time.Hour),
|
|
failsafeConsumptionLimit: testFailsafeConsumption,
|
|
failsafeProductionLimit: &failsafeProduction,
|
|
failsafeDuration: testFailsafeDuration,
|
|
}
|
|
}
|
|
|
|
// assertConsumptionLimit checks the HEMS consumption state through the api.HEMS surface.
|
|
func assertConsumptionLimit(t *testing.T, c *EEBus, limit float64) {
|
|
t.Helper()
|
|
assert.Equal(t, new(limit > 0), c.Dimmed())
|
|
assert.Equal(t, limit, c.MaxConsumptionPower())
|
|
}
|
|
|
|
// assertProductionLimit checks the HEMS production state through the api.HEMS surface.
|
|
func assertProductionLimit(t *testing.T, c *EEBus, active bool) {
|
|
t.Helper()
|
|
assert.Equal(t, new(active), c.Curtailed())
|
|
}
|
|
|
|
// TestRun_HeartbeatLost_EntersFailsafe verifies the LPC-911/LPP-911 transition:
|
|
// a missing heartbeat in the normal state must apply the configured failsafe
|
|
// consumption and production limits.
|
|
func TestRun_HeartbeatLost_EntersFailsafe(t *testing.T) {
|
|
c := newTestEEBus(t)
|
|
// heartbeat never Set -> Get() returns ErrTimeout
|
|
|
|
require.NoError(t, c.run())
|
|
assert.Equal(t, StatusFailsafe, c.status)
|
|
assertConsumptionLimit(t, c, testFailsafeConsumption)
|
|
assertProductionLimit(t, c, true)
|
|
}
|
|
|
|
// TestRun_FailsafeStaysOnMissingHeartbeat is the LPC-921/LPP-921 fix: when the
|
|
// heartbeat is still missing the CS keeps applying the failsafe limit (the
|
|
// self-determined protective default for Unlimited-autonomous) and does not
|
|
// transition to a no-limit state. The previous implementation transitioned to
|
|
// StatusNormal with limit=0 once failsafeDuration elapsed, leaving the system
|
|
// unprotected until heartbeat returned.
|
|
func TestRun_FailsafeStaysOnMissingHeartbeat(t *testing.T) {
|
|
c := newTestEEBus(t)
|
|
c.status = StatusFailsafe
|
|
// statusUpdated set in the past beyond failsafeDuration to verify we do not
|
|
// exit failsafe based on the duration alone.
|
|
c.statusUpdated = time.Now().Add(-2 * testFailsafeDuration)
|
|
// heartbeat missing.
|
|
|
|
require.NoError(t, c.run())
|
|
assert.Equal(t, StatusFailsafe, c.status, "must stay in failsafe when heartbeat is still missing")
|
|
}
|
|
|
|
// TestRun_HeartbeatReturned_AppliesFreshLimit covers LPC-918/919/920: when
|
|
// heartbeat is restored and an EG limit is pending, evcc must leave failsafe
|
|
// immediately and apply the freshly received limit. The previous code waited
|
|
// for failsafeDuration to elapse and then dropped to a zero limit, ignoring
|
|
// the fresh value.
|
|
func TestRun_HeartbeatReturned_AppliesFreshLimit(t *testing.T) {
|
|
const freshLimit = 3000.0
|
|
|
|
c := newTestEEBus(t)
|
|
c.status = StatusFailsafe
|
|
c.statusUpdated = time.Now() // well within failsafeDuration
|
|
c.heartbeat.Set(struct{}{})
|
|
c.consumptionLimit = ucapi.LoadLimit{Value: freshLimit, IsActive: true}
|
|
|
|
require.NoError(t, c.run())
|
|
assert.Equal(t, StatusNormal, c.status)
|
|
// Final state is the fresh limit (the LPC-914/1 block re-applies after the release).
|
|
assertConsumptionLimit(t, c, freshLimit)
|
|
assertProductionLimit(t, c, false)
|
|
}
|
|
|
|
// TestRun_HeartbeatReturned_NoFreshLimit covers the LPC-918 release case:
|
|
// heartbeat restored but EG has no active limit pending -> exit to normal,
|
|
// no limit applied.
|
|
func TestRun_HeartbeatReturned_NoFreshLimit(t *testing.T) {
|
|
c := newTestEEBus(t)
|
|
c.status = StatusFailsafe
|
|
c.heartbeat.Set(struct{}{})
|
|
c.consumptionLimit = ucapi.LoadLimit{IsActive: false}
|
|
|
|
require.NoError(t, c.run())
|
|
assert.Equal(t, StatusNormal, c.status)
|
|
assertConsumptionLimit(t, c, 0)
|
|
assertProductionLimit(t, c, false)
|
|
}
|
|
|
|
// TestRun_ProductionLimitReleasedEarly verifies that an active production limit
|
|
// is released as soon as the EG deactivates it (IsActive=false), without waiting
|
|
// for its duration to elapse. The previous code only released on duration expiry,
|
|
// so unchecking "Activate" in the control box had no effect until the timer ran
|
|
// out (see PR #30284 report).
|
|
func TestRun_ProductionLimitReleasedEarly(t *testing.T) {
|
|
c := newTestEEBus(t)
|
|
c.heartbeat.Set(struct{}{})
|
|
|
|
// EG activates a production limit with a long duration.
|
|
c.productionLimit = ucapi.LoadLimit{IsActive: true, Duration: time.Hour}
|
|
require.NoError(t, c.run())
|
|
assertProductionLimit(t, c, true)
|
|
|
|
// EG deactivates well within the duration -> must release immediately.
|
|
c.productionLimit.IsActive = false
|
|
require.NoError(t, c.run())
|
|
assertProductionLimit(t, c, false)
|
|
}
|
|
|
|
// TestRun_ConsumptionLimitReleasedEarly is the LPC mirror of the LPP early-release
|
|
// case: an active consumption limit must drop as soon as the EG deactivates it.
|
|
func TestRun_ConsumptionLimitReleasedEarly(t *testing.T) {
|
|
c := newTestEEBus(t)
|
|
c.heartbeat.Set(struct{}{})
|
|
|
|
// EG activates a consumption limit with a long duration.
|
|
c.consumptionLimit = ucapi.LoadLimit{Value: 3000, IsActive: true, Duration: time.Hour}
|
|
require.NoError(t, c.run())
|
|
assertConsumptionLimit(t, c, 3000)
|
|
|
|
// EG deactivates well within the duration -> must release immediately.
|
|
c.consumptionLimit.IsActive = false
|
|
require.NoError(t, c.run())
|
|
assertConsumptionLimit(t, c, 0)
|
|
}
|