From bf4dad98550c763d49c21c35456f39fef069a800 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Wed, 20 Nov 2024 18:43:24 +0100 Subject: [PATCH 1/2] Properly handle invalid measurement data If a measurement is provided with an ValueState != Normal, then the value should be ignored by the application. To handle that, the public API will then report an ErrDataInvalid error, so the application knows that the currently no valid data is available. --- api/errors.go | 3 +++ usecases/internal/measurement.go | 6 +++++ usecases/internal/measurement_test.go | 34 +++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/api/errors.go b/api/errors.go index 96eeffb7..c20ee464 100644 --- a/api/errors.go +++ b/api/errors.go @@ -9,6 +9,9 @@ var ErrMetadataNotAvailable = errors.New("meta data not available") // ErrDataNotAvailable indicates that no data set is yet available var ErrDataNotAvailable = errors.New("data not available") +// ErrDataInvalid indicates that the currently available data is not valid and should be ignored +var ErrDataInvalid = errors.New("data not valid") + // ErrDataForMetadataKeyNotFound indicates that no data item is found for the given key var ErrDataForMetadataKeyNotFound = errors.New("data for key not found") diff --git a/usecases/internal/measurement.go b/usecases/internal/measurement.go index 21b73484..09c72231 100644 --- a/usecases/internal/measurement.go +++ b/usecases/internal/measurement.go @@ -62,6 +62,12 @@ func MeasurementPhaseSpecificDataForFilter( } } + // if the value state is set and not normal, the value is not valid and should be ignored + // therefore we return an error + if item.ValueState != nil && *item.ValueState != model.MeasurementValueStateTypeNormal { + return nil, api.ErrDataInvalid + } + value := item.Value.GetValue() result = append(result, value) diff --git a/usecases/internal/measurement_test.go b/usecases/internal/measurement_test.go index d33190d2..05410387 100644 --- a/usecases/internal/measurement_test.go +++ b/usecases/internal/measurement_test.go @@ -161,4 +161,38 @@ func (s *InternalSuite) Test_MeasurementPhaseSpecificDataForFilter() { ) assert.Nil(s.T(), err) assert.Equal(s.T(), []float64{10, 10, 10}, data) + + measData = &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(10)), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + ValueState: util.Ptr(model.MeasurementValueStateTypeError), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(1)), + Value: model.NewScaledNumberType(10), + }, + { + MeasurementId: util.Ptr(model.MeasurementIdType(2)), + 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.NotNil(s.T(), err) + assert.Nil(s.T(), data) } From fed478ac01279d614bb65f5a9e1dbef27fab2076 Mon Sep 17 00:00:00 2001 From: Andreas Linde Date: Wed, 20 Nov 2024 19:50:08 +0100 Subject: [PATCH 2/2] Add missing MPC and MGPC cases --- usecases/ma/mgcp/public.go | 65 +++++++++++++++++++--- usecases/ma/mgcp/public_test.go | 51 ++++++++++++++++++ usecases/ma/mpc/public.go | 50 ++++++++++++++++- usecases/ma/mpc/public_test.go | 96 +++++++++++++++++++++++++++++++++ 4 files changed, 253 insertions(+), 9 deletions(-) diff --git a/usecases/ma/mgcp/public.go b/usecases/ma/mgcp/public.go index f966e3f3..357ad52e 100644 --- a/usecases/ma/mgcp/public.go +++ b/usecases/ma/mgcp/public.go @@ -15,18 +15,14 @@ import ( // return the current power limitation factor // // possible errors: -// - ErrDataNotAvailable if no such limit is (yet) available +// - 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) PowerLimitationFactor(entity spineapi.EntityRemoteInterface) (float64, error) { if !e.IsCompatibleEntityType(entity) { return 0, api.ErrNoCompatibleEntity } - measurement, err := client.NewMeasurement(e.LocalEntity, entity) - if err != nil || measurement == nil { - return 0, err - } - keyname := model.DeviceConfigurationKeyNameTypePvCurtailmentLimitFactor deviceConfiguration, err := client.NewDeviceConfiguration(e.LocalEntity, entity) @@ -58,6 +54,11 @@ func (e *MGCP) PowerLimitationFactor(entity spineapi.EntityRemoteInterface) (flo // // - positive values are used for consumption // - negative values are used for production +// +// possible errors: +// - 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) Power(entity spineapi.EntityRemoteInterface) (float64, error) { if !e.IsCompatibleEntityType(entity) { return 0, api.ErrNoCompatibleEntity @@ -69,7 +70,11 @@ func (e *MGCP) Power(entity spineapi.EntityRemoteInterface) (float64, error) { ScopeType: util.Ptr(model.ScopeTypeTypeACPowerTotal), } data, err := internal.MeasurementPhaseSpecificDataForFilter(e.LocalEntity, entity, filter, model.EnergyDirectionTypeConsume, nil) - if err != nil || len(data) != 1 { + if err != nil { + return 0, err + } + + if len(data) != 1 { return 0, api.ErrDataNotAvailable } @@ -81,6 +86,11 @@ func (e *MGCP) Power(entity spineapi.EntityRemoteInterface) (float64, error) { // return the total feed in energy at the grid connection point // // - negative values are used for production +// +// possible errors: +// - 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) EnergyFeedIn(entity spineapi.EntityRemoteInterface) (float64, error) { if !e.IsCompatibleEntityType(entity) { return 0, api.ErrNoCompatibleEntity @@ -100,6 +110,13 @@ func (e *MGCP) EnergyFeedIn(entity spineapi.EntityRemoteInterface) (float64, err if err != nil || len(result) == 0 || result[0].Value == nil { return 0, api.ErrDataNotAvailable } + + // if the value state is set and not normal, the value is not valid and should be ignored + // therefore we return an error + if result[0].ValueState != nil && *result[0].ValueState != model.MeasurementValueStateTypeNormal { + return 0, api.ErrDataInvalid + } + return result[0].Value.GetValue(), nil } @@ -108,6 +125,11 @@ func (e *MGCP) EnergyFeedIn(entity spineapi.EntityRemoteInterface) (float64, err // return the total consumption energy at the grid connection point // // - positive values are used for consumption +// +// possible errors: +// - 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) EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, error) { if !e.IsCompatibleEntityType(entity) { return 0, api.ErrNoCompatibleEntity @@ -127,6 +149,13 @@ func (e *MGCP) EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, e if err != nil || len(result) == 0 || result[0].Value == nil { return 0, api.ErrDataNotAvailable } + + // if the value state is set and not normal, the value is not valid and should be ignored + // therefore we return an error + if result[0].ValueState != nil && *result[0].ValueState != model.MeasurementValueStateTypeNormal { + return 0, api.ErrDataInvalid + } + return result[0].Value.GetValue(), nil } @@ -136,6 +165,11 @@ func (e *MGCP) EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, e // // - positive values are used for consumption // - negative values are used for production +// +// possible errors: +// - 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) { if !e.IsCompatibleEntityType(entity) { return nil, api.ErrNoCompatibleEntity @@ -152,6 +186,11 @@ func (e *MGCP) CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64 // Scenario 6 // return the voltage phase details at the grid connection point +// +// possible errors: +// - 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) { if !e.IsCompatibleEntityType(entity) { return nil, api.ErrNoCompatibleEntity @@ -168,6 +207,11 @@ func (e *MGCP) VoltagePerPhase(entity spineapi.EntityRemoteInterface) ([]float64 // Scenario 7 // return frequency at the grid connection point +// +// possible errors: +// - 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) Frequency(entity spineapi.EntityRemoteInterface) (float64, error) { if !e.IsCompatibleEntityType(entity) { return 0, api.ErrNoCompatibleEntity @@ -187,5 +231,12 @@ func (e *MGCP) Frequency(entity spineapi.EntityRemoteInterface) (float64, error) if err != nil || len(result) == 0 || result[0].Value == nil { return 0, api.ErrDataNotAvailable } + + // if the value state is set and not normal, the value is not valid and should be ignored + // therefore we return an error + if result[0].ValueState != nil && *result[0].ValueState != model.MeasurementValueStateTypeNormal { + return 0, api.ErrDataInvalid + } + return result[0].Value.GetValue(), nil } diff --git a/usecases/ma/mgcp/public_test.go b/usecases/ma/mgcp/public_test.go index 4ded2704..436af816 100644 --- a/usecases/ma/mgcp/public_test.go +++ b/usecases/ma/mgcp/public_test.go @@ -173,6 +173,23 @@ func (s *GcpMGCPSuite) Test_EnergyFeedIn() { data, err = s.sut.EnergyFeedIn(s.smgwEntity) assert.Nil(s.T(), err) assert.Equal(s.T(), 10.0, data) + + measData = &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + ValueState: util.Ptr(model.MeasurementValueStateTypeError), + }, + }, + } + + _, fErr = rFeature.UpdateData(true, model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyFeedIn(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) } func (s *GcpMGCPSuite) Test_EnergyConsumed() { @@ -218,6 +235,23 @@ func (s *GcpMGCPSuite) Test_EnergyConsumed() { data, err = s.sut.EnergyConsumed(s.smgwEntity) assert.Nil(s.T(), err) assert.Equal(s.T(), 10.0, data) + + measData = &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + ValueState: util.Ptr(model.MeasurementValueStateTypeError), + }, + }, + } + + _, fErr = rFeature.UpdateData(true, model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyConsumed(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) } func (s *GcpMGCPSuite) Test_CurrentPerPhase() { @@ -461,4 +495,21 @@ func (s *GcpMGCPSuite) Test_Frequency() { data, err = s.sut.Frequency(s.smgwEntity) assert.Nil(s.T(), err) assert.Equal(s.T(), 50.0, data) + + measData = &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(50), + ValueState: util.Ptr(model.MeasurementValueStateTypeError), + }, + }, + } + + _, fErr = rFeature.UpdateData(true, model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Frequency(s.smgwEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) } diff --git a/usecases/ma/mpc/public.go b/usecases/ma/mpc/public.go index 29a22c82..6df7fbfc 100644 --- a/usecases/ma/mpc/public.go +++ b/usecases/ma/mpc/public.go @@ -15,7 +15,8 @@ import ( // return the momentary active power consumption or production // // possible errors: -// - ErrDataNotAvailable if no such limit is (yet) available +// - 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) Power(entity spineapi.EntityRemoteInterface) (float64, error) { if !e.IsCompatibleEntityType(entity) { @@ -34,13 +35,15 @@ func (e *MPC) Power(entity spineapi.EntityRemoteInterface) (float64, error) { if len(values) != 1 { return 0, api.ErrDataNotAvailable } + return values[0], nil } // return the momentary active phase specific power consumption or production per phase // // possible errors: -// - ErrDataNotAvailable if no such limit is (yet) available +// - 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) { if !e.IsCompatibleEntityType(entity) { @@ -60,6 +63,11 @@ func (e *MPC) PowerPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, e // return the total consumption energy // // - positive values are used for consumption +// +// possible errors: +// - 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) EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, error) { if !e.IsCompatibleEntityType(entity) { return 0, api.ErrNoCompatibleEntity @@ -86,12 +94,23 @@ func (e *MPC) EnergyConsumed(entity spineapi.EntityRemoteInterface) (float64, er return 0, api.ErrDataNotAvailable } + // if the value state is set and not normal, the value is not valid and should be ignored + // therefore we return an error + if values[0].ValueState != nil && *values[0].ValueState != model.MeasurementValueStateTypeNormal { + return 0, api.ErrDataInvalid + } + return value.GetValue(), nil } // return the total feed in energy // // - negative values are used for production +// +// possible errors: +// - 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) EnergyProduced(entity spineapi.EntityRemoteInterface) (float64, error) { if !e.IsCompatibleEntityType(entity) { return 0, api.ErrNoCompatibleEntity @@ -118,6 +137,12 @@ func (e *MPC) EnergyProduced(entity spineapi.EntityRemoteInterface) (float64, er return 0, api.ErrDataNotAvailable } + // if the value state is set and not normal, the value is not valid and should be ignored + // therefore we return an error + if values[0].ValueState != nil && *values[0].ValueState != model.MeasurementValueStateTypeNormal { + return 0, api.ErrDataInvalid + } + return value.GetValue(), nil } @@ -127,6 +152,11 @@ func (e *MPC) EnergyProduced(entity spineapi.EntityRemoteInterface) (float64, er // // - positive values are used for consumption // - negative values are used for production +// +// possible errors: +// - 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) { if !e.IsCompatibleEntityType(entity) { return nil, api.ErrNoCompatibleEntity @@ -143,6 +173,11 @@ func (e *MPC) CurrentPerPhase(entity spineapi.EntityRemoteInterface) ([]float64, // Scenario 4 // return the phase specific voltage details +// +// possible errors: +// - 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) { if !e.IsCompatibleEntityType(entity) { return nil, api.ErrNoCompatibleEntity @@ -159,6 +194,11 @@ func (e *MPC) VoltagePerPhase(entity spineapi.EntityRemoteInterface) ([]float64, // Scenario 5 // return frequency +// +// possible errors: +// - 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) Frequency(entity spineapi.EntityRemoteInterface) (float64, error) { if !e.IsCompatibleEntityType(entity) { return 0, api.ErrNoCompatibleEntity @@ -179,6 +219,12 @@ func (e *MPC) Frequency(entity spineapi.EntityRemoteInterface) (float64, error) return 0, api.ErrDataNotAvailable } + // if the value state is set and not normal, the value is not valid and should be ignored + // therefore we return an error + if data[0].ValueState != nil && *data[0].ValueState != model.MeasurementValueStateTypeNormal { + return 0, api.ErrDataInvalid + } + // take the first item value := data[0].Value diff --git a/usecases/ma/mpc/public_test.go b/usecases/ma/mpc/public_test.go index cb6a8708..45622fe2 100644 --- a/usecases/ma/mpc/public_test.go +++ b/usecases/ma/mpc/public_test.go @@ -218,6 +218,21 @@ func (s *MaMPCSuite) Test_EnergyConsumed() { assert.Equal(s.T(), 0.0, data) measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + }, + }, + } + + _, fErr = rFeature.UpdateData(true, model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyConsumed(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData = &model.MeasurementListDataType{ MeasurementData: []model.MeasurementDataType{ { MeasurementId: util.Ptr(model.MeasurementIdType(0)), @@ -232,6 +247,23 @@ func (s *MaMPCSuite) Test_EnergyConsumed() { data, err = s.sut.EnergyConsumed(s.monitoredEntity) assert.Nil(s.T(), err) assert.Equal(s.T(), 10.0, data) + + measData = &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + ValueState: util.Ptr(model.MeasurementValueStateTypeError), + }, + }, + } + + _, fErr = rFeature.UpdateData(true, model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyConsumed(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) } func (s *MaMPCSuite) Test_EnergyProduced() { @@ -263,6 +295,21 @@ func (s *MaMPCSuite) Test_EnergyProduced() { assert.Equal(s.T(), 0.0, data) measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + }, + }, + } + + _, fErr = rFeature.UpdateData(true, model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyProduced(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData = &model.MeasurementListDataType{ MeasurementData: []model.MeasurementDataType{ { MeasurementId: util.Ptr(model.MeasurementIdType(0)), @@ -277,6 +324,23 @@ func (s *MaMPCSuite) Test_EnergyProduced() { data, err = s.sut.EnergyProduced(s.monitoredEntity) assert.Nil(s.T(), err) assert.Equal(s.T(), 10.0, data) + + measData = &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(10), + ValueState: util.Ptr(model.MeasurementValueStateTypeError), + }, + }, + } + + _, fErr = rFeature.UpdateData(true, model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.EnergyProduced(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) } func (s *MaMPCSuite) Test_CurrentPerPhase() { @@ -506,6 +570,21 @@ func (s *MaMPCSuite) Test_Frequency() { assert.Equal(s.T(), 0.0, data) measData := &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + }, + }, + } + + _, fErr = rFeature.UpdateData(true, model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Frequency(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) + + measData = &model.MeasurementListDataType{ MeasurementData: []model.MeasurementDataType{ { MeasurementId: util.Ptr(model.MeasurementIdType(0)), @@ -520,4 +599,21 @@ func (s *MaMPCSuite) Test_Frequency() { data, err = s.sut.Frequency(s.monitoredEntity) assert.Nil(s.T(), err) assert.Equal(s.T(), 50.0, data) + + measData = &model.MeasurementListDataType{ + MeasurementData: []model.MeasurementDataType{ + { + MeasurementId: util.Ptr(model.MeasurementIdType(0)), + Value: model.NewScaledNumberType(50), + ValueState: util.Ptr(model.MeasurementValueStateTypeError), + }, + }, + } + + _, fErr = rFeature.UpdateData(true, model.FunctionTypeMeasurementListData, measData, nil, nil) + assert.Nil(s.T(), fErr) + + data, err = s.sut.Frequency(s.monitoredEntity) + assert.NotNil(s.T(), err) + assert.Equal(s.T(), 0.0, data) }