Skip to content

Commit

Permalink
Only add scenarios when they're supported
Browse files Browse the repository at this point in the history
This commit also adds some fixes to allow leaving unsupported config
parameters to NewMPC as nil and a test for those scenarios.
  • Loading branch information
sthelen-enqs committed Nov 4, 2024
1 parent c70256a commit 6834d88
Show file tree
Hide file tree
Showing 2 changed files with 249 additions and 67 deletions.
165 changes: 98 additions & 67 deletions usecases/mu/mpc/usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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])
}
}
}
}
Expand Down
151 changes: 151 additions & 0 deletions usecases/mu/mpc/usecase_test.go
Original file line number Diff line number Diff line change
@@ -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

Check failure on line 30 in usecases/mu/mpc/usecase_test.go

View workflow job for this annotation

GitHub Actions / Build

field `remoteDevice` is unused (unused)
mockRemoteEntity *spinemocks.EntityRemoteInterface
monitoredEntity spineapi.EntityRemoteInterface

Check failure on line 32 in usecases/mu/mpc/usecase_test.go

View workflow job for this annotation

GitHub Actions / Build

field `monitoredEntity` is unused (unused)
loadControlFeature,

Check failure on line 33 in usecases/mu/mpc/usecase_test.go

View workflow job for this annotation

GitHub Actions / Build

field `loadControlFeature` is unused (unused)
deviceDiagnosisFeature,

Check failure on line 34 in usecases/mu/mpc/usecase_test.go

View workflow job for this annotation

GitHub Actions / Build

field `deviceDiagnosisFeature` is unused (unused)
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()
}
}

0 comments on commit 6834d88

Please sign in to comment.