166 lines
5.6 KiB
Go
166 lines
5.6 KiB
Go
package meter
|
|
|
|
// Conformance suite for EEBus MPC TestSpec V1.0.1 ch.8 (Monitoring Appliance as DUT).
|
|
// evcc's meter (non-grid usage) is the MA; it reads via MaMPCInterface.
|
|
|
|
import (
|
|
"testing"
|
|
|
|
mpcmocks "github.com/enbility/eebus-go/usecases/mocks"
|
|
spineapi "github.com/enbility/spine-go/api"
|
|
spinemocks "github.com/enbility/spine-go/mocks"
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/server/eebus"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func newMPCMeter(t *testing.T) (*EEBus, *mpcmocks.MaMPCInterface, spineapi.EntityRemoteInterface) {
|
|
t.Helper()
|
|
|
|
mm := mpcmocks.NewMaMPCInterface(t)
|
|
entity := spinemocks.NewEntityRemoteInterface(t)
|
|
|
|
c := &EEBus{
|
|
log: util.NewLogger("eebus-mpc-test"),
|
|
mm: mm,
|
|
maEntity: entity,
|
|
scenarios: mpcScenarios,
|
|
}
|
|
|
|
return c, mm, entity
|
|
}
|
|
|
|
// SCE1: total active power (ATC_SCE1_*_MATotalActivePower_*)
|
|
func TestMPC_SCE1_TotalActivePower(t *testing.T) {
|
|
// PT_001: state "normal"; MPC-TS-010 consumption positive, production negative.
|
|
t.Run("ATC_SCE1_PT_MATotalActivePower_001", func(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
dir string
|
|
value float64
|
|
}{
|
|
{"consume", 3300},
|
|
{"produce", -1800},
|
|
} {
|
|
t.Run(tc.dir, func(t *testing.T) {
|
|
c, mm, entity := newMPCMeter(t)
|
|
mm.EXPECT().IsScenarioAvailableAtEntity(entity, eebus.MPCPower).Return(true)
|
|
mm.EXPECT().Power(entity).Return(tc.value, nil)
|
|
|
|
got, err := c.CurrentPower()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tc.value, got)
|
|
})
|
|
}
|
|
})
|
|
|
|
// NT_002: error/out-of-range → discarded (MPC-TS-008).
|
|
t.Run("ATC_SCE1_NT_MATotalActivePower_002", func(t *testing.T) {
|
|
for _, badErr := range nonNormalErrors {
|
|
t.Run(badErr.Error(), func(t *testing.T) {
|
|
c, mm, entity := newMPCMeter(t)
|
|
mm.EXPECT().IsScenarioAvailableAtEntity(entity, eebus.MPCPower).Return(true)
|
|
mm.EXPECT().Power(entity).Return(0, badErr)
|
|
|
|
_, err := c.CurrentPower()
|
|
assert.ErrorIs(t, err, api.ErrNotAvailable)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// SCE2: total consumed energy (ATC_SCE2_*_MATotalConsumedEnergy_*)
|
|
func TestMPC_SCE2_TotalConsumedEnergy(t *testing.T) {
|
|
t.Run("ATC_SCE2_PT_MATotalConsumedEnergy_001", func(t *testing.T) {
|
|
c, mm, entity := newMPCMeter(t)
|
|
mm.EXPECT().IsScenarioAvailableAtEntity(entity, eebus.MPCEnergyConsumed).Return(true)
|
|
mm.EXPECT().EnergyConsumed(entity).Return(9876.5, nil)
|
|
|
|
got, err := c.TotalEnergy()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 9876.5, got)
|
|
})
|
|
|
|
t.Run("ATC_SCE2_NT_MATotalConsumedEnergy_002", func(t *testing.T) {
|
|
for _, badErr := range nonNormalErrors {
|
|
t.Run(badErr.Error(), func(t *testing.T) {
|
|
c, mm, entity := newMPCMeter(t)
|
|
mm.EXPECT().IsScenarioAvailableAtEntity(entity, eebus.MPCEnergyConsumed).Return(true)
|
|
mm.EXPECT().EnergyConsumed(entity).Return(0, badErr)
|
|
|
|
_, err := c.TotalEnergy()
|
|
assert.ErrorIs(t, err, api.ErrNotAvailable)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// SCE3: phase-specific AC current (ATC_SCE3_*_MAActiveACCurrent_*)
|
|
func TestMPC_SCE3_ActiveACCurrent(t *testing.T) {
|
|
// PT_001/003/005 (phase A/B/C, "normal") in one Currents() call.
|
|
t.Run("ATC_SCE3_PT_MAActiveACCurrent_001_003_005", func(t *testing.T) {
|
|
c, mm, entity := newMPCMeter(t)
|
|
mm.EXPECT().IsScenarioAvailableAtEntity(entity, eebus.MPCCurrentPerPhase).Return(true)
|
|
mm.EXPECT().CurrentPerPhase(entity).Return([]float64{5.1, 5.2, 5.3}, nil)
|
|
|
|
l1, l2, l3, err := c.Currents()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []float64{5.1, 5.2, 5.3}, []float64{l1, l2, l3})
|
|
})
|
|
|
|
// NT_002/004/006: error/out-of-range → discarded.
|
|
t.Run("ATC_SCE3_NT_MAActiveACCurrent_002_004_006", func(t *testing.T) {
|
|
for _, badErr := range nonNormalErrors {
|
|
t.Run(badErr.Error(), func(t *testing.T) {
|
|
c, mm, entity := newMPCMeter(t)
|
|
mm.EXPECT().IsScenarioAvailableAtEntity(entity, eebus.MPCCurrentPerPhase).Return(true)
|
|
mm.EXPECT().CurrentPerPhase(entity).Return(nil, badErr)
|
|
|
|
_, _, _, err := c.Currents()
|
|
assert.ErrorIs(t, err, api.ErrNotAvailable)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// SCE4: phase-specific AC voltage (ATC_SCE4_*_MAACVoltage_*)
|
|
func TestMPC_SCE4_ACVoltage(t *testing.T) {
|
|
t.Run("ATC_SCE4_PT_MAACVoltage", func(t *testing.T) {
|
|
c, mm, entity := newMPCMeter(t)
|
|
mm.EXPECT().IsScenarioAvailableAtEntity(entity, eebus.MPCVoltagePerPhase).Return(true)
|
|
mm.EXPECT().VoltagePerPhase(entity).Return([]float64{230.0, 230.5, 229.5}, nil)
|
|
|
|
u1, u2, u3, err := c.Voltages()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []float64{230.0, 230.5, 229.5}, []float64{u1, u2, u3})
|
|
})
|
|
|
|
t.Run("ATC_SCE4_NT_MAACVoltage", func(t *testing.T) {
|
|
for _, badErr := range nonNormalErrors {
|
|
t.Run(badErr.Error(), func(t *testing.T) {
|
|
c, mm, entity := newMPCMeter(t)
|
|
mm.EXPECT().IsScenarioAvailableAtEntity(entity, eebus.MPCVoltagePerPhase).Return(true)
|
|
mm.EXPECT().VoltagePerPhase(entity).Return(nil, badErr)
|
|
|
|
_, _, _, err := c.Voltages()
|
|
assert.ErrorIs(t, err, api.ErrNotAvailable)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestMPCNonCoverage records MPC MA abstract test cases out of scope for evcc.
|
|
func TestMPCNonCoverage(t *testing.T) {
|
|
for _, atc := range []string{
|
|
"ATC_SCE1_PT_MAPhaseActivePower_001", // per-phase active power not exposed by api.Meter
|
|
"ATC_SCE2_PT_MATotalProducedEnergy_001", // produced energy not exposed (consumed only)
|
|
"ATC_SCE5_PT_MAFrequency_001", // grid frequency not exposed by api.Meter
|
|
"ATC_COM_PT_MAPolling_001", // polling cadence owned by eebus-go
|
|
"ATC_COM_PT_MANotification_001", // notification timing owned by eebus-go
|
|
} {
|
|
t.Run(atc, func(t *testing.T) {
|
|
t.Skip("not applicable: evcc meter does not expose this MPC data point")
|
|
})
|
|
}
|
|
}
|