Updated the normalized observation schema to remove duplicate and/or unnecessary fields
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
This commit is contained in:
@@ -54,14 +54,6 @@ func buildObservation(parsed nwsObservationResponse) (model.WeatherObservation,
|
||||
ts = t.UTC()
|
||||
}
|
||||
|
||||
cloudLayers := make([]model.CloudLayer, 0, len(parsed.Properties.CloudLayers))
|
||||
for _, cl := range parsed.Properties.CloudLayers {
|
||||
cloudLayers = append(cloudLayers, model.CloudLayer{
|
||||
BaseMeters: cl.Base.Value,
|
||||
Amount: cl.Amount,
|
||||
})
|
||||
}
|
||||
|
||||
// Preserve raw presentWeather objects (for troubleshooting / drift analysis).
|
||||
present := make([]model.PresentWeather, 0, len(parsed.Properties.PresentWeather))
|
||||
for _, pw := range parsed.Properties.PresentWeather {
|
||||
@@ -70,6 +62,7 @@ func buildObservation(parsed nwsObservationResponse) (model.WeatherObservation,
|
||||
|
||||
// Decode presentWeather into typed METAR phenomena for mapping.
|
||||
phenomena := decodeMetarPhenomena(parsed.Properties.PresentWeather)
|
||||
cloudLayers := parsed.Properties.CloudLayers
|
||||
|
||||
providerDesc := strings.TrimSpace(parsed.Properties.TextDescription)
|
||||
|
||||
@@ -81,9 +74,6 @@ func buildObservation(parsed nwsObservationResponse) (model.WeatherObservation,
|
||||
isDay = isDayFromLatLonTime(*lat, *lon, ts)
|
||||
}
|
||||
|
||||
// Canonical condition text comes from our WMO table.
|
||||
canonicalText := standards.WMOText(wmo, isDay)
|
||||
|
||||
// Apparent temperature: prefer wind chill when both are supplied.
|
||||
var apparentC *float64
|
||||
if parsed.Properties.WindChill.Value != nil {
|
||||
@@ -98,15 +88,9 @@ func buildObservation(parsed nwsObservationResponse) (model.WeatherObservation,
|
||||
Timestamp: ts,
|
||||
|
||||
ConditionCode: wmo,
|
||||
ConditionText: canonicalText,
|
||||
IsDay: isDay,
|
||||
|
||||
ProviderRawDescription: providerDesc,
|
||||
|
||||
// Transitional / human-facing:
|
||||
// keep output consistent by populating TextDescription from canonical text.
|
||||
TextDescription: canonicalText,
|
||||
IconURL: parsed.Properties.Icon,
|
||||
TextDescription: providerDesc,
|
||||
|
||||
TemperatureC: parsed.Properties.Temperature.Value,
|
||||
DewpointC: parsed.Properties.Dewpoint.Value,
|
||||
@@ -115,19 +99,21 @@ func buildObservation(parsed nwsObservationResponse) (model.WeatherObservation,
|
||||
WindSpeedKmh: parsed.Properties.WindSpeed.Value,
|
||||
WindGustKmh: parsed.Properties.WindGust.Value,
|
||||
|
||||
BarometricPressurePa: parsed.Properties.BarometricPressure.Value,
|
||||
SeaLevelPressurePa: parsed.Properties.SeaLevelPressure.Value,
|
||||
BarometricPressurePa: pressurePrecedenceNWS(parsed.Properties.SeaLevelPressure.Value, parsed.Properties.BarometricPressure.Value),
|
||||
VisibilityMeters: parsed.Properties.Visibility.Value,
|
||||
|
||||
RelativeHumidityPercent: parsed.Properties.RelativeHumidity.Value,
|
||||
ApparentTemperatureC: apparentC,
|
||||
|
||||
ElevationMeters: parsed.Properties.Elevation.Value,
|
||||
RawMessage: parsed.Properties.RawMessage,
|
||||
|
||||
PresentWeather: present,
|
||||
CloudLayers: cloudLayers,
|
||||
}
|
||||
|
||||
return obs, ts, nil
|
||||
}
|
||||
|
||||
func pressurePrecedenceNWS(seaLevelPa, barometricPa *float64) *float64 {
|
||||
if seaLevelPa != nil {
|
||||
return seaLevelPa
|
||||
}
|
||||
return barometricPa
|
||||
}
|
||||
|
||||
51
internal/normalizers/nws/observation_test.go
Normal file
51
internal/normalizers/nws/observation_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package nws
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBuildObservationTextDescriptionAndPressurePrecedence(t *testing.T) {
|
||||
barometric := 100200.0
|
||||
seaLevel := 101400.0
|
||||
|
||||
parsed := nwsObservationResponse{}
|
||||
parsed.Properties.Timestamp = "2026-03-16T19:00:00Z"
|
||||
parsed.Properties.TextDescription = " Overcast "
|
||||
parsed.Properties.BarometricPressure.Value = &barometric
|
||||
parsed.Properties.SeaLevelPressure.Value = &seaLevel
|
||||
|
||||
obs, _, err := buildObservation(parsed)
|
||||
if err != nil {
|
||||
t.Fatalf("buildObservation() error = %v", err)
|
||||
}
|
||||
|
||||
if got, want := obs.TextDescription, "Overcast"; got != want {
|
||||
t.Fatalf("TextDescription = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if obs.BarometricPressurePa == nil {
|
||||
t.Fatalf("BarometricPressurePa = nil, want non-nil")
|
||||
}
|
||||
if got, want := *obs.BarometricPressurePa, seaLevel; got != want {
|
||||
t.Fatalf("BarometricPressurePa = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildObservationPressureFallbackToBarometric(t *testing.T) {
|
||||
barometric := 99900.0
|
||||
|
||||
parsed := nwsObservationResponse{}
|
||||
parsed.Properties.Timestamp = "2026-03-16T19:00:00Z"
|
||||
parsed.Properties.TextDescription = "Cloudy"
|
||||
parsed.Properties.BarometricPressure.Value = &barometric
|
||||
|
||||
obs, _, err := buildObservation(parsed)
|
||||
if err != nil {
|
||||
t.Fatalf("buildObservation() error = %v", err)
|
||||
}
|
||||
|
||||
if obs.BarometricPressurePa == nil {
|
||||
t.Fatalf("BarometricPressurePa = nil, want non-nil")
|
||||
}
|
||||
if got, want := *obs.BarometricPressurePa, barometric; got != want {
|
||||
t.Fatalf("BarometricPressurePa = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
@@ -86,16 +86,18 @@ type nwsObservationResponse struct {
|
||||
// We decode these as generic maps, then optionally interpret them in metar.go.
|
||||
PresentWeather []map[string]any `json:"presentWeather"`
|
||||
|
||||
CloudLayers []struct {
|
||||
Base struct {
|
||||
UnitCode string `json:"unitCode"`
|
||||
Value *float64 `json:"value"`
|
||||
} `json:"base"`
|
||||
Amount string `json:"amount"`
|
||||
} `json:"cloudLayers"`
|
||||
CloudLayers []nwsCloudLayer `json:"cloudLayers"`
|
||||
} `json:"properties"`
|
||||
}
|
||||
|
||||
type nwsCloudLayer struct {
|
||||
Base struct {
|
||||
UnitCode string `json:"unitCode"`
|
||||
Value *float64 `json:"value"`
|
||||
} `json:"base"`
|
||||
Amount string `json:"amount"`
|
||||
}
|
||||
|
||||
// nwsForecastResponse is a minimal-but-sufficient representation of the NWS
|
||||
// gridpoint forecast GeoJSON payload needed for mapping into model.WeatherForecastRun.
|
||||
//
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// 1. METAR phenomena (presentWeather) — most reliable for precip/hazards
|
||||
// 2. textDescription keywords — weaker, but reusable across providers
|
||||
// 3. cloud layers fallback — only for sky-only conditions
|
||||
func mapNWSToWMO(providerDesc string, cloudLayers []model.CloudLayer, phenomena []metarPhenomenon) model.WMOCode {
|
||||
func mapNWSToWMO(providerDesc string, cloudLayers []nwsCloudLayer, phenomena []metarPhenomenon) model.WMOCode {
|
||||
// 1) Prefer METAR phenomena if present.
|
||||
if code := wmoFromPhenomena(phenomena); code != model.WMOUnknown {
|
||||
return code
|
||||
@@ -167,7 +167,7 @@ func wmoFromPhenomena(phenomena []metarPhenomenon) model.WMOCode {
|
||||
return model.WMOUnknown
|
||||
}
|
||||
|
||||
func wmoFromCloudLayers(cloudLayers []model.CloudLayer) model.WMOCode {
|
||||
func wmoFromCloudLayers(cloudLayers []nwsCloudLayer) model.WMOCode {
|
||||
// NWS cloud layer amount values commonly include:
|
||||
// OVC, BKN, SCT, FEW, SKC, CLR, VV (vertical visibility / obscured sky)
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user