diff --git a/pkg/messageprocessors/normalizedpayload/uplink.go b/pkg/messageprocessors/normalizedpayload/uplink.go index 1cf083fda0c..2bd121322d6 100644 --- a/pkg/messageprocessors/normalizedpayload/uplink.go +++ b/pkg/messageprocessors/normalizedpayload/uplink.go @@ -141,6 +141,24 @@ func parseNumber(selector func(dst *Measurement) **float64, vals ...fieldValidat } } +// parsePercentage parses and validates a percentage. +func parsePercentage(selector func(dst *Measurement) **float64) fieldParser { + return parseNumber( + selector, + minimum(0.0), + maximum(100.0), + ) +} + +// parseConcentration parses and validates a concentration. Concentration must be in ppm between 0 and 1000000. +func parseConcentration(selector func(dst *Measurement) **float64) fieldParser { + return parseNumber( + selector, + minimum(0.0), + maximum(1000000.0), + ) +} + // minimum returns a field validator that checks the inclusive minimum. func minimum[T constraints.Ordered](min T) fieldValidator[T] { return func(v T, path string) error { @@ -212,12 +230,10 @@ var fieldParsers = map[string]fieldParser{ }, minimum(0.0), ), - "soil.moisture": parseNumber( + "soil.moisture": parsePercentage( func(dst *Measurement) **float64 { return &dst.Soil.Moisture }, - minimum(0.0), - maximum(100.0), ), "soil.temperature": parseNumber( func(dst *Measurement) **float64 { @@ -239,26 +255,20 @@ var fieldParsers = map[string]fieldParser{ minimum(0.0), maximum(14.0), ), - "soil.n": parseNumber( + "soil.n": parseConcentration( func(dst *Measurement) **float64 { return &dst.Soil.Nitrogen }, - minimum(0.0), - maximum(1000000.0), ), - "soil.p": parseNumber( + "soil.p": parseConcentration( func(dst *Measurement) **float64 { return &dst.Soil.Phosphorus }, - minimum(0.0), - maximum(1000000.0), ), - "soil.k": parseNumber( + "soil.k": parseConcentration( func(dst *Measurement) **float64 { return &dst.Soil.Potassium }, - minimum(0.0), - maximum(1000000.0), ), "air": object( func(dst *Measurement) *Air { @@ -271,12 +281,10 @@ var fieldParsers = map[string]fieldParser{ }, minimum(-273.15), ), - "air.relativeHumidity": parseNumber( + "air.relativeHumidity": parsePercentage( func(dst *Measurement) **float64 { return &dst.Air.RelativeHumidity }, - minimum(0.0), - maximum(100.0), ), "air.pressure": parseNumber( func(dst *Measurement) **float64 { @@ -285,12 +293,10 @@ var fieldParsers = map[string]fieldParser{ minimum(900.0), maximum(1100.0), ), - "air.co2": parseNumber( + "air.co2": parseConcentration( func(dst *Measurement) **float64 { return &dst.Air.CO2 }, - minimum(0.0), - maximum(1000000.0), ), "air.lightIntensity": parseNumber( func(dst *Measurement) **float64 { diff --git a/pkg/messageprocessors/normalizedpayload/uplink_test.go b/pkg/messageprocessors/normalizedpayload/uplink_test.go index 286f520a7fb..e023720cff1 100644 --- a/pkg/messageprocessors/normalizedpayload/uplink_test.go +++ b/pkg/messageprocessors/normalizedpayload/uplink_test.go @@ -62,6 +62,35 @@ func TestUplink(t *testing.T) { }, }, }, + { + name: "one soil nutrient concentration", + normalizedPayload: []*structpb.Struct{ + { + Fields: map[string]*structpb.Value{ + "soil": { + Kind: &structpb.Value_StructValue{ + StructValue: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "n": { + Kind: &structpb.Value_NumberValue{ + NumberValue: 999999.99, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: []normalizedpayload.Measurement{ + { + Soil: normalizedpayload.Soil{ + Nitrogen: float64Ptr(999999.99), + }, + }, + }, + }, { name: "two air temperatures", normalizedPayload: []*structpb.Struct{ @@ -122,6 +151,39 @@ func TestUplink(t *testing.T) { {}, }, }, + { + name: "above 100 percent soil moisture", + normalizedPayload: []*structpb.Struct{ + { + Fields: map[string]*structpb.Value{ + "soil": { + Kind: &structpb.Value_StructValue{ + StructValue: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "moisture": { + Kind: &structpb.Value_NumberValue{ + NumberValue: 120, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: []normalizedpayload.Measurement{ + {}, + }, + expectedValidationErrors: [][]error{ + { + normalizedpayload.ErrFieldMaximum.WithAttributes( + "path", "soil.moisture", + "maximum", 100.0, + ), + }, + }, + }, { name: "below absolute zero", normalizedPayload: []*structpb.Struct{