Moved the standards package out of internal/ so it can be imported by downstream consumers.
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:
23
standards/doc.go
Normal file
23
standards/doc.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// File: internal/standards/doc.go
|
||||
//
|
||||
// Package standards defines weatherfeeder’s provider-agnostic “project law”:
|
||||
//
|
||||
// - Schema identifiers and versioning conventions (see schema.go).
|
||||
// - Canonical interpretations / cross-provider mappings that are not specific to a
|
||||
// single upstream API (e.g., shared code tables, text heuristics, unit policy).
|
||||
// - Wire-format conventions for canonical payloads.
|
||||
//
|
||||
// Standards are used by both sources and normalizers. Keep this package free of
|
||||
// provider-specific logic and free of dependencies on internal/sources/* or
|
||||
// internal/normalizers/* to avoid import cycles.
|
||||
//
|
||||
// Wire-format conventions
|
||||
// -----------------------
|
||||
// For readability and stability, canonical payloads (weather.* schemas) should not emit
|
||||
// noisy floating-point representations. weatherfeeder enforces this by rounding float
|
||||
// values in canonical payloads to 4 digits after the decimal point at normalization
|
||||
// finalization time.
|
||||
//
|
||||
// Provider-specific decoding helpers and quirks live in internal/providers/<provider>.
|
||||
// Normalizer implementations and canonical mapping logic live in internal/normalizers/<provider>.
|
||||
package standards
|
||||
28
standards/schema.go
Normal file
28
standards/schema.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package standards
|
||||
|
||||
// Schema strings used by weatherfeeder.
|
||||
//
|
||||
// We standardize on schema matching for normalizers (rather than matching on
|
||||
// source names or kinds) because schema strings are explicit, versionable, and
|
||||
// independent of user configuration.
|
||||
//
|
||||
// Conventions:
|
||||
// - Raw upstream payloads: "raw.<provider>.<thing>.vN"
|
||||
// - Canonical domain events: "weather.<kind>.vN"
|
||||
const (
|
||||
// Raw upstream schemas (emitted by sources).
|
||||
SchemaRawNWSObservationV1 = "raw.nws.observation.v1"
|
||||
SchemaRawOpenMeteoCurrentV1 = "raw.openmeteo.current.v1"
|
||||
SchemaRawOpenWeatherCurrentV1 = "raw.openweather.current.v1"
|
||||
|
||||
SchemaRawNWSHourlyForecastV1 = "raw.nws.hourly.forecast.v1"
|
||||
SchemaRawOpenMeteoHourlyForecastV1 = "raw.openmeteo.hourly.forecast.v1"
|
||||
SchemaRawOpenWeatherHourlyForecastV1 = "raw.openweather.hourly.forecast.v1"
|
||||
|
||||
SchemaRawNWSAlertsV1 = "raw.nws.alerts.v1"
|
||||
|
||||
// Canonical domain schemas (emitted after normalization).
|
||||
SchemaWeatherObservationV1 = "weather.observation.v1"
|
||||
SchemaWeatherForecastV1 = "weather.forecast.v1"
|
||||
SchemaWeatherAlertV1 = "weather.alert.v1"
|
||||
)
|
||||
226
standards/wmo.go
Normal file
226
standards/wmo.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package standards
|
||||
|
||||
// This file provides small, shared helper functions for reasoning about WMO codes.
|
||||
// These are intentionally "coarse" categories that are useful for business logic,
|
||||
// dashboards, and alerting decisions.
|
||||
//
|
||||
// Example uses:
|
||||
// - jogging suitability: precipitation? thunderstorm? freezing precip?
|
||||
// - quick glance: "is it cloudy?" "is there any precip?"
|
||||
// - downstream normalizers / aggregators
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
)
|
||||
|
||||
type WMODescription struct {
|
||||
Day string
|
||||
Night string
|
||||
}
|
||||
|
||||
// WMODescriptions is the canonical internal mapping of WMO code -> day/night text.
|
||||
// These are used to populate model.WeatherObservation.ConditionText.
|
||||
var WMODescriptions = map[model.WMOCode]WMODescription{
|
||||
0: {Day: "Sunny", Night: "Clear"},
|
||||
1: {Day: "Mainly Sunny", Night: "Mainly Clear"},
|
||||
2: {Day: "Partly Cloudy", Night: "Partly Cloudy"},
|
||||
3: {Day: "Cloudy", Night: "Cloudy"},
|
||||
45: {Day: "Foggy", Night: "Foggy"},
|
||||
48: {Day: "Rime Fog", Night: "Rime Fog"},
|
||||
51: {Day: "Light Drizzle", Night: "Light Drizzle"},
|
||||
53: {Day: "Drizzle", Night: "Drizzle"},
|
||||
55: {Day: "Heavy Drizzle", Night: "Heavy Drizzle"},
|
||||
56: {Day: "Light Freezing Drizzle", Night: "Light Freezing Drizzle"},
|
||||
57: {Day: "Freezing Drizzle", Night: "Freezing Drizzle"},
|
||||
61: {Day: "Light Rain", Night: "Light Rain"},
|
||||
63: {Day: "Rain", Night: "Rain"},
|
||||
65: {Day: "Heavy Rain", Night: "Heavy Rain"},
|
||||
66: {Day: "Light Freezing Rain", Night: "Light Freezing Rain"},
|
||||
67: {Day: "Freezing Rain", Night: "Freezing Rain"},
|
||||
71: {Day: "Light Snow", Night: "Light Snow"},
|
||||
73: {Day: "Snow", Night: "Snow"},
|
||||
75: {Day: "Heavy Snow", Night: "Heavy Snow"},
|
||||
77: {Day: "Snow Grains", Night: "Snow Grains"},
|
||||
80: {Day: "Light Showers", Night: "Light Showers"},
|
||||
81: {Day: "Showers", Night: "Showers"},
|
||||
82: {Day: "Heavy Showers", Night: "Heavy Showers"},
|
||||
85: {Day: "Light Snow Showers", Night: "Light Snow Showers"},
|
||||
86: {Day: "Snow Showers", Night: "Snow Showers"},
|
||||
95: {Day: "Thunderstorm", Night: "Thunderstorm"},
|
||||
96: {Day: "Light Thunderstorms With Hail", Night: "Light Thunderstorms With Hail"},
|
||||
99: {Day: "Thunderstorm With Hail", Night: "Thunderstorm With Hail"},
|
||||
}
|
||||
|
||||
// WMOText returns the canonical text description for a WMO code.
|
||||
// If isDay is nil, it prefers the Day description (if present).
|
||||
//
|
||||
// This is intended to be used by drivers after they set ConditionCode.
|
||||
func WMOText(code model.WMOCode, isDay *bool) string {
|
||||
if code == model.WMOUnknown {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
desc, ok := WMODescriptions[code]
|
||||
if !ok {
|
||||
// Preserve the code in the message so it's diagnosable.
|
||||
return fmt.Sprintf("Unknown (WMO %d)", int(code))
|
||||
}
|
||||
|
||||
// If day/night is unknown, default to Day if it exists.
|
||||
if isDay == nil {
|
||||
if desc.Day != "" {
|
||||
return desc.Day
|
||||
}
|
||||
if desc.Night != "" {
|
||||
return desc.Night
|
||||
}
|
||||
return fmt.Sprintf("Unknown (WMO %d)", int(code))
|
||||
}
|
||||
|
||||
if *isDay {
|
||||
if desc.Day != "" {
|
||||
return desc.Day
|
||||
}
|
||||
// Fallback
|
||||
if desc.Night != "" {
|
||||
return desc.Night
|
||||
}
|
||||
return fmt.Sprintf("Unknown (WMO %d)", int(code))
|
||||
}
|
||||
|
||||
// Night
|
||||
if desc.Night != "" {
|
||||
return desc.Night
|
||||
}
|
||||
// Fallback
|
||||
if desc.Day != "" {
|
||||
return desc.Day
|
||||
}
|
||||
return fmt.Sprintf("Unknown (WMO %d)", int(code))
|
||||
}
|
||||
|
||||
// IsKnownWMO returns true if the code exists in our mapping table.
|
||||
func IsKnownWMO(code model.WMOCode) bool {
|
||||
if code == model.WMOUnknown {
|
||||
return false
|
||||
}
|
||||
_, ok := WMODescriptions[code]
|
||||
return ok
|
||||
}
|
||||
|
||||
func IsThunderstorm(code model.WMOCode) bool {
|
||||
switch code {
|
||||
case 95, 96, 99:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func IsHail(code model.WMOCode) bool {
|
||||
switch code {
|
||||
case 96, 99:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func IsFog(code model.WMOCode) bool {
|
||||
switch code {
|
||||
case 45, 48:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsPrecipitation returns true if the code represents any precipitation
|
||||
// (drizzle, rain, snow, showers, etc.).
|
||||
func IsPrecipitation(code model.WMOCode) bool {
|
||||
switch code {
|
||||
// Drizzle
|
||||
case 51, 53, 55, 56, 57:
|
||||
return true
|
||||
|
||||
// Rain
|
||||
case 61, 63, 65, 66, 67:
|
||||
return true
|
||||
|
||||
// Snow
|
||||
case 71, 73, 75, 77:
|
||||
return true
|
||||
|
||||
// Showers
|
||||
case 80, 81, 82, 85, 86:
|
||||
return true
|
||||
|
||||
// Thunderstorm (often includes rain/hail)
|
||||
case 95, 96, 99:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func IsRainFamily(code model.WMOCode) bool {
|
||||
switch code {
|
||||
// Drizzle + freezing drizzle
|
||||
case 51, 53, 55, 56, 57:
|
||||
return true
|
||||
|
||||
// Rain + freezing rain
|
||||
case 61, 63, 65, 66, 67:
|
||||
return true
|
||||
|
||||
// Rain showers
|
||||
case 80, 81, 82:
|
||||
return true
|
||||
|
||||
// Thunderstorm often implies rain
|
||||
case 95, 96, 99:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func IsSnowFamily(code model.WMOCode) bool {
|
||||
switch code {
|
||||
// Snow and related
|
||||
case 71, 73, 75, 77:
|
||||
return true
|
||||
|
||||
// Snow showers
|
||||
case 85, 86:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsFreezingPrecip returns true if the code represents freezing drizzle/rain.
|
||||
func IsFreezingPrecip(code model.WMOCode) bool {
|
||||
switch code {
|
||||
case 56, 57, 66, 67:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsSkyOnly returns true for codes that represent "sky condition only"
|
||||
// (clear/mostly/partly/cloudy) rather than fog/precip/etc.
|
||||
func IsSkyOnly(code model.WMOCode) bool {
|
||||
switch code {
|
||||
case 0, 1, 2, 3:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user