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:
- 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.
- The whole feature is gated behind an opt-in operator setting (recap.weather_enabled). Default off.
- 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
- func Bearing(lat1, lon1, lat2, lon2 float64) float64
- func Coarsen(lat, lon float64) (float64, float64)
- func Headwind(windKPH, windFromDeg, tripBearingDeg float64) float64
- type Cache
- func (c *Cache) Get(ctx context.Context, uid uuid.UUID, driveID string) (*Snapshot, error)
- func (c *Cache) GetSeries(ctx context.Context, uid uuid.UUID, driveID string) ([]SeriesRow, error)
- func (c *Cache) Put(ctx context.Context, uid uuid.UUID, driveID string, lat, lon float64, ...) error
- func (c *Cache) PutSeries(ctx context.Context, uid uuid.UUID, driveID string, lat, lon float64, ...) error
- type Client
- type MemCache
- type SeriesRow
- type Snapshot
Constants ¶
const CoarseRound = 0.1
CoarseRound is the lat/lon rounding step in degrees applied before the upstream request. 0.1 ~= 11 km.
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 ¶
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 Headwind ¶
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 (*Cache) Get ¶
Get returns the cached snapshot for (uid, driveID), or (nil, nil) when there's no row.
func (*Cache) GetSeries ¶
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 (*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
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.
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.