Documentation
¶
Overview ¶
Package samples persists raw vehicle_state polling snapshots.
Index ¶
- type DayActivity
- type Factory
- type PartitionJanitor
- type Sample
- type Store
- func (s *Store) Close() error
- func (s *Store) Count(ctx context.Context) (int, error)
- func (s *Store) InsertBatch(ctx context.Context, batch []Sample) error
- func (s *Store) ListAll(ctx context.Context) ([]Sample, error)
- func (s *Store) ListBetween(ctx context.Context, since, until time.Time, limit int) ([]Sample, error)
- func (s *Store) ListSince(ctx context.Context, since time.Time, limit int) ([]Sample, error)
- func (s *Store) Reset(ctx context.Context) (int64, error)
- func (s *Store) SleepActivity(ctx context.Context, vehicleID string, since, until time.Time) ([]DayActivity, error)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type DayActivity ¶ added in v0.30.0
type DayActivity struct {
Day time.Time `json:"day"`
AsleepH float64 `json:"asleep_h"`
IdleAwakeH float64 `json:"idle_awake_h"`
// Sleeps counts the times the car fell asleep that day;
// FailedSleeps the ones that ended within a minute. A healthy car
// sleeps a few times a day for hours; a high failed share means
// something keeps waking it, which is usually what drives
// IdleAwakeH up.
Sleeps int `json:"sleeps"`
FailedSleeps int `json:"failed_sleeps"`
}
DayActivity is one calendar day's (UTC) power-state breakdown, in hours, derived from the persisted power_state column (migration 0038). IdleAwakeH is the interesting one: the car awake while parked and not actively charging — i.e. staying awake for no obvious reason, the vampire-drain signal. Driving and active charging are excluded (both are expected awake time). Only intervals with a recorded power_state contribute, so the series fills forward from the deploy.
type Factory ¶
type Factory struct {
// contains filtered or unexported fields
}
Factory hands out per-user *Store instances. See drives.Factory for the rationale.
func NewFactory ¶
func NewFactory(d *sql.DB, resolvers *db.VehicleResolverFactory) *Factory
NewFactory wraps the shared pool + resolver factory.
type PartitionJanitor ¶
type PartitionJanitor struct {
// contains filtered or unexported fields
}
PartitionJanitor keeps `vehicle_state` ahead of the clock by at least `lookaheadMonths` partitions. It does this by calling the `rivolt_ensure_vehicle_state_partition(ts)` SQL helper once per target month.
func NewPartitionJanitor ¶
func NewPartitionJanitor(db *sql.DB) *PartitionJanitor
NewPartitionJanitor wires a janitor with sensible defaults. 3 months of lookahead is enough that a pod that runs continuously for a month still never writes into an unpartitioned range, and a pod that restarts hourly (deploys, node drains) has slack for clock skew. The interval is also 1 hour — cheap, boring, and avoids the "I deployed at 23:59 UTC on the last of the month and the janitor fired at 23:30" edge.
func (*PartitionJanitor) EnsureLookahead ¶
func (j *PartitionJanitor) EnsureLookahead(ctx context.Context) error
EnsureLookahead creates the current partition plus `lookaheadMonths` future partitions if they don't already exist. Idempotent; safe to call repeatedly. Returns the first error encountered but always attempts every month.
func (*PartitionJanitor) Run ¶
func (j *PartitionJanitor) Run(ctx context.Context)
Run blocks until ctx is cancelled, calling EnsureLookahead on an `interval` ticker. Intended to be launched as a dedicated goroutine at startup. Errors are logged, never returned — the next tick will retry.
type Sample ¶
type Sample struct {
VehicleID string
At time.Time
BatteryLevelPct float64
RangeMi float64
OdometerMi float64
Lat, Lon float64
// LocationFixAt is the timestamp Rivian's GNSS module reported on
// the (Lat, Lon) fix itself, NOT the wall-clock time the recorder
// observed it. Differs from At when the modem loses sky view and
// keeps replaying the last known fix — see migration 0013 for
// the field rationale and the Fort Stockton incident that
// motivated it. Pointer (rather than time.Time + omitempty)
// because Go's encoding/json doesn't honor omitempty on struct
// types: a zero time.Time would serialize as "0001-01-01T00:00:00Z"
// and the frontend would compute a 2000-year fix age. nil
// renders as "fix age unknown" upstream.
LocationFixAt *time.Time `json:",omitempty"`
SpeedMph float64
ShiftState string
DriveMode string `json:"drive_mode,omitempty"` // "everyday", "sport", "all-terrain", etc.; nullable in DB for legacy samples
ChargingState string
// PowerState is the car's power/sleep state ("sleep" | "ready" |
// "standby" | "go"). Empty on legacy rows / imports (NULL in DB).
// Feeds the sleep/activity aggregation.
PowerState string `json:"power_state,omitempty"`
ChargerPowerKW float64
ChargeLimitPct float64
InsideTempC float64
OutsideTempC float64
DriveNumber int64
ChargeNumber int64
Source string // "live" | "electrafi_import"
// AltitudeM is the elevation above sea level (meters) at
// (Lat, Lon), looked up by the live recorder against the
// Mapzen Terrarium DEM at sample-write time. Pointer because
// it's nullable: NULL on legacy rows written before migration
// 0019, on samples without a GPS fix, on ElectraFi imports,
// and on cold-cache misses where the tile fetch was kicked
// off async (the next sample on that tile will hit the cache).
AltitudeM *float64 `json:"altitude_m,omitempty"`
// TirePressureMinBar is the minimum of the four corner pressures
// at sample time, in bar. Pointer because nullable: NULL on
// legacy rows pre-migration 0024, ElectraFi imports, and live
// rows where Rivian reported zero on all four corners (sensor
// outage / vehicle parked w/ no recent reading). The efficiency
// analyzer pulls the median across a drive's samples; min-of-4
// keeps the worst tire (the one that actually hurts efficiency)
// dominant without bloating the schema with all four corners.
TirePressureMinBar *float64 `json:"tire_pressure_min_bar,omitempty"`
// PackTempAvg/Max/MinC are high-voltage battery cell temperatures
// (°C), merged in from the Parallax energy.high_voltage.battery_state
// topic. Pointers because nullable: NULL on legacy rows pre-migration
// 0036, imports, and live rows recorded before the vehicle pushed a
// battery_state frame. Avg/max/min kept separately so the spread
// (thermal imbalance) is queryable.
PackTempAvgC *float64 `json:"pack_temp_avg_c,omitempty"`
PackTempMaxC *float64 `json:"pack_temp_max_c,omitempty"`
PackTempMinC *float64 `json:"pack_temp_min_c,omitempty"`
}
Sample is a single polling snapshot. VehicleID is the Rivian gateway string id ("01-242521064"); the store resolves it to the internal vehicles.id UUID on write.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store wraps the vehicle_state table, scoped to one user.
func (*Store) ListAll ¶
ListAll returns every sample for this user, oldest first. Used by the Settings → Backup endpoint. Unbounded — a year of 60 s polls is on the order of 500k rows, ~100 MB JSON; acceptable for a full backup but NOT suitable for UI consumption.
func (*Store) ListBetween ¶
func (s *Store) ListBetween(ctx context.Context, since, until time.Time, limit int) ([]Sample, error)
ListBetween returns samples in (since, until], oldest first. Zero `until` means "no upper bound" (back-compat with ListSince). The upper bound matters for the drive detail page: without it, opening any drive ages back pulls every sample from the drive's start through now, which can be days of charging+parked rows.
func (*Store) ListSince ¶
ListSince returns samples newer than since, up to limit, oldest first.
Limit semantics:
- <= 0 falls back to a reasonable default (1000).
- Anything above the hard cap is clamped DOWN to the cap, not reset to the default. The previous "if limit > 10000 { limit = 1000 }" branch was a foot-gun: the UI asks for 20k samples so a 90-minute drive (~1.7k rows at the WS cadence) returns fully, but the store silently truncated to 1k → the route polyline ended halfway through the trip and the user reported "no GPS coordinates for the end of the route".
func (*Store) Reset ¶
Reset deletes every raw sample (vehicle_state row) for this store's user. See drives.Store.Reset.
func (*Store) SleepActivity ¶ added in v0.30.0
func (s *Store) SleepActivity(ctx context.Context, vehicleID string, since, until time.Time) ([]DayActivity, error)
SleepActivity attributes each inter-sample interval to the vehicle's state at its start and sums per UTC day over [since, until). Intervals are capped at 20 min so a data outage never counts as one giant block — steady-state cadence stays well under that even while the car sleeps (the Rivian cloud replays keep the sample stream dense). vehicleID scopes to one car; empty aggregates all of the user's vehicles.
idle_awake = awake (power_state<>'sleep') AND parked (gear not D/R/N) AND not actively charging (charging_state not charging_active / charging_connecting). That isolates unexplained awake time from normal driving / charging.
Charging is judged by charging_state alone, not charger_power_kw: until the recorder fix alongside this, the stored power kept the last session's value on every parked sample (see dropIdleChargerPower in internal/rivian), which made every awake hour look like charging and flattened idle-awake to zero. charging_state was always right, so keying on it corrects the history as well as new rows.