2026-03-17 09:46:39 -05:00
|
|
|
|
// FILE: model/forecast.go
|
2026-01-13 18:14:21 -06:00
|
|
|
|
package model
|
|
|
|
|
|
|
|
|
|
|
|
import "time"
|
|
|
|
|
|
|
2026-01-16 00:04:37 -06:00
|
|
|
|
// 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
|
|
|
|
|
|
|
2026-06-10 20:28:41 +00:00
|
|
|
|
// Some providers include both a generated time and a subsequent update time.
|
2026-01-16 00:04:37 -06:00
|
|
|
|
// 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"`
|
|
|
|
|
|
|
2026-05-28 07:47:07 -05:00
|
|
|
|
// Canonical internal representation (provider-independent), when applicable.
|
|
|
|
|
|
// Some products (notably narrative) may not provide or imply a canonical WMO code.
|
|
|
|
|
|
ConditionCode *WMOCode `json:"conditionCode,omitempty"`
|
2026-01-16 00:04:37 -06:00
|
|
|
|
|
2026-03-26 21:35:08 -05:00
|
|
|
|
// Human-facing narrative summary for this period.
|
|
|
|
|
|
TextDescription string `json:"textDescription,omitempty"`
|
2026-01-16 00:04:37 -06:00
|
|
|
|
|
|
|
|
|
|
// 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"`
|
2026-01-17 10:16:50 -06:00
|
|
|
|
ApparentTemperatureC *float64 `json:"apparentTemperatureC,omitempty"`
|
2026-01-16 00:04:37 -06:00
|
|
|
|
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"`
|
2026-01-17 17:39:18 -06:00
|
|
|
|
SnowfallDepthMM *float64 `json:"snowfallDepthMm,omitempty"`
|
2026-01-16 00:04:37 -06:00
|
|
|
|
|
|
|
|
|
|
// Optional extras that some providers supply and downstream might care about.
|
|
|
|
|
|
UVIndex *float64 `json:"uvIndex,omitempty"`
|
2026-01-13 18:14:21 -06:00
|
|
|
|
}
|