From 6834d885fbc84ff93f407723496bc93ccc8fa049 Mon Sep 17 00:00:00 2001 From: Simon Thelen Date: Mon, 4 Nov 2024 09:10:22 +0100 Subject: [PATCH] Only add scenarios when they're supported This commit also adds some fixes to allow leaving unsupported config parameters to NewMPC as nil and a test for those scenarios. --- usecases/mu/mpc/usecase.go | 165 +++++++++++++++++++------------- usecases/mu/mpc/usecase_test.go | 151 +++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+), 67 deletions(-) create mode 100644 usecases/mu/mpc/usecase_test.go diff --git a/usecases/mu/mpc/usecase.go b/usecases/mu/mpc/usecase.go index 8538a05f..d7fcd903 100644 --- a/usecases/mu/mpc/usecase.go +++ b/usecases/mu/mpc/usecase.go @@ -31,6 +31,19 @@ type MPC struct { acFrequency *model.MeasurementIdType } +// creates a new MPC usecase instance for a MonitoredUnit entity +// +// parameters: +// - localEntity: the local entity for which to construct an MPC instance +// - eventCB: the callback to notify about events for this usecase +// - monitorPowerConfig: (required) configuration parameters for MPC scenario 1 +// - monitorEnergyConfig: (optional) configuration parameters for MPC scenario 2, nil if not supported +// - monitorCurrentConfig: (optional) configuration parameters for MPC scenario 3, nil if not supported +// - monitorVoltageConfig: (optional) configuration parameters for MPC scenario 4, nil if not supported +// - monitorFrequencyConfig: (optional) configuration parameters for MPC scenario, nil if not supported5 +// +// possible errors: +// - if required fields in parameters are unset func NewMPC( localEntity spineapi.EntityLocalInterface, eventCB api.EntityEventCallback, @@ -54,38 +67,50 @@ func NewMPC( model.FeatureTypeTypeMeasurement, }, }, - { + } + + if monitorEnergyConfig != nil { + useCaseScenarios = append(useCaseScenarios, api.UseCaseScenario{ Scenario: model.UseCaseScenarioSupportType(2), Mandatory: false, ServerFeatures: []model.FeatureTypeType{ model.FeatureTypeTypeElectricalConnection, model.FeatureTypeTypeMeasurement, }, - }, - { + }) + } + + if monitorCurrentConfig != nil { + useCaseScenarios = append(useCaseScenarios, api.UseCaseScenario{ Scenario: model.UseCaseScenarioSupportType(3), Mandatory: false, ServerFeatures: []model.FeatureTypeType{ model.FeatureTypeTypeElectricalConnection, model.FeatureTypeTypeMeasurement, }, - }, - { + }) + } + + if monitorVoltageConfig != nil { + useCaseScenarios = append(useCaseScenarios, api.UseCaseScenario{ Scenario: model.UseCaseScenarioSupportType(4), Mandatory: false, ServerFeatures: []model.FeatureTypeType{ model.FeatureTypeTypeElectricalConnection, model.FeatureTypeTypeMeasurement, }, - }, - { + }) + } + + if monitorFrequencyConfig != nil { + useCaseScenarios = append(useCaseScenarios, api.UseCaseScenario{ Scenario: model.UseCaseScenarioSupportType(5), Mandatory: false, ServerFeatures: []model.FeatureTypeType{ model.FeatureTypeTypeElectricalConnection, model.FeatureTypeTypeMeasurement, }, - }, + }) } u := usecase.NewUseCaseBase( @@ -175,74 +200,80 @@ func (e *MPC) AddFeatures() { } } - if e.energyConfig.ValueSourceConsumption != nil { - e.acEnergyConsumed = measurements.AddDescription(model.MeasurementDescriptionDataType{ - MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), - CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), - Unit: util.Ptr(model.UnitOfMeasurementTypeWh), - ScopeType: util.Ptr(model.ScopeTypeTypeACEnergyConsumed), - }) - if e.energyConfig.ValueConstraintsConsumption != nil { - e.energyConfig.ValueConstraintsConsumption.MeasurementId = e.acEnergyConsumed - constraints = append(constraints, *e.energyConfig.ValueConstraintsConsumption) - } - } - - if e.energyConfig.ValueSourceProduction != nil { - e.acEnergyProduced = measurements.AddDescription(model.MeasurementDescriptionDataType{ - MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), - CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), - Unit: util.Ptr(model.UnitOfMeasurementTypeWh), - ScopeType: util.Ptr(model.ScopeTypeTypeACEnergyProduced), - }) - if e.energyConfig.ValueConstraintsProduction != nil { - e.energyConfig.ValueConstraintsProduction.MeasurementId = e.acEnergyProduced - constraints = append(constraints, *e.energyConfig.ValueConstraintsProduction) + if e.energyConfig != nil { + if e.energyConfig.ValueSourceConsumption != nil { + e.acEnergyConsumed = measurements.AddDescription(model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + Unit: util.Ptr(model.UnitOfMeasurementTypeWh), + ScopeType: util.Ptr(model.ScopeTypeTypeACEnergyConsumed), + }) + if e.energyConfig.ValueConstraintsConsumption != nil { + e.energyConfig.ValueConstraintsConsumption.MeasurementId = e.acEnergyConsumed + constraints = append(constraints, *e.energyConfig.ValueConstraintsConsumption) + } } - } - acCurrentConstraints := []*model.MeasurementConstraintsDataType{ - e.currentConfig.ValueConstraintsPhaseA, - e.currentConfig.ValueConstraintsPhaseB, - e.currentConfig.ValueConstraintsPhaseC, - } - for id := 0; id < len(e.acCurrent); id++ { - if e.powerConfig.SupportsPhases(phases[id]) { - e.acCurrent[id] = measurements.AddDescription(model.MeasurementDescriptionDataType{ - MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), + if e.energyConfig.ValueSourceProduction != nil { + e.acEnergyProduced = measurements.AddDescription(model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeEnergy), CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), - Unit: util.Ptr(model.UnitOfMeasurementTypeA), - ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + Unit: util.Ptr(model.UnitOfMeasurementTypeWh), + ScopeType: util.Ptr(model.ScopeTypeTypeACEnergyProduced), }) - if acCurrentConstraints[id] != nil { - acCurrentConstraints[id].MeasurementId = e.acCurrent[id] - constraints = append(constraints, *acCurrentConstraints[id]) + if e.energyConfig.ValueConstraintsProduction != nil { + e.energyConfig.ValueConstraintsProduction.MeasurementId = e.acEnergyProduced + constraints = append(constraints, *e.energyConfig.ValueConstraintsProduction) } } } - acVoltageConstraints := []*model.MeasurementConstraintsDataType{ - e.voltageConfig.ValueConstraintsPhaseA, - e.voltageConfig.ValueConstraintsPhaseB, - e.voltageConfig.ValueConstraintsPhaseC, - e.voltageConfig.ValueConstraintsPhaseAToB, - e.voltageConfig.ValueConstraintsPhaseBToC, - e.voltageConfig.ValueConstraintsPhaseCToA, - } - for id := 0; id < len(e.acVoltage); id++ { - if e.powerConfig.SupportsPhases(phases[id]) { - if len(phases[id]) == 2 && !e.voltageConfig.SupportPhaseToPhase { - continue + if e.currentConfig != nil { + acCurrentConstraints := []*model.MeasurementConstraintsDataType{ + e.currentConfig.ValueConstraintsPhaseA, + e.currentConfig.ValueConstraintsPhaseB, + e.currentConfig.ValueConstraintsPhaseC, + } + for id := 0; id < len(e.acCurrent); id++ { + if e.powerConfig.SupportsPhases(phases[id]) { + e.acCurrent[id] = measurements.AddDescription(model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeCurrent), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + Unit: util.Ptr(model.UnitOfMeasurementTypeA), + ScopeType: util.Ptr(model.ScopeTypeTypeACCurrent), + }) + if acCurrentConstraints[id] != nil { + acCurrentConstraints[id].MeasurementId = e.acCurrent[id] + constraints = append(constraints, *acCurrentConstraints[id]) + } } - e.acVoltage[id] = measurements.AddDescription(model.MeasurementDescriptionDataType{ - MeasurementType: util.Ptr(model.MeasurementTypeTypeVoltage), - CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), - Unit: util.Ptr(model.UnitOfMeasurementTypeV), - ScopeType: util.Ptr(model.ScopeTypeTypeACVoltage), - }) - if acVoltageConstraints[id] != nil { - acVoltageConstraints[id].MeasurementId = e.acVoltage[id] - constraints = append(constraints, *acVoltageConstraints[id]) + } + } + + if e.voltageConfig != nil { + acVoltageConstraints := []*model.MeasurementConstraintsDataType{ + e.voltageConfig.ValueConstraintsPhaseA, + e.voltageConfig.ValueConstraintsPhaseB, + e.voltageConfig.ValueConstraintsPhaseC, + e.voltageConfig.ValueConstraintsPhaseAToB, + e.voltageConfig.ValueConstraintsPhaseBToC, + e.voltageConfig.ValueConstraintsPhaseCToA, + } + for id := 0; id < len(e.acVoltage); id++ { + if e.powerConfig.SupportsPhases(phases[id]) { + if len(phases[id]) == 2 && !e.voltageConfig.SupportPhaseToPhase { + continue + } + e.acVoltage[id] = measurements.AddDescription(model.MeasurementDescriptionDataType{ + MeasurementType: util.Ptr(model.MeasurementTypeTypeVoltage), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + Unit: util.Ptr(model.UnitOfMeasurementTypeV), + ScopeType: util.Ptr(model.ScopeTypeTypeACVoltage), + }) + if acVoltageConstraints[id] != nil { + acVoltageConstraints[id].MeasurementId = e.acVoltage[id] + constraints = append(constraints, *acVoltageConstraints[id]) + } } } } diff --git a/usecases/mu/mpc/usecase_test.go b/usecases/mu/mpc/usecase_test.go new file mode 100644 index 00000000..af42d9f2 --- /dev/null +++ b/usecases/mu/mpc/usecase_test.go @@ -0,0 +1,151 @@ +package mpc + +import ( + "testing" + "time" + + "github.com/enbility/eebus-go/api" + "github.com/enbility/eebus-go/mocks" + "github.com/enbility/eebus-go/service" + shipapi "github.com/enbility/ship-go/api" + "github.com/enbility/ship-go/cert" + spineapi "github.com/enbility/spine-go/api" + spinemocks "github.com/enbility/spine-go/mocks" + "github.com/enbility/spine-go/model" + "github.com/enbility/spine-go/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +func TestBasicSuite(t *testing.T) { + suite.Run(t, new(BasicSuite)) +} + +type BasicSuite struct { + suite.Suite + + service api.ServiceInterface + + remoteDevice spineapi.DeviceRemoteInterface + mockRemoteEntity *spinemocks.EntityRemoteInterface + monitoredEntity spineapi.EntityRemoteInterface + loadControlFeature, + deviceDiagnosisFeature, + deviceConfigurationFeature spineapi.FeatureLocalInterface + + eventCalled bool +} + +func (s *BasicSuite) Event(ski string, device spineapi.DeviceRemoteInterface, entity spineapi.EntityRemoteInterface, event api.EventType) { + s.eventCalled = true +} + +func (s *BasicSuite) BeforeTest(suiteName, testName string) { + s.eventCalled = false + cert, _ := cert.CreateCertificate("test", "test", "DE", "test") + configuration, _ := api.NewConfiguration( + "test", "test", "test", "test", + []shipapi.DeviceCategoryType{shipapi.DeviceCategoryTypeEnergyManagementSystem}, + model.DeviceTypeTypeEnergyManagementSystem, + []model.EntityTypeType{model.EntityTypeTypeInverter}, + 9999, cert, time.Second*4) + + serviceHandler := mocks.NewServiceReaderInterface(s.T()) + serviceHandler.EXPECT().ServicePairingDetailUpdate(mock.Anything, mock.Anything).Return().Maybe() + + s.service = service.NewService(configuration, serviceHandler) + _ = s.service.Setup() + + mockRemoteDevice := spinemocks.NewDeviceRemoteInterface(s.T()) + s.mockRemoteEntity = spinemocks.NewEntityRemoteInterface(s.T()) + mockRemoteFeature := spinemocks.NewFeatureRemoteInterface(s.T()) + mockRemoteDevice.EXPECT().FeatureByEntityTypeAndRole(mock.Anything, mock.Anything, mock.Anything).Return(mockRemoteFeature).Maybe() + mockRemoteDevice.EXPECT().Ski().Return(remoteSki).Maybe() + s.mockRemoteEntity.EXPECT().Device().Return(mockRemoteDevice).Maybe() + s.mockRemoteEntity.EXPECT().EntityType().Return(mock.Anything).Maybe() + entityAddress := &model.EntityAddressType{} + s.mockRemoteEntity.EXPECT().Address().Return(entityAddress).Maybe() + mockRemoteFeature.EXPECT().DataCopy(mock.Anything).Return(mock.Anything).Maybe() + mockRemoteFeature.EXPECT().Address().Return(&model.FeatureAddressType{}).Maybe() + mockRemoteFeature.EXPECT().Operations().Return(nil).Maybe() +} + +func (s *BasicSuite) Test_MpcOptionalParameters() { + localEntity := s.service.LocalDevice().EntityForType(model.EntityTypeTypeInverter) + + // required + var monitorPowerConfig = MonitorPowerConfig{ + ConnectedPhases: ConnectedPhasesABC, + ValueSourceTotal: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseA: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseB: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseC: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + } + + // the following 4 parameters are optional and can be nil + var monitorEnergyConfig = MonitorEnergyConfig{ + ValueSourceProduction: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourceConsumption: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + } + var monitorCurrentConfig = MonitorCurrentConfig{ + ValueSourcePhaseA: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseB: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseC: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + } + var monitorVoltageConfig = MonitorVoltageConfig{ + SupportPhaseToPhase: true, + ValueSourcePhaseA: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseB: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseC: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseAToB: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseBToC: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueSourcePhaseCToA: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + } + var monitorFrequencyConfig = MonitorFrequencyConfig{ + ValueSource: util.Ptr(model.MeasurementValueSourceTypeMeasuredValue), + ValueConstraints: util.Ptr(model.MeasurementConstraintsDataType{ + ValueRangeMin: model.NewScaledNumberType(0), + ValueRangeMax: model.NewScaledNumberType(100), + ValueStepSize: model.NewScaledNumberType(1), + }), + } + + numOptionalParams := 4 + + // iterate over all permutations of nil/set + for i := 0; i < (1 << numOptionalParams); i++ { + // Determine which parameters to set + var optEnergyConfig *MonitorEnergyConfig + var optCurrentConfig *MonitorCurrentConfig + var optVoltageConfig *MonitorVoltageConfig + var optFrequencyConfig *MonitorFrequencyConfig + if i&1 != 0 { + optEnergyConfig = &monitorEnergyConfig + } + if i&2 != 0 { + optCurrentConfig = &monitorCurrentConfig + } + if i&4 != 0 { + optVoltageConfig = &monitorVoltageConfig + } + if i&8 != 0 { + optFrequencyConfig = &monitorFrequencyConfig + } + + mpc, err := NewMPC( + localEntity, + s.Event, + &monitorPowerConfig, + optEnergyConfig, + optCurrentConfig, + optVoltageConfig, + optFrequencyConfig, + ) + + assert.Nil(s.T(), err) + + mpc.AddFeatures() + mpc.AddUseCase() + } +}