From b90c2695c38265cc58659cdedc33f5ad665e4aef Mon Sep 17 00:00:00 2001 From: Simon Thelen Date: Thu, 13 Feb 2025 14:23:12 +0100 Subject: [PATCH 1/4] Add failing test to internal/measurement --- usecases/internal/measurement_test.go | 79 +++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/usecases/internal/measurement_test.go b/usecases/internal/measurement_test.go index 05410387..28ff1ff8 100644 --- a/usecases/internal/measurement_test.go +++ b/usecases/internal/measurement_test.go @@ -196,3 +196,82 @@ func (s *InternalSuite) Test_MeasurementPhaseSpecificDataForFilter() { assert.NotNil(s.T(), err) assert.Nil(s.T(), data) } + +func (s *InternalSuite) Test_MeasurementSinglePhaseSpecificDataForFilter() { + measurementType := model.MeasurementTypeTypePower + commodityType := model.CommodityTypeTypeElectricity + scopeType := model.ScopeTypeTypeACPower + energyDirection := model.EnergyDirectionTypeConsume + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: &measurementType, + CommodityType: &commodityType, + ScopeType: &scopeType, + } + + // set up ElectricalConnection + elDescData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + PositiveEnergyDirection: util.Ptr(model.EnergyDirectionTypeConsume), + }, + }, + } + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeB), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + + _, fErr := rElFeature.UpdateData(true, model.FunctionTypeElectricalConnectionDescriptionListData, elDescData, nil, nil) + assert.Nil(s.T(), fErr) + + _, fErr = rElFeature.UpdateData(true, model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: util.Ptr(model.MeasurementTypeTypePower), + CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), + ScopeType: util.Ptr(model.ScopeTypeTypeACPower), + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + _, fErr = rFeature.UpdateData(true, model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + _, fErr = rFeature.UpdateData(true, model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err := MeasurementPhaseSpecificDataForFilter( + s.localEntity, + s.monitoredEntity, + filter, + energyDirection, + ucapi.PhaseNameMapping, + ) + assert.Nil(s.T(), err) + assert.Equal(s.T(), []float64{0, 10, 0}, data) + +} From 9f94f4f2a559672443ee35c0faef265e4bbfbfc4 Mon Sep 17 00:00:00 2001 From: Simon Thelen Date: Mon, 16 Dec 2024 11:22:33 +0100 Subject: [PATCH 2/4] Ensure we always report per-phase measurements for the correct phase --- usecases/internal/measurement.go | 25 +++++++++++++++++++++---- usecases/internal/measurement_test.go | 2 +- usecases/ma/mgcp/public_test.go | 4 ++-- usecases/ma/mpc/public_test.go | 6 +++--- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/usecases/internal/measurement.go b/usecases/internal/measurement.go index 09c72231..05c03726 100644 --- a/usecases/internal/measurement.go +++ b/usecases/internal/measurement.go @@ -29,20 +29,31 @@ func MeasurementPhaseSpecificDataForFilter( } var result []float64 + if validPhaseNameTypes != nil { + // pre-allocate result array for each possible phase so we can add phases to it in arbitrary order + result = make([]float64, len(validPhaseNameTypes)) + } for _, item := range data { if item.Value == nil || item.MeasurementId == nil { continue } + phaseIndex := -1 if validPhaseNameTypes != nil { filter := model.ElectricalConnectionParameterDescriptionDataType{ MeasurementId: item.MeasurementId, } param, err := electricalConnection.GetParameterDescriptionsForFilter(filter) - if err != nil || len(param) == 0 || - param[0].AcMeasuredPhases == nil || - !slices.Contains(validPhaseNameTypes, *param[0].AcMeasuredPhases) { + if err != nil || len(param) == 0 || param[0].AcMeasuredPhases == nil { + // error getting parameter description + continue + } + + // calculate the offset into result for the measured phase + phaseIndex = slices.Index(validPhaseNameTypes, *param[0].AcMeasuredPhases) + if phaseIndex == -1 { + // ignore phase measurements not specified in validPhaseNameTypes continue } } @@ -70,7 +81,13 @@ func MeasurementPhaseSpecificDataForFilter( value := item.Value.GetValue() - result = append(result, value) + if validPhaseNameTypes == nil { + // measurement is not for a specific phase + result = append(result, value) + } else { + // measurement is for a specific phase, store the value at the corresponding phaseIndex + result[phaseIndex] = value + } } return result, nil diff --git a/usecases/internal/measurement_test.go b/usecases/internal/measurement_test.go index 28ff1ff8..a98f9a01 100644 --- a/usecases/internal/measurement_test.go +++ b/usecases/internal/measurement_test.go @@ -114,7 +114,7 @@ func (s *InternalSuite) Test_MeasurementPhaseSpecificDataForFilter() { ucapi.PhaseNameMapping, ) assert.Nil(s.T(), err) - assert.Equal(s.T(), 0, len(data)) + assert.Equal(s.T(), []float64{0, 0, 0}, data) elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ diff --git a/usecases/ma/mgcp/public_test.go b/usecases/ma/mgcp/public_test.go index 436af816..9f330586 100644 --- a/usecases/ma/mgcp/public_test.go +++ b/usecases/ma/mgcp/public_test.go @@ -316,7 +316,7 @@ func (s *GcpMGCPSuite) Test_CurrentPerPhase() { data, err = s.sut.CurrentPerPhase(s.smgwEntity) assert.Nil(s.T(), err) - assert.Equal(s.T(), 0, len(data)) + assert.Equal(s.T(), []float64{0, 0, 0}, data) elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ @@ -421,7 +421,7 @@ func (s *GcpMGCPSuite) Test_VoltagePerPhase() { data, err = s.sut.VoltagePerPhase(s.smgwEntity) assert.Nil(s.T(), err) - assert.Equal(s.T(), 0, len(data)) + assert.Equal(s.T(), []float64{0, 0, 0}, data) elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ diff --git a/usecases/ma/mpc/public_test.go b/usecases/ma/mpc/public_test.go index 45622fe2..a7dc5beb 100644 --- a/usecases/ma/mpc/public_test.go +++ b/usecases/ma/mpc/public_test.go @@ -146,7 +146,7 @@ func (s *MaMPCSuite) Test_PowerPerPhase() { data, err = s.sut.PowerPerPhase(s.monitoredEntity) assert.Nil(s.T(), err) - assert.Equal(s.T(), 0, len(data)) + assert.Equal(s.T(), []float64{0, 0, 0}, data) elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ @@ -405,7 +405,7 @@ func (s *MaMPCSuite) Test_CurrentPerPhase() { data, err = s.sut.CurrentPerPhase(s.monitoredEntity) assert.Nil(s.T(), err) - assert.Equal(s.T(), 0, len(data)) + assert.Equal(s.T(), []float64{0, 0, 0}, data) elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ @@ -510,7 +510,7 @@ func (s *MaMPCSuite) Test_VoltagePerPhase() { data, err = s.sut.VoltagePerPhase(s.monitoredEntity) assert.Nil(s.T(), err) - assert.Equal(s.T(), 0, len(data)) + assert.Equal(s.T(), []float64{0, 0, 0}, data) elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ From 6eb115417754578b5a7c0978156649dd697a0bc1 Mon Sep 17 00:00:00 2001 From: Simon Thelen Date: Wed, 5 Mar 2025 16:21:43 +0100 Subject: [PATCH 3/4] Enable differentiation between unset phase values and 0 phase values - changes return type of MeasurementPhaseSpecificDataForFilter to a map from PhaseName to value instead of a list --- usecases/api/ma_mgcp.go | 5 +- usecases/api/ma_mpc.go | 7 +- usecases/internal/measurement.go | 46 +++++-------- usecases/internal/measurement_test.go | 95 ++++++++++++++++++++++++++- usecases/ma/mgcp/public.go | 12 +++- usecases/ma/mgcp/public_test.go | 9 +-- usecases/ma/mpc/public.go | 14 ++-- usecases/ma/mpc/public_test.go | 13 ++-- 8 files changed, 147 insertions(+), 54 deletions(-) diff --git a/usecases/api/ma_mgcp.go b/usecases/api/ma_mgcp.go index 4387db47..3b491c50 100644 --- a/usecases/api/ma_mgcp.go +++ b/usecases/api/ma_mgcp.go @@ -3,6 +3,7 @@ package api import ( "github.com/enbility/eebus-go/api" spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" ) // Actor: Monitoring Appliance @@ -66,7 +67,7 @@ type MaMGCPInterface interface { // return values: // - positive values are used for consumption // - negative values are used for production - CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + CurrentPerPhase(entity spineapi.EntityRemoteInterface) (map[model.ElectricalConnectionPhaseNameType]float64, error) // Scenario 6 @@ -74,7 +75,7 @@ type MaMGCPInterface interface { // // parameters: // - entity: the entity of the device (e.g. SMGW) - VoltagePerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + VoltagePerPhase(entity spineapi.EntityRemoteInterface) (map[model.ElectricalConnectionPhaseNameType]float64, error) // Scenario 7 diff --git a/usecases/api/ma_mpc.go b/usecases/api/ma_mpc.go index 57402cb2..be4b6350 100644 --- a/usecases/api/ma_mpc.go +++ b/usecases/api/ma_mpc.go @@ -3,6 +3,7 @@ package api import ( "github.com/enbility/eebus-go/api" spineapi "github.com/enbility/spine-go/api" + "github.com/enbility/spine-go/model" ) // Actor: Monitoring Appliance @@ -30,7 +31,7 @@ type MaMPCInterface interface { // possible errors: // - ErrDataNotAvailable if no such limit is (yet) available // - and others - PowerPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + PowerPerPhase(entity spineapi.EntityRemoteInterface) (map[model.ElectricalConnectionPhaseNameType]float64, error) // Scenario 2 @@ -61,7 +62,7 @@ type MaMPCInterface interface { // return values // - positive values are used for consumption // - negative values are used for production - CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + CurrentPerPhase(entity spineapi.EntityRemoteInterface) (map[model.ElectricalConnectionPhaseNameType]float64, error) // Scenario 4 @@ -69,7 +70,7 @@ type MaMPCInterface interface { // // parameters: // - entity: the entity of the device (e.g. EVSE) - VoltagePerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) + VoltagePerPhase(entity spineapi.EntityRemoteInterface) (map[model.ElectricalConnectionPhaseNameType]float64, error) // Scenario 5 diff --git a/usecases/internal/measurement.go b/usecases/internal/measurement.go index 05c03726..1a1197ee 100644 --- a/usecases/internal/measurement.go +++ b/usecases/internal/measurement.go @@ -16,7 +16,7 @@ func MeasurementPhaseSpecificDataForFilter( measurementFilter model.MeasurementDescriptionDataType, energyDirection model.EnergyDirectionType, validPhaseNameTypes []model.ElectricalConnectionPhaseNameType, -) ([]float64, error) { +) (map[model.ElectricalConnectionPhaseNameType]float64, error) { measurement, err := client.NewMeasurement(localEntity, remoteEntity) electricalConnection, err1 := client.NewElectricalConnection(localEntity, remoteEntity) if err != nil || err1 != nil { @@ -28,34 +28,28 @@ func MeasurementPhaseSpecificDataForFilter( return nil, api.ErrDataNotAvailable } - var result []float64 - if validPhaseNameTypes != nil { - // pre-allocate result array for each possible phase so we can add phases to it in arbitrary order - result = make([]float64, len(validPhaseNameTypes)) - } + result := make(map[model.ElectricalConnectionPhaseNameType]float64, len(validPhaseNameTypes)) for _, item := range data { if item.Value == nil || item.MeasurementId == nil { continue } - phaseIndex := -1 - if validPhaseNameTypes != nil { - filter := model.ElectricalConnectionParameterDescriptionDataType{ - MeasurementId: item.MeasurementId, - } - param, err := electricalConnection.GetParameterDescriptionsForFilter(filter) - if err != nil || len(param) == 0 || param[0].AcMeasuredPhases == nil { - // error getting parameter description - continue - } + filter := model.ElectricalConnectionParameterDescriptionDataType{ + MeasurementId: item.MeasurementId, + } + param, err := electricalConnection.GetParameterDescriptionsForFilter(filter) + if err != nil || len(param) == 0 || param[0].AcMeasuredPhases == nil { + // error getting parameter description + continue + } - // calculate the offset into result for the measured phase - phaseIndex = slices.Index(validPhaseNameTypes, *param[0].AcMeasuredPhases) - if phaseIndex == -1 { - // ignore phase measurements not specified in validPhaseNameTypes - continue - } + // calculate the offset into result for the measured phase + phaseName := *param[0].AcMeasuredPhases + if validPhaseNameTypes != nil && + !slices.Contains(validPhaseNameTypes, phaseName) { + // ignore phase measurements not specified in validPhaseNameTypes + continue } if energyDirection != "" { @@ -81,13 +75,7 @@ func MeasurementPhaseSpecificDataForFilter( value := item.Value.GetValue() - if validPhaseNameTypes == nil { - // measurement is not for a specific phase - result = append(result, value) - } else { - // measurement is for a specific phase, store the value at the corresponding phaseIndex - result[phaseIndex] = value - } + result[phaseName] = value } return result, nil diff --git a/usecases/internal/measurement_test.go b/usecases/internal/measurement_test.go index a98f9a01..4bcd86eb 100644 --- a/usecases/internal/measurement_test.go +++ b/usecases/internal/measurement_test.go @@ -114,7 +114,7 @@ func (s *InternalSuite) Test_MeasurementPhaseSpecificDataForFilter() { ucapi.PhaseNameMapping, ) assert.Nil(s.T(), err) - assert.Equal(s.T(), []float64{0, 0, 0}, data) + assert.Equal(s.T(), map[model.ElectricalConnectionPhaseNameType]float64{}, data) elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ @@ -160,7 +160,7 @@ func (s *InternalSuite) Test_MeasurementPhaseSpecificDataForFilter() { ucapi.PhaseNameMapping, ) assert.Nil(s.T(), err) - assert.Equal(s.T(), []float64{10, 10, 10}, data) + assert.Equal(s.T(), map[model.ElectricalConnectionPhaseNameType]float64{"a": 10, "b": 10, "c": 10}, data) measData = &model.MeasurementListDataType{ MeasurementData: []model.MeasurementDataType{ @@ -272,6 +272,95 @@ func (s *InternalSuite) Test_MeasurementSinglePhaseSpecificDataForFilter() { ucapi.PhaseNameMapping, ) assert.Nil(s.T(), err) - assert.Equal(s.T(), []float64{0, 10, 0}, data) + assert.Equal(s.T(), map[model.ElectricalConnectionPhaseNameType]float64{"b": 10}, data) + + data, err = MeasurementPhaseSpecificDataForFilter( + s.localEntity, + s.monitoredEntity, + filter, + energyDirection, + nil, + ) + assert.Nil(s.T(), err) + assert.Equal(s.T(), map[model.ElectricalConnectionPhaseNameType]float64{"b": 10}, data) + +} + +func (s *InternalSuite) Test_MeasurementTotalPhaseSpecificDataForFilter() { + measurementType := model.MeasurementTypeTypePower + commodityType := model.CommodityTypeTypeElectricity + scopeType := model.ScopeTypeTypeACPowerTotal + energyDirection := model.EnergyDirectionTypeConsume + + filter := model.MeasurementDescriptionDataType{ + MeasurementType: &measurementType, + CommodityType: &commodityType, + ScopeType: &scopeType, + } + + // set up ElectricalConnection + elDescData := &model.ElectricalConnectionDescriptionListDataType{ + ElectricalConnectionDescriptionData: []model.ElectricalConnectionDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + PositiveEnergyDirection: util.Ptr(model.EnergyDirectionTypeConsume), + }, + }, + } + + elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ + ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ + { + ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeAbc), + }, + }, + } + + rElFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeElectricalConnection, model.RoleTypeServer) + + _, fErr := rElFeature.UpdateData(true, model.FunctionTypeElectricalConnectionDescriptionListData, elDescData, nil, nil) + assert.Nil(s.T(), fErr) + + _, fErr = rElFeature.UpdateData(true, model.FunctionTypeElectricalConnectionParameterDescriptionListData, elParamData, nil, nil) + assert.Nil(s.T(), fErr) + + descData := &model.MeasurementDescriptionListDataType{ + MeasurementDescriptionData: []model.MeasurementDescriptionDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + MeasurementType: &measurementType, + CommodityType: &commodityType, + ScopeType: &scopeType, + }, + }, + } + + rFeature := s.remoteDevice.FeatureByEntityTypeAndRole(s.monitoredEntity, model.FeatureTypeTypeMeasurement, model.RoleTypeServer) + _, fErr = rFeature.UpdateData(true, model.FunctionTypeMeasurementDescriptionListData, descData, nil, nil) + assert.Nil(s.T(), fErr) + + measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + }, + }, + } + + _, fErr = rFeature.UpdateData(true, model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err := MeasurementPhaseSpecificDataForFilter( + s.localEntity, + s.monitoredEntity, + filter, + energyDirection, + nil, + ) + assert.Nil(s.T(), err) + assert.Equal(s.T(), map[model.ElectricalConnectionPhaseNameType]float64{"abc": 10}, data) } diff --git a/usecases/ma/mgcp/public.go b/usecases/ma/mgcp/public.go index 357ad52e..87456b9d 100644 --- a/usecases/ma/mgcp/public.go +++ b/usecases/ma/mgcp/public.go @@ -78,7 +78,13 @@ func (e *MGCP) Power(entity spineapi.EntityRemoteInterface) (float64, error) { return 0, api.ErrDataNotAvailable } - return data[0], nil + for _, k := range data { + // If the Monitored Unit is connected to less than three phases, one of the other combinations like "a" or "ab" are allowed instead of "abc". + // The values "a", "b", and "c" are permitted if and only if only one + return k, nil + } + // unreachable + return 0, api.ErrDataNotAvailable } // Scenario 3 @@ -170,7 +176,7 @@ func (e *MGCP) EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, e // - ErrDataNotAvailable if no such value is (yet) available // - ErrDataInvalid if the currently available data is invalid and should be ignored // - and others -func (e *MGCP) CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { +func (e *MGCP) CurrentPerPhase(entity spineapi.EntityRemoteInterface) (map[model.ElectricalConnectionPhaseNameType]float64, error) { if !e.IsCompatibleEntityType(entity) { return nil, api.ErrNoCompatibleEntity } @@ -191,7 +197,7 @@ func (e *MGCP) CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64 // - ErrDataNotAvailable if no such value is (yet) available // - ErrDataInvalid if the currently available data is invalid and should be ignored // - and others -func (e *MGCP) VoltagePerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { +func (e *MGCP) VoltagePerPhase(entity spineapi.EntityRemoteInterface) (map[model.ElectricalConnectionPhaseNameType]float64, error) { if !e.IsCompatibleEntityType(entity) { return nil, api.ErrNoCompatibleEntity } diff --git a/usecases/ma/mgcp/public_test.go b/usecases/ma/mgcp/public_test.go index 9f330586..51a03c49 100644 --- a/usecases/ma/mgcp/public_test.go +++ b/usecases/ma/mgcp/public_test.go @@ -118,6 +118,7 @@ func (s *GcpMGCPSuite) Test_Power() { { ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), MeasurementId: util.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeA), }, }, } @@ -316,7 +317,7 @@ func (s *GcpMGCPSuite) Test_CurrentPerPhase() { data, err = s.sut.CurrentPerPhase(s.smgwEntity) assert.Nil(s.T(), err) - assert.Equal(s.T(), []float64{0, 0, 0}, data) + assert.Equal(s.T(), map[model.ElectricalConnectionPhaseNameType]float64{}, data) elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ @@ -356,7 +357,7 @@ func (s *GcpMGCPSuite) Test_CurrentPerPhase() { data, err = s.sut.CurrentPerPhase(s.smgwEntity) assert.Nil(s.T(), err) - assert.Equal(s.T(), []float64{10, 10, 10}, data) + assert.Equal(s.T(), map[model.ElectricalConnectionPhaseNameType]float64{"a": 10, "b": 10, "c": 10}, data) } func (s *GcpMGCPSuite) Test_VoltagePerPhase() { @@ -421,7 +422,7 @@ func (s *GcpMGCPSuite) Test_VoltagePerPhase() { data, err = s.sut.VoltagePerPhase(s.smgwEntity) assert.Nil(s.T(), err) - assert.Equal(s.T(), []float64{0, 0, 0}, data) + assert.Equal(s.T(), map[model.ElectricalConnectionPhaseNameType]float64{}, data) elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ @@ -449,7 +450,7 @@ func (s *GcpMGCPSuite) Test_VoltagePerPhase() { data, err = s.sut.VoltagePerPhase(s.smgwEntity) assert.Nil(s.T(), err) - assert.Equal(s.T(), []float64{230, 230, 230}, data) + assert.Equal(s.T(), map[model.ElectricalConnectionPhaseNameType]float64{"a": 230, "b": 230, "c": 230}, data) } func (s *GcpMGCPSuite) Test_Frequency() { diff --git a/usecases/ma/mpc/public.go b/usecases/ma/mpc/public.go index 6df7fbfc..509a364a 100644 --- a/usecases/ma/mpc/public.go +++ b/usecases/ma/mpc/public.go @@ -36,7 +36,13 @@ func (e *MPC) Power(entity spineapi.EntityRemoteInterface) (float64, error) { return 0, api.ErrDataNotAvailable } - return values[0], nil + for _, k := range values { + // If the Monitored Unit is connected to less than three phases, one of the other combinations like "a" or "ab" are allowed instead of "abc". + // The values "a", "b", and "c" are permitted if and only if only one + return k, nil + } + // unreachable + return 0, api.ErrDataNotAvailable } // return the momentary active phase specific power consumption or production per phase @@ -45,7 +51,7 @@ func (e *MPC) Power(entity spineapi.EntityRemoteInterface) (float64, error) { // - ErrDataNotAvailable if no such value is (yet) available // - ErrDataInvalid if the currently available data is invalid and should be ignored // - and others -func (e *MPC) PowerPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { +func (e *MPC) PowerPerPhase(entity spineapi.EntityRemoteInterface) (map[model.ElectricalConnectionPhaseNameType]float64, error) { if !e.IsCompatibleEntityType(entity) { return nil, api.ErrNoCompatibleEntity } @@ -157,7 +163,7 @@ func (e *MPC) EnergyProduced(entity spineapi.EntityRemoteInterface) (float64, er // - ErrDataNotAvailable if no such value is (yet) available // - ErrDataInvalid if the currently available data is invalid and should be ignored // - and others -func (e *MPC) CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { +func (e *MPC) CurrentPerPhase(entity spineapi.EntityRemoteInterface) (map[model.ElectricalConnectionPhaseNameType]float64, error) { if !e.IsCompatibleEntityType(entity) { return nil, api.ErrNoCompatibleEntity } @@ -178,7 +184,7 @@ func (e *MPC) CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, // - ErrDataNotAvailable if no such value is (yet) available // - ErrDataInvalid if the currently available data is invalid and should be ignored // - and others -func (e *MPC) VoltagePerPhase(entity spineapi.EntityRemoteInterface) ([]float64, error) { +func (e *MPC) VoltagePerPhase(entity spineapi.EntityRemoteInterface) (map[model.ElectricalConnectionPhaseNameType]float64, error) { if !e.IsCompatibleEntityType(entity) { return nil, api.ErrNoCompatibleEntity } diff --git a/usecases/ma/mpc/public_test.go b/usecases/ma/mpc/public_test.go index a7dc5beb..ad125958 100644 --- a/usecases/ma/mpc/public_test.go +++ b/usecases/ma/mpc/public_test.go @@ -72,6 +72,7 @@ func (s *MaMPCSuite) Test_Power() { { ElectricalConnectionId: util.Ptr(model.ElectricalConnectionIdType(0)), MeasurementId: util.Ptr(model.MeasurementIdType(0)), + AcMeasuredPhases: util.Ptr(model.ElectricalConnectionPhaseNameTypeAbc), }, }, } @@ -146,7 +147,7 @@ func (s *MaMPCSuite) Test_PowerPerPhase() { data, err = s.sut.PowerPerPhase(s.monitoredEntity) assert.Nil(s.T(), err) - assert.Equal(s.T(), []float64{0, 0, 0}, data) + assert.Equal(s.T(), map[model.ElectricalConnectionPhaseNameType]float64{}, data) elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ @@ -186,7 +187,7 @@ func (s *MaMPCSuite) Test_PowerPerPhase() { data, err = s.sut.PowerPerPhase(s.monitoredEntity) assert.Nil(s.T(), err) - assert.Equal(s.T(), []float64{10, 10, 10}, data) + assert.Equal(s.T(), map[model.ElectricalConnectionPhaseNameType]float64{"a": 10, "b": 10, "c": 10}, data) } func (s *MaMPCSuite) Test_EnergyConsumed() { @@ -405,7 +406,7 @@ func (s *MaMPCSuite) Test_CurrentPerPhase() { data, err = s.sut.CurrentPerPhase(s.monitoredEntity) assert.Nil(s.T(), err) - assert.Equal(s.T(), []float64{0, 0, 0}, data) + assert.Equal(s.T(), map[model.ElectricalConnectionPhaseNameType]float64{}, data) elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ @@ -445,7 +446,7 @@ func (s *MaMPCSuite) Test_CurrentPerPhase() { data, err = s.sut.CurrentPerPhase(s.monitoredEntity) assert.Nil(s.T(), err) - assert.Equal(s.T(), []float64{10, 10, 10}, data) + assert.Equal(s.T(), map[model.ElectricalConnectionPhaseNameType]float64{"a": 10, "b": 10, "c": 10}, data) } func (s *MaMPCSuite) Test_VoltagePerPhase() { @@ -510,7 +511,7 @@ func (s *MaMPCSuite) Test_VoltagePerPhase() { data, err = s.sut.VoltagePerPhase(s.monitoredEntity) assert.Nil(s.T(), err) - assert.Equal(s.T(), []float64{0, 0, 0}, data) + assert.Equal(s.T(), map[model.ElectricalConnectionPhaseNameType]float64{}, data) elParamData := &model.ElectricalConnectionParameterDescriptionListDataType{ ElectricalConnectionParameterDescriptionData: []model.ElectricalConnectionParameterDescriptionDataType{ @@ -538,7 +539,7 @@ func (s *MaMPCSuite) Test_VoltagePerPhase() { data, err = s.sut.VoltagePerPhase(s.monitoredEntity) assert.Nil(s.T(), err) - assert.Equal(s.T(), []float64{230, 230, 230}, data) + assert.Equal(s.T(), map[model.ElectricalConnectionPhaseNameType]float64{"a": 230, "b": 230, "c": 230}, data) } func (s *MaMPCSuite) Test_Frequency() { From 33f84014b5d1b28acea0529e3562a95b49dfaf6d Mon Sep 17 00:00:00 2001 From: Simon Thelen Date: Mon, 26 May 2025 16:39:19 +0200 Subject: [PATCH 4/4] Fix Power() for ABC when AcMeasuredPhases is missing --- usecases/internal/measurement.go | 15 ++++++++++++--- usecases/ma/mpc/public.go | 3 ++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/usecases/internal/measurement.go b/usecases/internal/measurement.go index 1a1197ee..81301f6e 100644 --- a/usecases/internal/measurement.go +++ b/usecases/internal/measurement.go @@ -39,13 +39,22 @@ func MeasurementPhaseSpecificDataForFilter( MeasurementId: item.MeasurementId, } param, err := electricalConnection.GetParameterDescriptionsForFilter(filter) - if err != nil || len(param) == 0 || param[0].AcMeasuredPhases == nil { + if err != nil || len(param) == 0 { + // error getting parameter description + continue + } + + var phaseName model.ElectricalConnectionPhaseNameType + if param[0].AcMeasuredPhases != nil { + phaseName = *param[0].AcMeasuredPhases + } else if validPhaseNameTypes == nil { + // if we're not filtering by valid phase names, allow acMeasuredPhases to be unset + phaseName = model.ElectricalConnectionPhaseNameTypeNone + } else { // error getting parameter description continue } - // calculate the offset into result for the measured phase - phaseName := *param[0].AcMeasuredPhases if validPhaseNameTypes != nil && !slices.Contains(validPhaseNameTypes, phaseName) { // ignore phase measurements not specified in validPhaseNameTypes diff --git a/usecases/ma/mpc/public.go b/usecases/ma/mpc/public.go index 509a364a..856e9e6b 100644 --- a/usecases/ma/mpc/public.go +++ b/usecases/ma/mpc/public.go @@ -28,6 +28,7 @@ func (e *MPC) Power(entity spineapi.EntityRemoteInterface) (float64, error) { CommodityType: util.Ptr(model.CommodityTypeTypeElectricity), ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), } + // acMeasuredPhases is optional for total active power, therefore we pass nil for validPhaseNameTypes values, err := internal.MeasurementPhaseSpecificDataForFilter(e.LocalEntity, entity, filter, model.EnergyDirectionTypeConsume, nil) if err != nil { return 0, err @@ -38,7 +39,7 @@ func (e *MPC) Power(entity spineapi.EntityRemoteInterface) (float64, error) { for _, k := range values { // If the Monitored Unit is connected to less than three phases, one of the other combinations like "a" or "ab" are allowed instead of "abc". - // The values "a", "b", and "c" are permitted if and only if only one + // The values "a", "b", and "c" are permitted if and only if only one phase is connected return k, nil } // unreachable