Skip to content

Commit

Permalink
Properly handle invalid measurement data (#143)
Browse files Browse the repository at this point in the history
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.

This is the case for MPC and MGPC use cases
  • Loading branch information
DerAndereAndi authored Nov 22, 2024
2 parents 8366d85 + fed478a commit 59e7016
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 9 deletions.
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

0 comments on commit 59e7016

Please sign in to comment.