Help us shape the future of Go! Take the 2026 Go Developer Survey

weather

package
v0.40.22 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package weather enriches a drive with the conditions at its start point and time. Used by the trip recap so the model can attribute efficiency swings to weather instead of inventing the comparison.

Privacy posture

Recap.go's header pins the rule: no GPS coordinates leave the box. That's strictly broken by hitting any external weather API, so we mitigate three ways:

  1. Coords are rounded to CoarseRound (0.1 deg, ~11 km) before the upstream request. Open-Meteo's grid is ~9 km, so the rounding costs nothing in accuracy.
  2. The whole feature is gated behind an opt-in operator setting (recap.weather_enabled). Default off.
  3. Results are cached per drive in drive_weather and never re-fetched on regenerate.

Source

Open-Meteo (https://open-meteo.com/) free archive endpoint. No API key required, no quota issues at single-tenant volume. ERA5 reanalysis ~9 km grid, hourly back to 1940. We pull a single hour (the trip start hour, UTC truncated) and lift the closest reading.

Index

Constants

View Source
const CoarseRound = 0.1

CoarseRound is the lat/lon rounding step in degrees applied before the upstream request. 0.1 ~= 11 km.

View Source
const Provider = "open-meteo"

Provider is the upstream identifier we stamp into the cache row so future swaps stay clean.

Variables

This section is empty.

Functions

func Bearing

func Bearing(lat1, lon1, lat2, lon2 float64) float64

Bearing returns the initial compass bearing in degrees (0-360) from (lat1,lon1) to (lat2,lon2). Used to project wind onto the trip direction.

func Coarsen

func Coarsen(lat, lon float64) (float64, float64)

Coarsen rounds (lat, lon) to CoarseRound before disclosure.

func Headwind

func Headwind(windKPH, windFromDeg, tripBearingDeg float64) float64

Headwind returns the signed component of the wind vector along the trip bearing in kph. Open-Meteo reports wind direction as the direction the wind is coming FROM, so a wind from due-west (270) against a bearing of due-east (90) is a 180 deg disagreement, i.e. pure headwind.

Result interpretation:

+v  -> headwind of v kph (slows you down)
-v  -> tailwind of v kph (speeds you up)

Types

type Cache

type Cache struct {
	// contains filtered or unexported fields
}

Cache is the persistent cache backing for drive weather. Methods are safe to call with a nil Cache; they degrade to "no-op". The recap handler keeps a single Cache for the lifetime of the process.

func NewCache

func NewCache(db *sql.DB) *Cache

NewCache wraps a pool. Returns nil on a nil pool so callers can stay branchless.

func (*Cache) Get

func (c *Cache) Get(ctx context.Context, uid uuid.UUID, driveID string) (*Snapshot, error)

Get returns the cached snapshot for (uid, driveID), or (nil, nil) when there's no row.

func (*Cache) GetSeries

func (c *Cache) GetSeries(ctx context.Context, uid uuid.UUID, driveID string) ([]SeriesRow, error)

GetSeries returns the cached time-series rows for (uid, driveID), ordered by sampled_at. (nil, nil) when no rows exist.

func (*Cache) Put

func (c *Cache) Put(ctx context.Context, uid uuid.UUID, driveID string, lat, lon float64, sampledAt time.Time, s *Snapshot) error

Put writes a snapshot. Uses ON CONFLICT so a regenerate after a successful first fetch is a no-op rather than churning the row.

func (*Cache) PutSeries

func (c *Cache) PutSeries(ctx context.Context, uid uuid.UUID, driveID string, lat, lon float64, rows []SeriesRow) error

PutSeries persists rows for (uid, driveID). Replaces any existing rows for that drive in a single transaction so a partial re-fetch can never leave a torn series.

type Client

type Client struct {
	HTTP    *http.Client
	BaseURL string // override for tests; defaults to https://archive-api.open-meteo.com
}

Client fetches an hourly snapshot from Open-Meteo's archive API. Stateless; safe to share.

func NewClient

func NewClient() *Client

NewClient returns a Client with sane defaults.

func (*Client) FetchHour

func (c *Client) FetchHour(ctx context.Context, lat, lon float64, at time.Time, tripBearingDeg float64, hasBearing bool) (*Snapshot, time.Time, error)

FetchHour returns the snapshot for the hour containing `at` at the rounded (lat, lon). The trip bearing is used to project wind onto a headwind component; pass 0 if unknown (HeadwindKPH stays unset).

Routes to the forecast endpoint (api.open-meteo.com/v1/forecast) when `at` is within forecastWindowDays of now — past or future — so a planned future departure resolves to a real forecast instead of failing against the archive's "no data past today" window. Older timestamps go to the archive endpoint as before.

func (*Client) FetchRange

func (c *Client) FetchRange(ctx context.Context, lat, lon float64, start, end time.Time, tripBearingDeg float64, hasBearing bool) ([]SeriesRow, error)

FetchRange returns one weather sample per cadence step covering [start, end] at the rounded (lat, lon). Picks 15-minute cadence from the forecast endpoint when the drive is within the past forecastWindowDays, otherwise falls back to hourly archive data.

tripBearingDeg + hasBearing are used to project wind onto a per-sample headwind component the same way FetchHour does for the start-hour snapshot.

The returned slice is empty (not nil) on a successful upstream call that yielded zero usable rows; nil + non-nil error means a transport / decode failure that the caller should log.

type MemCache added in v0.18.81

type MemCache struct {
	// contains filtered or unexported fields
}

MemCache is a small in-process LRU for FetchHour results, keyed by (coarsened lat, coarsened lon, UTC hour). Trip-planner plans frequently revisit the same geographic hour-buckets across successive replan-on-edit cycles; this collapses those into a single upstream call.

Entries expire after TTL — relevant for the forecast endpoint, whose values shift as the underlying ECMWF run refreshes. Capacity is a soft cap: when exceeded the cache evicts the oldest entries on the next insert (sorted by insertedAt). The drive-weather `Cache` type is unrelated — that one is Postgres-backed and keyed per-drive.

Headwind is NOT cached. The upstream snapshot's wind speed + dir are the expensive bits to fetch; headwind is recomputed on every read from the caller's trip bearing so two plans heading different directions through the same hour-bucket share the same row.

Safe for concurrent use; nil receiver is a no-op (FetchHourCached just calls through to the client).

func NewMemCache added in v0.18.81

func NewMemCache(ttl time.Duration, max int) *MemCache

NewMemCache builds a MemCache with the given TTL and capacity. A zero or negative ttl falls back to 15 minutes (the natural freshness of Open-Meteo forecast data — ECMWF reruns roughly quarter-hourly). A zero or negative max falls back to 1024.

func (*MemCache) FetchHourCached added in v0.18.81

func (m *MemCache) FetchHourCached(
	ctx context.Context,
	client *Client,
	lat, lon float64,
	at time.Time,
	tripBearingDeg float64,
	hasBearing bool,
) (*Snapshot, time.Time, error)

FetchHourCached returns the snapshot for (lat, lon, at), preferring the cache. On miss it calls client.FetchHour and inserts. Headwind is recomputed from the cached wind fields on every read using tripBearingDeg, so callers with different bearings can share the same cached upstream row.

func (*MemCache) Len added in v0.18.81

func (m *MemCache) Len() int

Len returns the number of live entries. Exposed for tests + the /metrics endpoint if anyone wants to wire a gauge.

type SeriesRow

type SeriesRow struct {
	SampledAt      time.Time
	CadenceMinutes int
	TempC          float64
	ApparentTempC  float64
	WindKPH        float64
	WindDirDeg     float64
	HeadwindKPH    float64
	PrecipMM       float64
	HumidityPct    float64
	Conditions     string
	HasTemp        bool
	HasApparent    bool
	HasWind        bool
	HasHeadwind    bool
	HasPrecip      bool
	HasHumidity    bool
	HasConditions  bool
}

SeriesRow is one sample of weather along a drive. Used to back the temperature + precipitation graph on the drive detail page. Same metric base units as Snapshot. CadenceMinutes is 15 (forecast API minutely_15) or 60 (archive API hourly), set by the fetcher.

type Snapshot

type Snapshot struct {
	TempC         float64
	ApparentTempC float64
	WindKPH       float64
	WindDirDeg    float64 // direction the wind is coming FROM (Open-Meteo convention)
	HeadwindKPH   float64 // signed projection onto trip bearing; negative = tailwind
	PrecipMM      float64
	HumidityPct   float64
	Conditions    string // short label from WMO weather code
	HasTemp       bool
	HasWind       bool
	HasPrecip     bool
	HasHumidity   bool
	HasConditions bool
	HasApparent   bool
	HasHeadwind   bool
}

Snapshot is the prompt-ready view of weather at a drive's start. Fields are zero when the upstream omitted the metric, with one exception: WindDirDeg is documented as "from direction" by Open- Meteo (degrees clockwise from north), so 0.0 is a valid value; callers must check HasWind to disambiguate.

func FetchAndCache

func FetchAndCache(
	ctx context.Context,
	cache *Cache,
	uid uuid.UUID,
	driveID string,
	startedAt, endedAt time.Time,
	startLat, startLon, endLat, endLon float64,
) (*Snapshot, error)

FetchAndCache populates both the start-hour snapshot (drive_weather, used by the recap prompt + start-strip) and the per-cadence time series (drive_weather_series, used by the drive- detail panel) for one drive. Designed for two callers: the API backfill / efficiency-analysis path, and the live recorder's post-close hook.

Returns the snapshot so the recap path can render it inline; nil without error when the drive has no usable start fix. Each upstream call is given its own bounded timeout so a slow provider can't pin a recorder goroutine. A series-fetch failure after a successful snapshot fetch is logged at the call site but does not roll back the snapshot — the recap can still render with start-hour data while the chart stays empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL