evcc-io/meter/eebus_mpc_test.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")
})
}
}