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

This commit is contained in:
2026-02-08 08:56:16 -06:00
parent c96a6bb78b
commit 5923592b53
17 changed files with 13 additions and 13 deletions

78
model/alert.go Normal file
View File

@@ -0,0 +1,78 @@
// 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 dont 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"`
}

10
model/doc.go Normal file
View File

@@ -0,0 +1,10 @@
// 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

118
model/forecast.go Normal file
View File

@@ -0,0 +1,118 @@
// 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 WeatherObservations 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 wont 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 wont).
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"`
}

55
model/observation.go Normal file
View File

@@ -0,0 +1,55 @@
// 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"`
}

15
model/wmo.go Normal file
View File

@@ -0,0 +1,15 @@
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
)