samples

package
v0.30.0 Latest Latest
Warning

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

Go to latest
Published: Jul 18, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package samples persists raw vehicle_state polling snapshots.

Index

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

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.

func (*Factory) For

func (f *Factory) For(userID uuid.UUID) *Store

For returns the *Store for userID, constructing one on first sight.

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 OpenStore

func OpenStore(d *sql.DB, userID uuid.UUID, v *db.VehicleResolver) (*Store, error)

OpenStore binds a pooled connection to a user + vehicle resolver.

func (*Store) Close

func (s *Store) Close() error

Close is a no-op; the pool is managed by main.

func (*Store) Count

func (s *Store) Count(ctx context.Context) (int, error)

Count returns the total number of samples for this user.

func (*Store) InsertBatch

func (s *Store) InsertBatch(ctx context.Context, batch []Sample) error

func (*Store) ListAll

func (s *Store) ListAll(ctx context.Context) ([]Sample, error)

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

func (s *Store) ListSince(ctx context.Context, since time.Time, limit int) ([]Sample, error)

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

func (s *Store) Reset(ctx context.Context) (int64, error)

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<>'charging_active' and no charger draw). That isolates unexplained awake time from normal driving / charging.

Jump to

Keyboard shortcuts

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