Moved the weatherfeeder model out of internal/ so that downstream consumers can import it directly.
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:
@@ -1,78 +0,0 @@
|
||||
// FILE: internal/model/alert.go
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// WeatherAlertRun is a snapshot of *active* alerts for a location as-of a point in time.
|
||||
//
|
||||
// This mirrors WeatherForecastRun's "one issued snapshot -> many contained items" shape:
|
||||
//
|
||||
// - A single run may contain zero, one, or many alerts.
|
||||
// - Runs are intended to be immutable snapshots (“provider asserted X at AsOf”).
|
||||
//
|
||||
// Normalizers should prefer to set AsOf from a provider-supplied “updated/generated” timestamp.
|
||||
// If unavailable, AsOf may be set to the poll/emit time as a fallback.
|
||||
type WeatherAlertRun struct {
|
||||
// Optional location metadata (provider-dependent).
|
||||
LocationID string `json:"locationId,omitempty"`
|
||||
LocationName string `json:"locationName,omitempty"`
|
||||
|
||||
// AsOf is when the provider asserted this alert snapshot is current (required).
|
||||
AsOf time.Time `json:"asOf"`
|
||||
|
||||
// Optional spatial context.
|
||||
Latitude *float64 `json:"latitude,omitempty"`
|
||||
Longitude *float64 `json:"longitude,omitempty"`
|
||||
|
||||
// Active alerts contained in this snapshot (order is provider-dependent).
|
||||
Alerts []WeatherAlert `json:"alerts"`
|
||||
}
|
||||
|
||||
// WeatherAlert is the canonical representation of a single alert.
|
||||
//
|
||||
// This is intentionally a “useful subset” of rich provider payloads.
|
||||
// Normalizers may populate ProviderExtras for structured provider-specific fields
|
||||
// that don’t cleanly fit the canonical shape.
|
||||
type WeatherAlert struct {
|
||||
// Provider-stable identifier (often a URL/URI).
|
||||
ID string `json:"id"`
|
||||
|
||||
// Classification / headline fields.
|
||||
Event string `json:"event,omitempty"`
|
||||
Headline string `json:"headline,omitempty"`
|
||||
|
||||
Severity string `json:"severity,omitempty"` // e.g. Extreme/Severe/Moderate/Minor/Unknown
|
||||
Urgency string `json:"urgency,omitempty"` // e.g. Immediate/Expected/Future/Past/Unknown
|
||||
Certainty string `json:"certainty,omitempty"` // e.g. Observed/Likely/Possible/Unlikely/Unknown
|
||||
|
||||
Status string `json:"status,omitempty"` // e.g. Actual/Exercise/Test/System/Unknown
|
||||
MessageType string `json:"messageType,omitempty"` // e.g. Alert/Update/Cancel
|
||||
Category string `json:"category,omitempty"` // e.g. Met/Geo/Safety/Rescue/Fire/Health/Env/Transport/Infra/CBRNE/Other
|
||||
Response string `json:"response,omitempty"` // e.g. Shelter/Evacuate/Prepare/Execute/Avoid/Monitor/Assess/AllClear/None
|
||||
|
||||
// Narrative.
|
||||
Description string `json:"description,omitempty"`
|
||||
Instruction string `json:"instruction,omitempty"`
|
||||
|
||||
// Timing (all optional; provider-dependent).
|
||||
Sent *time.Time `json:"sent,omitempty"`
|
||||
Effective *time.Time `json:"effective,omitempty"`
|
||||
Onset *time.Time `json:"onset,omitempty"`
|
||||
Expires *time.Time `json:"expires,omitempty"`
|
||||
|
||||
// Scope / affected area.
|
||||
AreaDescription string `json:"areaDescription,omitempty"` // often a provider string
|
||||
|
||||
// Provenance.
|
||||
SenderName string `json:"senderName,omitempty"`
|
||||
|
||||
References []AlertReference `json:"references,omitempty"`
|
||||
}
|
||||
|
||||
// AlertReference is a reference to a related alert (updates, replacements, etc.).
|
||||
type AlertReference struct {
|
||||
ID string `json:"id,omitempty"` // provider reference ID/URI
|
||||
Identifier string `json:"identifier,omitempty"` // provider identifier string, if distinct
|
||||
Sender string `json:"sender,omitempty"`
|
||||
Sent *time.Time `json:"sent,omitempty"`
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// FILE: internal/model/doc.go
|
||||
// Package model defines weatherfeeder's canonical domain payload types.
|
||||
//
|
||||
// These structs are emitted as the Payload of canonical events (schemas "weather.*.vN").
|
||||
// JSON tags are treated as part of the wire contract for sinks (stdout today; others later).
|
||||
//
|
||||
// Compatibility guidance:
|
||||
// - Prefer additive changes.
|
||||
// - Avoid renaming/removing fields without a schema version bump.
|
||||
package model
|
||||
@@ -1,118 +0,0 @@
|
||||
// FILE: internal/model/forecast.go
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// ForecastProduct distinguishes *what kind* of forecast a provider is offering.
|
||||
// This is intentionally canonical and not provider-nomenclature.
|
||||
type ForecastProduct string
|
||||
|
||||
const (
|
||||
// ForecastProductHourly is a sub-daily forecast where each period is typically ~1 hour.
|
||||
ForecastProductHourly ForecastProduct = "hourly"
|
||||
|
||||
// ForecastProductNarrative is a human-oriented sequence of periods like
|
||||
// "Tonight", "Friday", "Friday Night" with variable duration.
|
||||
//
|
||||
// (NWS "/forecast" looks like this; it is not strictly “daily”.)
|
||||
ForecastProductNarrative ForecastProduct = "narrative"
|
||||
|
||||
// ForecastProductDaily is a calendar-day (or day-bucketed) forecast where a period
|
||||
// commonly carries min/max values (Open-Meteo daily, many others).
|
||||
ForecastProductDaily ForecastProduct = "daily"
|
||||
)
|
||||
|
||||
// WeatherForecastRun is a single issued forecast snapshot for a location and product.
|
||||
//
|
||||
// Design goals:
|
||||
// - Immutable snapshot semantics: “provider asserted X at IssuedAt”.
|
||||
// - Provider-independent schema: normalize many upstreams into one shape.
|
||||
// - Retrieval-friendly: periods are inside the run, but can be stored/indexed separately.
|
||||
type WeatherForecastRun struct {
|
||||
// Identity / metadata (aligned with WeatherObservation’s StationID/StationName/Timestamp).
|
||||
LocationID string `json:"locationId,omitempty"`
|
||||
LocationName string `json:"locationName,omitempty"`
|
||||
IssuedAt time.Time `json:"issuedAt"` // required: when this run was generated/issued
|
||||
|
||||
// Some providers include both a generated time and a later update time.
|
||||
// Keep UpdatedAt optional; many providers won’t supply it.
|
||||
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
|
||||
|
||||
// What sort of forecast this run represents (hourly vs narrative vs daily).
|
||||
Product ForecastProduct `json:"product"`
|
||||
|
||||
// Optional spatial context. Many providers are fundamentally lat/lon-based.
|
||||
Latitude *float64 `json:"latitude,omitempty"`
|
||||
Longitude *float64 `json:"longitude,omitempty"`
|
||||
|
||||
// Kept to align with WeatherObservation and because elevation is sometimes important
|
||||
// for interpreting temps/precip, even if not always supplied by the provider.
|
||||
ElevationMeters *float64 `json:"elevationMeters,omitempty"`
|
||||
|
||||
// The forecast periods contained in this issued run. Order should be chronological.
|
||||
Periods []WeatherForecastPeriod `json:"periods"`
|
||||
}
|
||||
|
||||
// WeatherForecastPeriod is a forecast for a specific valid interval [StartTime, EndTime).
|
||||
//
|
||||
// Conceptually, it mirrors WeatherObservation (condition + measurements), but:
|
||||
// / - It has a time *range* (start/end) instead of a single timestamp.
|
||||
// / - It adds forecast-specific fields like probability/amount of precip.
|
||||
// / - It supports min/max for “daily” products (and other aggregated periods).
|
||||
type WeatherForecastPeriod struct {
|
||||
// Identity / validity window (required)
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
|
||||
// Human-facing period label (e.g., "Tonight", "Friday"). Often empty for hourly.
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
// Canonical day/night hint (aligned with WeatherObservation.IsDay).
|
||||
// Providers vary in whether they explicitly include this.
|
||||
IsDay *bool `json:"isDay,omitempty"`
|
||||
|
||||
// Canonical internal representation (provider-independent).
|
||||
// Like WeatherObservation, this is required; use an “unknown” WMOCode if unmappable.
|
||||
ConditionCode WMOCode `json:"conditionCode"`
|
||||
|
||||
// Provider-independent short text describing the conditions (normalized, if possible).
|
||||
ConditionText string `json:"conditionText,omitempty"`
|
||||
|
||||
// Provider-specific “evidence” for troubleshooting mapping and drift.
|
||||
ProviderRawDescription string `json:"providerRawDescription,omitempty"`
|
||||
|
||||
// Human-facing narrative. Not all providers supply rich text (Open-Meteo often won’t).
|
||||
TextDescription string `json:"textDescription,omitempty"` // short phrase / summary
|
||||
DetailedText string `json:"detailedText,omitempty"` // longer narrative, if available
|
||||
|
||||
// Provider-specific (legacy / transitional)
|
||||
IconURL string `json:"iconUrl,omitempty"`
|
||||
|
||||
// Core predicted measurements (nullable; units align with WeatherObservation)
|
||||
TemperatureC *float64 `json:"temperatureC,omitempty"`
|
||||
|
||||
// For aggregated products (notably “daily”), providers may supply min/max.
|
||||
TemperatureCMin *float64 `json:"temperatureCMin,omitempty"`
|
||||
TemperatureCMax *float64 `json:"temperatureCMax,omitempty"`
|
||||
|
||||
DewpointC *float64 `json:"dewpointC,omitempty"`
|
||||
RelativeHumidityPercent *float64 `json:"relativeHumidityPercent,omitempty"`
|
||||
WindDirectionDegrees *float64 `json:"windDirectionDegrees,omitempty"`
|
||||
WindSpeedKmh *float64 `json:"windSpeedKmh,omitempty"`
|
||||
WindGustKmh *float64 `json:"windGustKmh,omitempty"`
|
||||
BarometricPressurePa *float64 `json:"barometricPressurePa,omitempty"`
|
||||
VisibilityMeters *float64 `json:"visibilityMeters,omitempty"`
|
||||
ApparentTemperatureC *float64 `json:"apparentTemperatureC,omitempty"`
|
||||
CloudCoverPercent *float64 `json:"cloudCoverPercent,omitempty"`
|
||||
|
||||
// Precipitation (forecast-specific). Keep these generic and provider-independent.
|
||||
ProbabilityOfPrecipitationPercent *float64 `json:"probabilityOfPrecipitationPercent,omitempty"`
|
||||
|
||||
// Quantitative precip is not universally available, but OpenWeather/Open-Meteo often supply it.
|
||||
// Use liquid-equivalent mm for interoperability.
|
||||
PrecipitationAmountMm *float64 `json:"precipitationAmountMm,omitempty"`
|
||||
SnowfallDepthMM *float64 `json:"snowfallDepthMm,omitempty"`
|
||||
|
||||
// Optional extras that some providers supply and downstream might care about.
|
||||
UVIndex *float64 `json:"uvIndex,omitempty"`
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// FILE: internal/model/observation.go
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type WeatherObservation struct {
|
||||
// Identity / metadata
|
||||
StationID string `json:"stationId,omitempty"`
|
||||
StationName string `json:"stationName,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
|
||||
// Canonical internal representation (provider-independent).
|
||||
ConditionCode WMOCode `json:"conditionCode"`
|
||||
ConditionText string `json:"conditionText,omitempty"`
|
||||
IsDay *bool `json:"isDay,omitempty"`
|
||||
|
||||
// Provider-specific “evidence” for troubleshooting mapping and drift.
|
||||
ProviderRawDescription string `json:"providerRawDescription,omitempty"`
|
||||
|
||||
// Human-facing (legacy / transitional)
|
||||
TextDescription string `json:"textDescription,omitempty"`
|
||||
|
||||
// Provider-specific (legacy / transitional)
|
||||
IconURL string `json:"iconUrl,omitempty"`
|
||||
|
||||
// Core measurements (nullable)
|
||||
TemperatureC *float64 `json:"temperatureC,omitempty"`
|
||||
DewpointC *float64 `json:"dewpointC,omitempty"`
|
||||
|
||||
WindDirectionDegrees *float64 `json:"windDirectionDegrees,omitempty"`
|
||||
WindSpeedKmh *float64 `json:"windSpeedKmh,omitempty"`
|
||||
WindGustKmh *float64 `json:"windGustKmh,omitempty"`
|
||||
|
||||
BarometricPressurePa *float64 `json:"barometricPressurePa,omitempty"`
|
||||
SeaLevelPressurePa *float64 `json:"seaLevelPressurePa,omitempty"`
|
||||
VisibilityMeters *float64 `json:"visibilityMeters,omitempty"`
|
||||
|
||||
RelativeHumidityPercent *float64 `json:"relativeHumidityPercent,omitempty"`
|
||||
ApparentTemperatureC *float64 `json:"apparentTemperatureC,omitempty"`
|
||||
|
||||
ElevationMeters *float64 `json:"elevationMeters,omitempty"`
|
||||
RawMessage string `json:"rawMessage,omitempty"`
|
||||
|
||||
PresentWeather []PresentWeather `json:"presentWeather,omitempty"`
|
||||
CloudLayers []CloudLayer `json:"cloudLayers,omitempty"`
|
||||
}
|
||||
|
||||
type CloudLayer struct {
|
||||
BaseMeters *float64 `json:"baseMeters,omitempty"`
|
||||
Amount string `json:"amount,omitempty"`
|
||||
}
|
||||
|
||||
type PresentWeather struct {
|
||||
Raw map[string]any `json:"raw,omitempty"`
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package model
|
||||
|
||||
// WMOCode is the canonical internal “current conditions” vocabulary.
|
||||
//
|
||||
// We standardize on the WMO weather interpretation codes used by providers like
|
||||
// Open-Meteo, and we map other providers (e.g., NWS) into these codes.
|
||||
//
|
||||
// Reference codes include: 0,1,2,3,45,48,51,53,...,99.
|
||||
type WMOCode int
|
||||
|
||||
const (
|
||||
// WMOUnknown is used when we cannot confidently map an upstream condition
|
||||
// into a known WMO code.
|
||||
WMOUnknown WMOCode = -1
|
||||
)
|
||||
@@ -4,7 +4,7 @@ package common
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/internal/model"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
)
|
||||
|
||||
// WMOFromTextDescription is a cross-provider fallback that tries to infer a WMO code
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/ejr/feedkit/event"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/internal/model"
|
||||
normcommon "gitea.maximumdirect.net/ejr/weatherfeeder/internal/normalizers/common"
|
||||
nwscommon "gitea.maximumdirect.net/ejr/weatherfeeder/internal/providers/nws"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/internal/standards"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
)
|
||||
|
||||
// AlertsNormalizer converts:
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/ejr/feedkit/event"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/internal/model"
|
||||
normcommon "gitea.maximumdirect.net/ejr/weatherfeeder/internal/normalizers/common"
|
||||
nwscommon "gitea.maximumdirect.net/ejr/weatherfeeder/internal/providers/nws"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/internal/standards"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
)
|
||||
|
||||
// ForecastNormalizer converts:
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/internal/model"
|
||||
normcommon "gitea.maximumdirect.net/ejr/weatherfeeder/internal/normalizers/common"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
)
|
||||
|
||||
// centroidLatLon returns a best-effort centroid (lat, lon) from a GeoJSON polygon.
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/ejr/feedkit/event"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/internal/model"
|
||||
normcommon "gitea.maximumdirect.net/ejr/weatherfeeder/internal/normalizers/common"
|
||||
nwscommon "gitea.maximumdirect.net/ejr/weatherfeeder/internal/providers/nws"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/internal/standards"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
)
|
||||
|
||||
// ObservationNormalizer converts:
|
||||
|
||||
@@ -4,8 +4,8 @@ package nws
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/internal/model"
|
||||
normcommon "gitea.maximumdirect.net/ejr/weatherfeeder/internal/normalizers/common"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
)
|
||||
|
||||
// mapNWSToWMO maps NWS signals into a canonical WMO code.
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/ejr/feedkit/event"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/internal/model"
|
||||
normcommon "gitea.maximumdirect.net/ejr/weatherfeeder/internal/normalizers/common"
|
||||
omcommon "gitea.maximumdirect.net/ejr/weatherfeeder/internal/providers/openmeteo"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/internal/standards"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
)
|
||||
|
||||
// ForecastNormalizer converts:
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/ejr/feedkit/event"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/internal/model"
|
||||
normcommon "gitea.maximumdirect.net/ejr/weatherfeeder/internal/normalizers/common"
|
||||
omcommon "gitea.maximumdirect.net/ejr/weatherfeeder/internal/providers/openmeteo"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/internal/standards"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
)
|
||||
|
||||
// ObservationNormalizer converts:
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/ejr/feedkit/event"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/internal/model"
|
||||
normcommon "gitea.maximumdirect.net/ejr/weatherfeeder/internal/normalizers/common"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/internal/standards"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
)
|
||||
|
||||
// ObservationNormalizer converts:
|
||||
@@ -121,7 +121,7 @@ func buildObservation(parsed owmResponse) (model.WeatherObservation, time.Time,
|
||||
TextDescription: canonicalText,
|
||||
IconURL: iconURL,
|
||||
|
||||
TemperatureC: &tempC,
|
||||
TemperatureC: &tempC,
|
||||
ApparentTemperatureC: apparentC,
|
||||
|
||||
WindDirectionDegrees: parsed.Wind.Deg,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FILE: ./internal/normalizers/openweather/wmo_map.go
|
||||
package openweather
|
||||
|
||||
import "gitea.maximumdirect.net/ejr/weatherfeeder/internal/model"
|
||||
import "gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
|
||||
// mapOpenWeatherToWMO maps OpenWeather weather condition IDs into weatherfeeder's
|
||||
// canonical WMO code vocabulary.
|
||||
|
||||
@@ -3,7 +3,7 @@ package standards
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/internal/model"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
)
|
||||
|
||||
type WMODescription struct {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package standards
|
||||
|
||||
import "gitea.maximumdirect.net/ejr/weatherfeeder/internal/model"
|
||||
import "gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
|
||||
// This file provides small, shared helper functions for reasoning about WMO codes.
|
||||
// These are intentionally "coarse" categories that are useful for business logic,
|
||||
|
||||
Reference in New Issue
Block a user