489 lines
15 KiB
Go
489 lines
15 KiB
Go
package charger
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
ucapi "github.com/enbility/eebus-go/usecases/api"
|
|
evcemuc "github.com/enbility/eebus-go/usecases/cem/evcem"
|
|
"github.com/enbility/eebus-go/usecases/mocks"
|
|
spineapi "github.com/enbility/spine-go/api"
|
|
spinemocks "github.com/enbility/spine-go/mocks"
|
|
"github.com/enbility/spine-go/model"
|
|
"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/mock"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/mock/gomock"
|
|
)
|
|
|
|
// Test measurements updated after writing limits detction works
|
|
func TestEEBusNoCurrents(t *testing.T) {
|
|
evcc := mocks.NewCemEVCCInterface(t)
|
|
evcem := mocks.NewCemEVCEMInterface(t)
|
|
|
|
evEntity := spinemocks.NewEntityRemoteInterface(t)
|
|
eebus := &EEBus{
|
|
cem: &eebus.CustomerEnergyManagement{
|
|
EvCC: evcc,
|
|
EvCem: evcem,
|
|
},
|
|
ev: evEntity,
|
|
log: util.NewLogger("test"),
|
|
}
|
|
|
|
evcc.EXPECT().EVConnected(evEntity).Return(true)
|
|
evcem.EXPECT().IsScenarioAvailableAtEntity(evEntity, mock.Anything).Return(true)
|
|
|
|
// limit set 15:04:45, measurement receviced afterwards before calling currents
|
|
eebus.limitUpdated = time.Date(2024, 9, 16, 15, 4, 45, 0, time.UTC)
|
|
eebus.UseCaseEvent(nil, evEntity, evcemuc.DataUpdateCurrentPerPhase)
|
|
|
|
evcem.EXPECT().CurrentPerPhase(evEntity).Return([]float64{10.5, 10.5, 10.5}, nil).Once()
|
|
|
|
l1, l2, l3, err := eebus.currents()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 10.5, l1)
|
|
assert.Equal(t, 10.5, l2)
|
|
assert.Equal(t, 10.5, l3)
|
|
|
|
// limit set 15:05:09, measurement receviced afterwards before calling currents
|
|
eebus.limitUpdated = time.Date(2024, 9, 16, 15, 5, 9, 0, time.UTC)
|
|
eebus.UseCaseEvent(nil, evEntity, evcemuc.DataUpdateCurrentPerPhase)
|
|
|
|
evcem.EXPECT().CurrentPerPhase(evEntity).Return([]float64{6.6, 6.6, 6.6}, nil).Once()
|
|
|
|
l1, l2, l3, err = eebus.currents()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 6.6, l1)
|
|
assert.Equal(t, 6.6, l2)
|
|
assert.Equal(t, 6.6, l3)
|
|
|
|
// limit set 15:05:39, measurement received afterwards before calling currents
|
|
eebus.limitUpdated = time.Date(2024, 9, 16, 15, 5, 39, 0, time.UTC)
|
|
eebus.UseCaseEvent(nil, evEntity, evcemuc.DataUpdateCurrentPerPhase)
|
|
|
|
evcem.EXPECT().CurrentPerPhase(evEntity).Return([]float64{10.4, 10.5, 10.4}, nil).Once()
|
|
|
|
l1, l2, l3, err = eebus.currents()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 10.4, l1)
|
|
assert.Equal(t, 10.5, l2)
|
|
assert.Equal(t, 10.4, l3)
|
|
|
|
// limit set 15:06:09, measurement received afterwards before calling currents
|
|
eebus.limitUpdated = time.Date(2024, 9, 16, 15, 6, 9, 0, time.UTC)
|
|
eebus.UseCaseEvent(nil, evEntity, evcemuc.DataUpdateCurrentPerPhase)
|
|
|
|
evcem.EXPECT().CurrentPerPhase(evEntity).Return([]float64{10.4, 10.4, 10.4}, nil).Once()
|
|
|
|
l1, l2, l3, err = eebus.currents()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 10.4, l1)
|
|
assert.Equal(t, 10.4, l2)
|
|
assert.Equal(t, 10.4, l3)
|
|
|
|
// limit set 20 seconds ago, no measurement received yet
|
|
eebus.limitUpdated = time.Now().Add(-20 * time.Second)
|
|
|
|
l1, l2, l3, err = eebus.currents()
|
|
require.Error(t, err)
|
|
assert.Equal(t, 0.0, l1)
|
|
assert.Equal(t, 0.0, l2)
|
|
assert.Equal(t, 0.0, l3)
|
|
|
|
// now we got a measurement again
|
|
eebus.UseCaseEvent(nil, evEntity, evcemuc.DataUpdateCurrentPerPhase)
|
|
|
|
evcem.EXPECT().CurrentPerPhase(evEntity).Return([]float64{10.4, 10.4, 10.4}, nil).Once()
|
|
|
|
l1, l2, l3, err = eebus.currents()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 10.4, l1)
|
|
assert.Equal(t, 10.4, l2)
|
|
assert.Equal(t, 10.4, l3)
|
|
}
|
|
|
|
// newTestEEBus creates an EEBus instance with all mocks wired up for limit writing tests.
|
|
func newTestEEBus(t *testing.T) (*EEBus, *mocks.CemOPEVInterface, *mocks.CemOSCEVInterface, *spinemocks.EntityRemoteInterface) {
|
|
t.Helper()
|
|
|
|
opev := mocks.NewCemOPEVInterface(t)
|
|
oscev := mocks.NewCemOSCEVInterface(t)
|
|
evEntity := spinemocks.NewEntityRemoteInterface(t)
|
|
|
|
eebus := &EEBus{
|
|
cem: &eebus.CustomerEnergyManagement{
|
|
OpEV: opev,
|
|
OscEV: oscev,
|
|
},
|
|
ev: evEntity,
|
|
log: util.NewLogger("test"),
|
|
}
|
|
|
|
return eebus, opev, oscev, evEntity
|
|
}
|
|
|
|
// 3-phase limits helper
|
|
func opevLimits3p(min, max, def float64) ([]float64, []float64, []float64, error) {
|
|
return []float64{min, min, min}, []float64{max, max, max}, []float64{def, def, def}, nil
|
|
}
|
|
|
|
// ackWrite makes a mocked WriteLoadControlLimits invoke its result callback with a
|
|
// success result, as the real eebus-go does, so eebus.Await completes.
|
|
func ackWrite(_ spineapi.EntityRemoteInterface, _ []ucapi.LoadLimitsPhase, resultCB func(model.ResultDataType, model.MsgCounterType)) {
|
|
resultCB(model.ResultDataType{}, model.MsgCounterType(0))
|
|
}
|
|
|
|
func TestWriteCurrentLimitData_OpevOnly(t *testing.T) {
|
|
eebus, opev, oscev, evEntity := newTestEEBus(t)
|
|
_ = eebus
|
|
|
|
// OPEV available, OSCEV not available
|
|
opev.EXPECT().IsScenarioAvailableAtEntity(evEntity, uint(1)).Return(true)
|
|
opev.EXPECT().CurrentLimits(evEntity).Return(opevLimits3p(6, 16, 0))
|
|
opev.EXPECT().WriteLoadControlLimits(evEntity, mock.MatchedBy(func(limits []ucapi.LoadLimitsPhase) bool {
|
|
return len(limits) == 3 && limits[0].IsActive && limits[0].Value == 10
|
|
}), mock.Anything).Run(ackWrite).Return(nil, nil)
|
|
|
|
oscev.EXPECT().IsScenarioAvailableAtEntity(evEntity, uint(1)).Return(false)
|
|
|
|
err := eebus.writeCurrentLimitData(evEntity, 10)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestWriteCurrentLimitData_OpevAndOscev(t *testing.T) {
|
|
eebus, opev, oscev, evEntity := newTestEEBus(t)
|
|
_ = eebus
|
|
|
|
// Both available, current = 10A (between min and max)
|
|
opev.EXPECT().IsScenarioAvailableAtEntity(evEntity, uint(1)).Return(true)
|
|
opev.EXPECT().CurrentLimits(evEntity).Return(opevLimits3p(6, 16, 0))
|
|
opev.EXPECT().WriteLoadControlLimits(evEntity, mock.MatchedBy(func(limits []ucapi.LoadLimitsPhase) bool {
|
|
// OPEV: active at 10A (below max of 16)
|
|
return len(limits) == 3 && limits[0].IsActive && limits[0].Value == 10
|
|
}), mock.Anything).Run(ackWrite).Return(nil, nil)
|
|
|
|
oscev.EXPECT().IsScenarioAvailableAtEntity(evEntity, uint(1)).Return(true)
|
|
oscev.EXPECT().LoadControlLimits(evEntity).Return([]ucapi.LoadLimitsPhase{}, nil)
|
|
oscev.EXPECT().CurrentLimits(evEntity).Return(opevLimits3p(2, 16, 0))
|
|
oscev.EXPECT().WriteLoadControlLimits(evEntity, mock.MatchedBy(func(limits []ucapi.LoadLimitsPhase) bool {
|
|
// OSCEV: active at 10A (>= min of 2, recommendation to charge)
|
|
return len(limits) == 3 && limits[0].IsActive && limits[0].Value == 10
|
|
}), mock.Anything).Run(ackWrite).Return(nil, nil)
|
|
|
|
err := eebus.writeCurrentLimitData(evEntity, 10)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestWriteCurrentLimitData_AtMax(t *testing.T) {
|
|
eebus, opev, oscev, evEntity := newTestEEBus(t)
|
|
_ = eebus
|
|
|
|
// Current equals max limit
|
|
opev.EXPECT().IsScenarioAvailableAtEntity(evEntity, uint(1)).Return(true)
|
|
opev.EXPECT().CurrentLimits(evEntity).Return(opevLimits3p(6, 16, 0))
|
|
opev.EXPECT().WriteLoadControlLimits(evEntity, mock.MatchedBy(func(limits []ucapi.LoadLimitsPhase) bool {
|
|
// OPEV: inactive at max (no restriction needed)
|
|
return len(limits) == 3 && !limits[0].IsActive && limits[0].Value == 16
|
|
}), mock.Anything).Run(ackWrite).Return(nil, nil)
|
|
|
|
oscev.EXPECT().IsScenarioAvailableAtEntity(evEntity, uint(1)).Return(true)
|
|
oscev.EXPECT().LoadControlLimits(evEntity).Return([]ucapi.LoadLimitsPhase{}, nil)
|
|
oscev.EXPECT().CurrentLimits(evEntity).Return(opevLimits3p(2, 16, 0))
|
|
oscev.EXPECT().WriteLoadControlLimits(evEntity, mock.MatchedBy(func(limits []ucapi.LoadLimitsPhase) bool {
|
|
// OSCEV: active at 16A (>= min, recommend charging)
|
|
return len(limits) == 3 && limits[0].IsActive && limits[0].Value == 16
|
|
}), mock.Anything).Run(ackWrite).Return(nil, nil)
|
|
|
|
err := eebus.writeCurrentLimitData(evEntity, 16)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestWriteCurrentLimitData_Disable(t *testing.T) {
|
|
eebus, opev, oscev, evEntity := newTestEEBus(t)
|
|
_ = eebus
|
|
|
|
// Current = 0 (disable charging)
|
|
opev.EXPECT().IsScenarioAvailableAtEntity(evEntity, uint(1)).Return(true)
|
|
opev.EXPECT().CurrentLimits(evEntity).Return(opevLimits3p(6, 16, 0))
|
|
opev.EXPECT().WriteLoadControlLimits(evEntity, mock.MatchedBy(func(limits []ucapi.LoadLimitsPhase) bool {
|
|
// OPEV: active at 0A (hard stop)
|
|
return len(limits) == 3 && limits[0].IsActive && limits[0].Value == 0
|
|
}), mock.Anything).Run(ackWrite).Return(nil, nil)
|
|
|
|
oscev.EXPECT().IsScenarioAvailableAtEntity(evEntity, uint(1)).Return(true)
|
|
oscev.EXPECT().LoadControlLimits(evEntity).Return([]ucapi.LoadLimitsPhase{}, nil)
|
|
oscev.EXPECT().CurrentLimits(evEntity).Return(opevLimits3p(2, 16, 0))
|
|
oscev.EXPECT().WriteLoadControlLimits(evEntity, mock.MatchedBy(func(limits []ucapi.LoadLimitsPhase) bool {
|
|
// OSCEV: inactive at 0A (no recommendation, < min)
|
|
return len(limits) == 3 && !limits[0].IsActive && limits[0].Value == 0
|
|
}), mock.Anything).Run(ackWrite).Return(nil, nil)
|
|
|
|
err := eebus.writeCurrentLimitData(evEntity, 0)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestWriteCurrentLimitData_OscevNoLimitData(t *testing.T) {
|
|
eebus, opev, oscev, evEntity := newTestEEBus(t)
|
|
_ = eebus
|
|
|
|
// OSCEV scenario available but no limit data (e.g. PCMP wallbox)
|
|
opev.EXPECT().IsScenarioAvailableAtEntity(evEntity, uint(1)).Return(true)
|
|
opev.EXPECT().CurrentLimits(evEntity).Return(opevLimits3p(6, 16, 0))
|
|
opev.EXPECT().WriteLoadControlLimits(evEntity, mock.Anything, mock.Anything).Run(ackWrite).Return(nil, nil)
|
|
|
|
oscev.EXPECT().IsScenarioAvailableAtEntity(evEntity, uint(1)).Return(true)
|
|
oscev.EXPECT().LoadControlLimits(evEntity).Return(nil, errors.New("data not available"))
|
|
// no WriteLoadControlLimits call expected for OSCEV
|
|
|
|
err := eebus.writeCurrentLimitData(evEntity, 10)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestWriteCurrentLimitData_OpevNotAvailable(t *testing.T) {
|
|
eebus, opev, _, evEntity := newTestEEBus(t)
|
|
_ = eebus
|
|
|
|
opev.EXPECT().IsScenarioAvailableAtEntity(evEntity, uint(1)).Return(false)
|
|
|
|
err := eebus.writeCurrentLimitData(evEntity, 10)
|
|
require.ErrorIs(t, err, api.ErrNotAvailable)
|
|
}
|
|
|
|
func TestEnabledAlwaysReadsOpev(t *testing.T) {
|
|
evcc := mocks.NewCemEVCCInterface(t)
|
|
opev := mocks.NewCemOPEVInterface(t)
|
|
|
|
evEntity := spinemocks.NewEntityRemoteInterface(t)
|
|
eebus := &EEBus{
|
|
cem: &eebus.CustomerEnergyManagement{
|
|
EvCC: evcc,
|
|
OpEV: opev,
|
|
},
|
|
ev: evEntity,
|
|
log: util.NewLogger("test"),
|
|
}
|
|
|
|
evcc.EXPECT().EVConnected(evEntity).Return(true)
|
|
opev.EXPECT().LoadControlLimits(evEntity).Return([]ucapi.LoadLimitsPhase{
|
|
{Phase: model.ElectricalConnectionPhaseNameTypeA, IsActive: true, Value: 10},
|
|
{Phase: model.ElectricalConnectionPhaseNameTypeB, IsActive: true, Value: 10},
|
|
{Phase: model.ElectricalConnectionPhaseNameTypeC, IsActive: true, Value: 10},
|
|
}, nil)
|
|
|
|
enabled, err := eebus.Enabled()
|
|
require.NoError(t, err)
|
|
assert.True(t, enabled)
|
|
}
|
|
|
|
func TestEEBusIsCharging(t *testing.T) {
|
|
type limitStruct struct {
|
|
min, max, pause float64
|
|
}
|
|
|
|
type testMeasurementStruct struct {
|
|
charging bool
|
|
currents []float64
|
|
powers []float64
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
limits []limitStruct
|
|
measurements []testMeasurementStruct
|
|
}{
|
|
{
|
|
"3 phase IEC",
|
|
[]limitStruct{
|
|
{6, 16, 0},
|
|
{6, 16, 0},
|
|
{6, 16, 0},
|
|
},
|
|
[]testMeasurementStruct{
|
|
{
|
|
false,
|
|
[]float64{0, 3, 0},
|
|
[]float64{0, 690, 0},
|
|
},
|
|
{
|
|
true,
|
|
[]float64{6, 0, 1},
|
|
[]float64{1380, 0, 230},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"1 phase IEC",
|
|
[]limitStruct{
|
|
{6, 16, 0},
|
|
},
|
|
[]testMeasurementStruct{
|
|
{
|
|
false,
|
|
[]float64{2},
|
|
[]float64{460},
|
|
},
|
|
{
|
|
true,
|
|
[]float64{6},
|
|
[]float64{1380},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"3 phase ISO",
|
|
[]limitStruct{
|
|
{2.2, 16, 0.1},
|
|
{2.2, 16, 0.1},
|
|
{2.2, 16, 0.1},
|
|
},
|
|
[]testMeasurementStruct{
|
|
{
|
|
false,
|
|
[]float64{1, 0, 0},
|
|
[]float64{230, 0, 0},
|
|
},
|
|
{
|
|
true,
|
|
[]float64{1.8, 1, 3},
|
|
[]float64{414, 230, 690},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var limitsMin, limitsMax, limitsDefault []float64
|
|
|
|
for _, limit := range tc.limits {
|
|
limitsMin = append(limitsMin, limit.min)
|
|
limitsMax = append(limitsMax, limit.max)
|
|
limitsDefault = append(limitsDefault, limit.pause)
|
|
}
|
|
|
|
for _, m := range tc.measurements {
|
|
ctrl := gomock.NewController(t)
|
|
|
|
evcc := mocks.NewCemEVCCInterface(t)
|
|
evcem := mocks.NewCemEVCEMInterface(t)
|
|
opev := mocks.NewCemOPEVInterface(t)
|
|
|
|
evEntity := spinemocks.NewEntityRemoteInterface(t)
|
|
eebus := &EEBus{
|
|
cem: &eebus.CustomerEnergyManagement{
|
|
EvCC: evcc,
|
|
EvCem: evcem,
|
|
OpEV: opev,
|
|
},
|
|
ev: evEntity,
|
|
}
|
|
|
|
evcc.EXPECT().EVConnected(evEntity).Return(true)
|
|
evcem.EXPECT().IsScenarioAvailableAtEntity(evEntity, mock.Anything).Return(true)
|
|
evcem.EXPECT().PowerPerPhase(evEntity).Return(m.powers, nil)
|
|
opev.EXPECT().CurrentLimits(evEntity).Return(limitsMin, limitsMax, limitsDefault, nil)
|
|
|
|
require.Equal(t, m.charging, eebus.isCharging(evEntity))
|
|
|
|
ctrl.Finish()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEEBusCurrentPower(t *testing.T) {
|
|
evcc := mocks.NewCemEVCCInterface(t)
|
|
evcem := mocks.NewCemEVCEMInterface(t)
|
|
|
|
evEntity := spinemocks.NewEntityRemoteInterface(t)
|
|
eebus := &EEBus{
|
|
cem: &eebus.CustomerEnergyManagement{
|
|
EvCC: evcc,
|
|
EvCem: evcem,
|
|
},
|
|
ev: evEntity,
|
|
log: util.NewLogger("test"),
|
|
}
|
|
|
|
evcc.EXPECT().EVConnected(evEntity).Return(true)
|
|
evcem.EXPECT().IsScenarioAvailableAtEntity(evEntity, mock.Anything).Return(true)
|
|
evcem.EXPECT().PowerPerPhase(evEntity).Return([]float64{600, 600, 600}, nil)
|
|
|
|
power, err := eebus.currentPower()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1800.0, power)
|
|
}
|
|
|
|
func TestEEBusCurrentPower_Elli(t *testing.T) {
|
|
evcc := mocks.NewCemEVCCInterface(t)
|
|
evcem := mocks.NewCemEVCEMInterface(t)
|
|
|
|
evEntity := spinemocks.NewEntityRemoteInterface(t)
|
|
eebus := &EEBus{
|
|
cem: &eebus.CustomerEnergyManagement{
|
|
EvCC: evcc,
|
|
EvCem: evcem,
|
|
},
|
|
ev: evEntity,
|
|
log: util.NewLogger("test"),
|
|
}
|
|
|
|
evcc.EXPECT().EVConnected(evEntity).Return(true)
|
|
evcem.EXPECT().IsScenarioAvailableAtEntity(evEntity, mock.Anything).Return(true)
|
|
evcem.EXPECT().PowerPerPhase(evEntity).Return(nil, errors.New("error"))
|
|
evcem.EXPECT().CurrentPerPhase(evEntity).Return([]float64{5.8, 5.8, 5.8}, nil)
|
|
|
|
power, err := eebus.currentPower()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 4002.0, power)
|
|
}
|
|
|
|
// minMax must only trust EV current limits under ISO15118; IEC61851 EVs
|
|
// report EVSE defaults that would override the loadpoint config (#14418).
|
|
func TestEEBusMinMaxCommunicationStandard(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
standard model.DeviceConfigurationKeyValueStringType
|
|
iso bool
|
|
}{
|
|
{"iec", model.DeviceConfigurationKeyValueStringTypeIEC61851, false},
|
|
{"unknown", model.DeviceConfigurationKeyValueStringType("unknown"), false},
|
|
{"iso", model.DeviceConfigurationKeyValueStringTypeISO151182ED2, true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
evcc := mocks.NewCemEVCCInterface(t)
|
|
opev := mocks.NewCemOPEVInterface(t)
|
|
evEntity := spinemocks.NewEntityRemoteInterface(t)
|
|
|
|
eebus := &EEBus{
|
|
cem: &eebus.CustomerEnergyManagement{
|
|
EvCC: evcc,
|
|
OpEV: opev,
|
|
},
|
|
ev: evEntity,
|
|
log: util.NewLogger("test"),
|
|
}
|
|
|
|
evcc.EXPECT().EVConnected(evEntity).Return(true)
|
|
evcc.EXPECT().CommunicationStandard(evEntity).Return(tc.standard, nil)
|
|
|
|
if tc.iso {
|
|
opev.EXPECT().CurrentLimits(evEntity).Return(opevLimits3p(6, 16, 0))
|
|
}
|
|
|
|
mm, err := eebus.minMax()
|
|
if tc.iso {
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 6.0, mm.min)
|
|
assert.Equal(t, 16.0, mm.max)
|
|
} else {
|
|
require.ErrorIs(t, err, api.ErrNotAvailable)
|
|
}
|
|
})
|
|
}
|
|
}
|