// 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"` }