Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Properly handle invalid measurement data #143

Merged
merged 2 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
6 changes: 6 additions & 0 deletions usecases/internal/measurement.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 34 additions & 0 deletions usecases/internal/measurement_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
65 changes: 58 additions & 7 deletions usecases/ma/mgcp/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand All @@ -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
Expand All @@ -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
}

Expand All @@ -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
Expand All @@ -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
}

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
}
51 changes: 51 additions & 0 deletions usecases/ma/mgcp/public_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
}
Loading