Documentation
¶
Overview ¶
Package rivian is Rivolt's adapter to the unofficial Rivian owner-app GraphQL API. The public surface is intentionally narrow so we can swap the implementation if the upstream breaks.
NOTE: this is a stub. Credentials flow, token refresh, and the real GraphQL calls are implemented in follow-up commits once we vendor or reimplement a community Rivian client in Go.
Index ¶
- Constants
- Variables
- func AwayFromHome(lat, lon float64, home HomeLookup) bool
- func BeginDrain(deadline time.Time)
- func ChargeETAFromState(st *State) (float64, string)
- func DemoVehicleID(uid uuid.UUID) string
- func Describe(err error) string
- func EncodePolyline(points [][2]float64) string
- func ErrCode(err error) string
- func FieldSupport(model string) map[string]string
- func InferPackKWh(model, trimID string, modelYear int) float64
- func InferPackKWhKnown(model, trimID string, modelYear int) (kwh float64, known bool)
- func IsAuthFlavoredReason(reason string) bool
- func IsBackground(ctx context.Context) bool
- func IsChargingState(chargerState string) bool
- func IsDCFCSession(peakKW, energyKWh, activeSeconds float64, span time.Duration) bool
- func IsNotAvailable(err error) bool
- func IsUpstream(err error, class ErrorClass) bool
- func OwnChargeETAMins(socPct, limitPct, packKWh, powerKW float64) float64
- func ParseBoolEnv(v string) bool
- func PickChargeETA(truckMins float64, validity string, socPct, limitPct, oursMins float64) (float64, string)
- func PlaceOf(lat, lon float64, home HomeLookup) string
- func QSessionChargerForTest() string
- func SampleTripPlanNames() []string
- func SampleUserPrice(startedAt time.Time) (cost float64, currency string, ok bool)
- func SessionLifetimeFromEnv(getenv func(string) string) time.Duration
- func SessionVersionFrom(ctx context.Context) time.Time
- func SetRoutePointObserver(f func(where string))
- func TireStatusLow(status string) bool
- func WithBackground(ctx context.Context) context.Context
- func WithPriority(ctx context.Context) context.Context
- type Account
- type AccountDBState
- type AccountRegistry
- type AccountStateSource
- type AccountSyncer
- func (s *AccountSyncer) Fresh(ctx context.Context, uid uuid.UUID) (AccountDBState, bool)
- func (s *AccountSyncer) OnChange(fn func(uuid.UUID, SyncChange))
- func (s *AccountSyncer) OnReconcile(fn func(Reconcile))
- func (s *AccountSyncer) Run(ctx context.Context, interval time.Duration, loaded func() []uuid.UUID)
- func (s *AccountSyncer) Superseded(ctx context.Context, uid uuid.UUID, usedSess string) bool
- func (s *AccountSyncer) SyncAll(ctx context.Context, uids []uuid.UUID)
- func (s *AccountSyncer) Touch(uid uuid.UUID)
- type BatteryCharacteristics
- type BatteryTemp
- type BatteryTempCallback
- type Breaker
- type BreakerConfig
- type BreakerObserver
- type BreakerState
- type ChargeCloseHook
- type ChargeEnergyObserver
- type ChargeNow
- type ChargingFrame
- type ChargingSchedule
- type ChargingStatus
- type Client
- type Credentials
- type DCFCDetector
- type DCFCEstimate
- type DCFCEstimator
- type DCFCPriceLookup
- type DCFCVerdict
- type DriveCloseHook
- type DriveDynamicsCallback
- type DriveDynamicsFrame
- type DriveLocationObserver
- type DynamicsGNSS
- type DynamicsGNSSCallback
- type ElevationLookup
- type ErrRateLimited
- type ErrorClass
- type FirstPlanner
- type FixturePlanner
- type FrameSource
- type GatedLiveStateStore
- func (g *GatedLiveStateStore) Delete(ctx context.Context, vehicleID string) error
- func (g *GatedLiveStateStore) GetState(ctx context.Context, vehicleID string) (*State, error)
- func (g *GatedLiveStateStore) Load(ctx context.Context, vehicleID string) (LiveStateSnapshot, bool, error)
- func (g *GatedLiveStateStore) PutState(ctx context.Context, vehicleID string, st *State, ttl time.Duration) error
- func (g *GatedLiveStateStore) Save(ctx context.Context, vehicleID string, snap LiveStateSnapshot, ...) error
- type HomeLookup
- type LiveChargeSnapshot
- type LiveClient
- func (c *LiveClient) AuthReady() <-chan struct{}
- func (c *LiveClient) Authenticated() bool
- func (c *LiveClient) ChargingFieldProbe(ctx context.Context, field, vehicleID string) (map[string]any, error)
- func (c *LiveClient) ChargingFieldProbeWithSelection(ctx context.Context, field, vehicleID, selection string) (map[string]any, error)
- func (c *LiveClient) ChargingSchedules(ctx context.Context, vehicleID string) ([]ChargingSchedule, error)
- func (c *LiveClient) ChargingSchemaProbe(ctx context.Context) (map[string]any, error)
- func (c *LiveClient) CompletedSessionSummaries(ctx context.Context) ([]SessionSummary, error)
- func (c *LiveClient) Email() string
- func (c *LiveClient) IntrospectEnum(ctx context.Context, typeName string) (map[string]any, error)
- func (c *LiveClient) IntrospectInputType(ctx context.Context, typeName string) (map[string]any, error)
- func (c *LiveClient) LiveSession(ctx context.Context, vehicleID string) (*LiveSession, error)
- func (c *LiveClient) Login(ctx context.Context, creds Credentials) error
- func (c *LiveClient) Logout()
- func (c *LiveClient) MFAPending() bool
- func (c *LiveClient) NeedsReauth() (bool, string)
- func (c *LiveClient) OTAUpdateDetails(ctx context.Context, vehicleID string) (*OTAInfo, error)
- func (c *LiveClient) PendingSnapshot() (PendingMFA, bool)
- func (c *LiveClient) PlanTrip(ctx context.Context, in PlanTripInput) (*TripPlan, error)
- func (c *LiveClient) PlanTripRaw(ctx context.Context, variables map[string]any) (map[string]any, error)
- func (c *LiveClient) ProbeBatteryTemperature(ctx context.Context, vehicleID string) (*BatteryTemp, error)
- func (c *LiveClient) ProbeParallaxRaw(ctx context.Context, vehicleID string, rvms []string, maxFrames int) ([]ParallaxRawFrame, error)
- func (c *LiveClient) RawGraphQL(ctx context.Context, operationName, query string, variables map[string]any) (map[string]any, error)
- func (c *LiveClient) RecentActions(vehicleID string, from, to time.Time) []RivoltAction
- func (c *LiveClient) RecentChargingFrames(vehicleID string) []ChargingFrame
- func (c *LiveClient) RecordChargingEvent(vehicleID, event, errMsg string)
- func (c *LiveClient) RecordChargingFrame(vehicleID string, raw []byte)
- func (c *LiveClient) RefreshSession(ctx context.Context) error
- func (c *LiveClient) Restore(s Session)
- func (c *LiveClient) RestorePending(p PendingMFA)
- func (c *LiveClient) SessionCharger(ctx context.Context, transactionID string) (*SessionCharger, error)
- func (c *LiveClient) SessionVersion() time.Time
- func (c *LiveClient) SessionVersionFor(usedToken string) time.Time
- func (c *LiveClient) SetNeedsReauth(needs bool, reason string)
- func (c *LiveClient) SetSessionVersion(v time.Time)
- func (c *LiveClient) Snapshot() Session
- func (c *LiveClient) State(ctx context.Context, vehicleID string) (*State, error)
- func (c *LiveClient) StateRaw(ctx context.Context, vehicleID string) (map[string]any, error)
- func (c *LiveClient) SubscribeBatteryState(ctx context.Context, vehicleID string, cb BatteryTempCallback) error
- func (c *LiveClient) SubscribeChargingSession(ctx context.Context, vehicleID string, cb LiveSessionCallback) error
- func (c *LiveClient) SubscribeDriveDynamics(ctx context.Context, vehicleID string, extra func() []string, ...) error
- func (c *LiveClient) SubscribeDynamicsGNSS(ctx context.Context, vehicleID string, cb DynamicsGNSSCallback) error
- func (c *LiveClient) SubscribeParallaxCharging(ctx context.Context, vehicleID string, cb LiveSessionCallback) error
- func (c *LiveClient) SubscribeVehicleState(ctx context.Context, vehicleID string, cb StateCallback) error
- func (c *LiveClient) SubscribeVehicleStateHeld(ctx context.Context, vehicleID string, hold ReconnectHold, cb StateCallback) error
- func (c *LiveClient) SupportedFeatures(ctx context.Context) (map[string]map[string]string, error)
- func (c *LiveClient) VehicleImages(ctx context.Context) ([]VehicleImage, error)
- func (c *LiveClient) VehicleSupportsParallax(ctx context.Context, vehicleID string) (bool, error)
- func (c *LiveClient) Vehicles(ctx context.Context) ([]Vehicle, error)
- func (c *LiveClient) WithBreaker(b *Breaker) *LiveClient
- func (c *LiveClient) WithEndpoint(url string) *LiveClient
- func (c *LiveClient) WithHTTPClient(h *http.Client) *LiveClient
- func (c *LiveClient) WithRateLimit(rl RateLimiter) *LiveClient
- func (c *LiveClient) WithReauthGuard(superseded func(ctx context.Context, usedSess string) bool) *LiveClient
- func (c *LiveClient) WithReauthSink(sink func(ctx context.Context, reason string)) *LiveClient
- func (c *LiveClient) WithResultObserver(o ResultObserver) *LiveClient
- func (c *LiveClient) WithRivoltVersion(v string) *LiveClient
- func (c *LiveClient) WithUpstreamGate(gate func(ctx context.Context) error) *LiveClient
- type LiveDrive
- type LiveDriveSnapshot
- type LiveSession
- type LiveSessionCallback
- type LiveStatePurger
- type LiveStateSnapshot
- type LiveStateStore
- type MockClient
- func (c *MockClient) AuthReady() <-chan struct{}
- func (c *MockClient) Authenticated() bool
- func (c *MockClient) Email() string
- func (c *MockClient) Login(_ context.Context, cr Credentials) error
- func (c *MockClient) Logout()
- func (c *MockClient) MFAPending() bool
- func (c *MockClient) NeedsReauth() (bool, string)
- func (c *MockClient) Restore(s Session)
- func (c *MockClient) SetState(vehicleID string, s State)
- func (c *MockClient) SetVehicles(vs []Vehicle)
- func (c *MockClient) Snapshot() Session
- func (c *MockClient) State(_ context.Context, vehicleID string) (*State, error)
- func (c *MockClient) Vehicles(_ context.Context) ([]Vehicle, error)
- type MonitorRegistry
- func (r *MonitorRegistry) AddChargeCloseHookFactory(factory func(uid uuid.UUID) ChargeCloseHook)
- func (r *MonitorRegistry) AddDriveCloseHookFactory(factory func(uid uuid.UUID) DriveCloseHook)
- func (r *MonitorRegistry) AllVehicleInfo() []Vehicle
- func (r *MonitorRegistry) EnsureSubscribed(rivianVehicleID string)
- func (r *MonitorRegistry) FirstFrameAt(vehicleID string) time.Time
- func (r *MonitorRegistry) FlushPending(ctx context.Context) int
- func (r *MonitorRegistry) For(uid uuid.UUID) *StateMonitor
- func (r *MonitorRegistry) LateWirings() int
- func (r *MonitorRegistry) LatestAt(vehicleID string) time.Time
- func (r *MonitorRegistry) RequireFence(on bool)
- func (r *MonitorRegistry) SessionActivity(rivianVehicleID string) (open, asleep bool)
- func (r *MonitorRegistry) SetBatteryCapacityHookFactory(...)
- func (r *MonitorRegistry) SetChargeCloseHookFactory(factory func(uid uuid.UUID) ChargeCloseHook)
- func (r *MonitorRegistry) SetDCFCDetectorFactory(factory func(uid uuid.UUID) DCFCDetector)
- func (r *MonitorRegistry) SetDCFCEstimatorFactory(factory func(uid uuid.UUID) DCFCEstimator)
- func (r *MonitorRegistry) SetDCFCPriceLookupFactory(factory func(uid uuid.UUID) DCFCPriceLookup)
- func (r *MonitorRegistry) SetDriveCloseHookFactory(factory func(uid uuid.UUID) DriveCloseHook)
- func (r *MonitorRegistry) SetElevationLookup(e ElevationLookup)
- func (r *MonitorRegistry) SetFence(src fence.Source, onFenced func(kind string))
- func (r *MonitorRegistry) SetLiveStateStoreFactory(factory func(uid uuid.UUID) LiveStateStore)
- func (r *MonitorRegistry) SetParent(ctx context.Context)
- func (r *MonitorRegistry) SetRouteFiller(rf RouteFiller)
- func (r *MonitorRegistry) SetScheduleObserver(fn func(outcome string))
- func (r *MonitorRegistry) SetSessionRehydrator(fn func(ctx context.Context, uid uuid.UUID) bool)
- func (r *MonitorRegistry) SetSleepObserver(o SleepObserver)
- func (r *MonitorRegistry) SetSleepSafe(fn func(vehicleID string) bool)
- func (r *MonitorRegistry) SetVehicleInfoHookFactory(factory func(uid uuid.UUID) func(context.Context, []Vehicle))
- func (r *MonitorRegistry) SetWakeSafety(fn func(vehicleID string) WakeSafety)
- func (r *MonitorRegistry) Start(ctx context.Context, uid uuid.UUID) *StateMonitor
- func (r *MonitorRegistry) Stop(uid uuid.UUID)
- func (r *MonitorRegistry) Streaming(rivianVehicleID string) bool
- func (r *MonitorRegistry) Unsubscribe(rivianVehicleID string)
- type NetworkPreference
- type OTAInfo
- type OTAObserver
- type OTASource
- type OTAVersion
- type ParallaxRawFrame
- type ParallaxReplayObserver
- type PendingMFA
- type PlanFixture
- type PlanTripInput
- type PlanTripWaypoint
- type PlannedWaypoint
- type PriceLookup
- type RateLimiter
- type ReceiptSource
- type Reconcile
- type ReconnectHold
- type RedisLiveStateStore
- func (s *RedisLiveStateStore) Delete(ctx context.Context, vehicleID string) error
- func (s *RedisLiveStateStore) GetState(ctx context.Context, vehicleID string) (*State, error)
- func (s *RedisLiveStateStore) Load(ctx context.Context, vehicleID string) (LiveStateSnapshot, bool, error)
- func (s *RedisLiveStateStore) PutState(ctx context.Context, vehicleID string, st *State, ttl time.Duration) error
- func (s *RedisLiveStateStore) Save(ctx context.Context, vehicleID string, snap LiveStateSnapshot, ...) error
- type ReplayClient
- func (c *ReplayClient) AuthReady() <-chan struct{}
- func (c *ReplayClient) Authenticated() bool
- func (c *ReplayClient) Email() string
- func (c *ReplayClient) Login(_ context.Context, cr Credentials) error
- func (c *ReplayClient) Logout()
- func (c *ReplayClient) MFAPending() bool
- func (c *ReplayClient) NeedsReauth() (bool, string)
- func (c *ReplayClient) PlanTrip(ctx context.Context, in PlanTripInput) (*TripPlan, error)
- func (c *ReplayClient) Restore(s Session)
- func (c *ReplayClient) Snapshot() Session
- func (c *ReplayClient) State(ctx context.Context, vehicleID string) (*State, error)
- func (c *ReplayClient) Stream(ctx context.Context, vehicleID string, emit func(st State, catchingUp bool)) error
- func (c *ReplayClient) Vehicles(context.Context) ([]Vehicle, error)
- type ReplayCorpus
- type ReplayFeed
- type Replayer
- type ResultObserver
- type RivoltAction
- type RouteFiller
- type RoutePreferences
- type SampleCorpus
- type SamplePlace
- type SampleSummary
- type SampleTripPlan
- type Session
- type SessionCharger
- type SessionSummary
- type SleepObserver
- type Stagger
- type State
- type StateCallback
- type StateMonitor
- func (m *StateMonitor) ActiveDrive(vehicleID string) *LiveDrive
- func (m *StateMonitor) AllVehicleInfo() []Vehicle
- func (m *StateMonitor) EnsureSubscribed(vehicleID string)
- func (m *StateMonitor) FirstFrameAt(vehicleID string) time.Time
- func (m *StateMonitor) FlushPending(ctx context.Context) int
- func (m *StateMonitor) Latest(vehicleID string) (*State, time.Time)
- func (m *StateMonitor) LatestLiveSession(vehicleID string) *LiveSession
- func (m *StateMonitor) LiveSessionForCharge(vehicleID string) (*LiveSession, ChargeNow)
- func (m *StateMonitor) PackKWhFor(vehicleID string) float64
- func (m *StateMonitor) Prime(vehicleID string, st *State)
- func (m *StateMonitor) RefreshVehicleInfo(ctx context.Context) error
- func (m *StateMonitor) RemoteLatest(ctx context.Context, vehicleID string) *State
- func (m *StateMonitor) ServeState(ctx context.Context, vehicleID string) (st *State, fetch bool)
- func (m *StateMonitor) SessionActivity(vehicleID string) (open, asleep bool)
- func (m *StateMonitor) SetBatteryCapacityHook(hook func(ctx context.Context, vehicleID string, kwh float64))
- func (m *StateMonitor) SetChargeCloseHook(h ChargeCloseHook)
- func (m *StateMonitor) SetDCFCDetector(fn DCFCDetector)
- func (m *StateMonitor) SetDCFCEstimator(fn DCFCEstimator)
- func (m *StateMonitor) SetDCFCPriceLookup(fn DCFCPriceLookup)
- func (m *StateMonitor) SetDriveCloseHook(h DriveCloseHook)
- func (m *StateMonitor) SetElevationLookup(e ElevationLookup)
- func (m *StateMonitor) SetFence(src fence.Source, onFenced func(kind string))
- func (m *StateMonitor) SetHomeLookup(fn HomeLookup)
- func (m *StateMonitor) SetLiveStateStore(s LiveStateStore)
- func (m *StateMonitor) SetPriceLookup(fn PriceLookup)
- func (m *StateMonitor) SetRouteFiller(r RouteFiller)
- func (m *StateMonitor) SetScheduleObserver(fn func(outcome string))
- func (m *StateMonitor) SetSleepObserver(o SleepObserver)
- func (m *StateMonitor) SetSleepSafe(fn func(vehicleID string) bool)
- func (m *StateMonitor) SetStagger(s *Stagger)
- func (m *StateMonitor) SetStores(samplesStore *samples.Store, drivesStore *drives.Store, ...)
- func (m *StateMonitor) SetVehicleInfoHook(h func(context.Context, []Vehicle))
- func (m *StateMonitor) SetWakeSafety(fn func(vehicleID string) WakeSafety)
- func (m *StateMonitor) Start(ctx context.Context)
- func (m *StateMonitor) Subscribed(vehicleID string) bool
- func (m *StateMonitor) Unsubscribe(vehicleID string)
- func (m *StateMonitor) VehicleInfo(vehicleID string) *Vehicle
- func (m *StateMonitor) VehicleStateFrameAt(ctx context.Context, vehicleID string) *time.Time
- type StubClient
- type SyncChange
- type TireObserver
- type TirePressure
- type TripPlan
- type TripPlanner
- type TripRoute
- type UpstreamError
- type Vehicle
- type VehicleImage
- type WakeSafety
Constants ¶
const ( // ETASourceTruck: the minutes are the truck's own (shown without "est."). ETASourceTruck = "truck" // ETASourceEstimate: the minutes are ours (shown with "est."). ETASourceEstimate = "estimate" )
const ( PlaceHome = "home" PlaceAway = "away" )
Values of charges.Charge.Place (migration 0044).
const ( CostSourceRivian = "rivian" CostSourceHomeRate = "home_rate" CostSourceNetworkEstimate = "network_estimate" CostSourceObservedEstimate = "observed_estimate" CostSourceUser = "user" )
Values of charges.Charge.CostSource (migration 0040).
const ( SamplePackKWh = samplePackKWh SampleMiPerKWh = sampleMiPerKWh )
The sample truck's pack size and efficiency, for planners that model its energy use.
const ( SamplePlanVegasTwoDay = "Las Vegas, 2 days" // SampleHotelLabel names the overnight stop; SampleHotelKW is its L2. SampleHotelLabel = "Las Vegas hotel (sample)" SampleHotelKW = sampleHotelKW // SampleOvernightMaxSoC caps the overnight charge, as the owner's // charge limit did at the hotel. SampleOvernightMaxSoC = 90.0 )
The sample owner's two-day plan for the Las Vegas trip: the way out as day 1, a night at the hotel on its L2, and the way home as day 2.
const ( TrailerPresent = "present" TrailerAbsent = "not_present" )
Trailer states carried in State.TrailerState. "" is unknown.
const ( // FieldReported: the vehicle sends it and we show it. FieldReported = "reported" // FieldEstimate: derived or best guess; the UI labels it "(estimate)". FieldEstimate = "estimate" // FieldBestEffort: usually there, can lag or be missing for a while. FieldBestEffort = "best_effort" FieldUnavailable = "unavailable" )
Field support levels, as /api/vehicles reports them in field_support.
const ChargeWallToPackFactor = 1.08
ChargeWallToPackFactor is how much more energy the wall delivers than the pack gains, the charging losses: the median across clean AC charges (R1 and R2) is about 1.08. The estimate uses it so it does not understate; the repair of past rows uses the same factor when a vehicle has too few clean charges of its own.
const DCFCMinEffectiveKW = 20.0
DCFCMinEffectiveKW is the energy-over-charging-time floor a session must also clear. Recorded peaks glitch: prod has 15-hour home L2 sessions with a single "145 kW" sample. Real DC fast charging averages well above any AC charger's ~11-19 kW.
const DCFCMinKW = 50.0
isDrivingGear is true for any non-park gear. Empty ("") is treated as parked so a missing/unknown value doesn't spuriously open a drive session on startup. DCFCMinKW is the session peak power at which a charge counts as DC fast charging — the same line analytics.DefaultParams draws for the Fast bucket (no AC L2 hardware exceeds ~22 kW).
const DCFCMinKWh = 1.0
DCFCMinKWh is the least energy a session must deliver to count as DC fast charging. A few-second fragment of a home session can carry a spurious high-kW sample, and over 4 s even 0.12 kWh averages ~110 kW (seen on preview), so the effective-power test alone can't reject it. A real fast-charging stop delivers tens of kWh; anything under 1 kWh isn't worth pricing as one.
const DefaultAccept = "application/json"
DefaultAccept stays application/json. The iOS multipart/mixed value makes the gateway @defer fields like gnssLocation into separate parts our single-shot json.Unmarshal can't reassemble.
const DefaultChargingEndpoint = "https://rivian.com/api/gql/chrg/user/graphql"
DefaultChargingEndpoint hosts getLiveSessionHistory and friends.
const DefaultClientName = "com.rivian.android.consumer"
DefaultClientName identifies us as the Android Owner App. The iOS client name triggers server-side @defer of gnssLocation on the WS subscription, which our single-shot frame parser drops.
const DefaultClientVersion = "3.6.0-3989"
DefaultClientVersion is paired with DefaultClientName; keep them in sync to avoid a never-shipped hybrid fingerprint.
const DefaultEndpoint = "https://rivian.com/api/gql/gateway/graphql"
DefaultEndpoint is the Rivian Owner App GraphQL gateway. Unofficial.
const DefaultPackKWh = 131.0
DefaultPackKWh is the usable capacity we fall back to when trim / model are unknown. It matches the R1T/R1S Gen 1 Large pack (131 kWh usable per Rivian spec) and is a reasonable default for Rivolt's current owner base.
const DefaultUserAgent = "RivianApp/4400 CFNetwork/1498.700.2 Darwin/23.6.0"
DefaultUserAgent matches the iOS app verbatim; the gateway 403s on modified UAs.
const FeatureConnectivityParallax = "VEHICLE_CONNECTIVITY_PARALLAX"
FeatureConnectivityParallax is the API `name` value gating whether a vehicle does Parallax connectivity at all. This is the right gate for cherry-picking individual Parallax topics (e.g. dynamics.vehicle.gnss for GPS): AVAILABLE means the topics stream, even when the full vehicle-state migration (PX_STATE_ALL) hasn't been enabled.
const FeatureParallaxVehicleState = "PX_STATE_ALL"
FeatureParallaxVehicleState is the API `name` value for the *full* Parallax vehicle-state migration — replacing legacy vehicleState with Parallax RVMs wholesale. The app models it as the enum PARALLAX_VEHICLE_STATE; the wire value is "PX_STATE_ALL". Not required for consuming a single topic (see FeatureConnectivityParallax): a vehicle streams dynamics.vehicle.gnss with this absent.
const FeatureStatusAvailable = "AVAILABLE"
FeatureStatusAvailable is the only supportedFeatures status the app treats as "on" (per UserVehicleKt.isFeatureSupported).
const HomeRadiusM = 250.0
HomeRadiusM is how far from the saved home location a charge can start and still count as charging at home. Shared with the ledger so the recorder's pricing and the Money tab's Home/Away agree.
const LiveStateSnapshotTTL = 90 * time.Second
LiveStateSnapshotTTL bounds how long a published live State snapshot lives in the shared store after the last refresh. Much shorter than LiveStateTTL because a snapshot is a live-view convenience, not durable session state: when the owning pod dies, the snapshot should age out within a couple of publish intervals so peer replicas fall back to REST rather than serving a stale frame indefinitely.
const LiveStateTTL = 2 * time.Hour
LiveStateTTL is how long a snapshot lingers in Redis after the last upsert. Sized to ~2× liveDriveMaxGap so a graceful pod-handoff window comfortably covers WS reconnect cadence; the recorder's own stale-session guard at liveDriveMaxGap will reject any rehydrated state whose endAt is clearly out of date.
const MaxDepartureAhead = 30 * 24 * time.Hour
MaxDepartureAhead is how far in the future a planned departure may be. The gateway is sent only departures a person could plausibly pick in the app, so a request for a far-future date is refused here rather than forwarded.
const MaxPlanWaypoints = 10
MaxPlanWaypoints is the gateway's limit on waypoints in one planTrip call, counting the origin and the destination.
const NetworkRivianAdventure = "Rivian Adventure Network"
NetworkRivianAdventure is the display name the ledger and the charge map use for the Rivian Adventure Network.
const SampleSeed int64 = 20261014
SampleSeed is the seed the demo uses.
const SessionLifetime = 180 * 24 * time.Hour
SessionLifetime is how long Rivian keeps a user session (u-sess) after sign-in. The 3.17.0 app has no way to extend one (no refresh or session-extension call), and the owner's session ended 180 days and about 15 minutes after it was made, so Rivolt treats it as a fixed 180 days from the sign-in (Session.AuthenticatedAt). The number is configurable at the sweep (RIVOLT_RIVIAN_SESSION_DAYS) because it is observed, not documented.
Variables ¶
var CaptureRVMs = []string{
rvmTrailer,
rvmLowVoltBatt, rvmOtaState, rvmAlarmState, rvmPreconStatus,
rvmPetMode, rvmClimateHold, rvmImmobilizer,
}
CaptureRVMs are the read topics carried on the drive-dynamics subscription only under the preview capture flag, so prod isn't subscribed to them yet. alarm + 12V health are decoded from the APK enums and applied on preview (with the concurrent vehicleState value logged for verification); trailer + precon are decode-logged only (no State field / unconfirmed field number); ota_state is still on the raw default case. Graduate each into the base list above once the logs confirm the mapping (ota.deployment.state has: decodeOTADeployment).
var ConfiguredSessionLifetime = SessionLifetime
ConfiguredSessionLifetime is the lifetime in force: SessionLifetime unless the operator set RIVOLT_RIVIAN_SESSION_DAYS (cmd/rivolt, at boot, before anything reads it).
var ErrInvalidTrip = errors.New("invalid trip")
ErrInvalidTrip marks a trip request the planner cannot accept (the number of waypoints, a missing starting SoC). The handlers answer it with a 400.
var ErrMFARequired = errors.New("rivian: MFA code required")
ErrMFARequired signals an OTP challenge; resubmit Login on the same client with Credentials.OTP populated.
var ErrNeedsReauth = errors.New("rivian: re-authentication required")
ErrNeedsReauth is returned when the per-user needs_reauth flag is set. Cleared on a successful Login.
var ErrNoFixture = errors.New("rivian: no stored plan matches this trip")
ErrNoFixture means no stored plan matches the request.
var ErrNoPlanner = errors.New("rivian: no trip planner on this install")
ErrNoPlanner means this replay install has no trip planner configured.
var ErrNotAuthenticated = errors.New("rivian: not authenticated; call Login first")
ErrNotAuthenticated means the caller invoked a data method before Login succeeded (or after the session was cleared).
var ErrNotImplemented = errors.New("rivian: client not implemented yet")
ErrNotImplemented is returned by the stub. Every method that hits the network returns this until we land the real client. Callers should treat ErrNotImplemented as "fine for boot, unusable at runtime."
var ErrReplayNotAllowed = errors.New("replay: demo vehicle not enabled for this account")
ErrReplayNotAllowed is returned by ReplayClient.Login for a user the install hasn't allowed to connect a demo vehicle.
var ErrUpstreamBreakerOpen = errors.New("rivian upstream breaker open")
ErrUpstreamBreakerOpen is returned by the upstream gate when the circuit breaker has tripped. It satisfies errors.Is for the same "don't bother retrying right now" semantics as ErrUpstreamPaused — the api layer maps both to a 503-with-Retry-After.
var ErrUpstreamPaused = errors.New("rivian: upstream paused by operator")
ErrUpstreamPaused is returned when the operator kill switch is on.
var ErrVehicleNotFound = errors.New("rivian: vehicle not found")
ErrVehicleNotFound means the caller asked for state on a vehicle ID the client does not know about.
var RecorderWriteFailureObserver func(kind string)
RecorderWriteFailureObserver, when set (once, at startup, before serving), is told about every failed recorder write by kind ("drive_upsert", "charge_upsert", "stale_charge"); main counts them. Never given an id.
var ( // SampleHome is the sample truck's "home": the Rose Bowl Stadium's // public parking (Pasadena, CA). Non-residential by design. SampleHome = samplePoint{34.1613, -118.1676} )
Places: public landmarks only.
var WSFrameDropObserver func()
WSFrameDropObserver, when set (once, at startup, before serving), is told each time a received "next" frame is dropped because its subscription's channel was full; main counts them. Never given an id.
Functions ¶
func AwayFromHome ¶ added in v0.32.0
func AwayFromHome(lat, lon float64, home HomeLookup) bool
AwayFromHome reports whether (lat, lon) is known to be more than HomeRadiusM from the saved home location. False when either the home location or the position is unknown.
func BeginDrain ¶ added in v0.37.7
BeginDrain starts the shutdown drain: from now on the per-vehicle flushes end at deadline.
func ChargeETAFromState ¶ added in v0.39.1
ChargeETAFromState is the rule applied to a vehicle's latest State (the Live Activity's source).
func DemoVehicleID ¶ added in v0.32.0
DemoVehicleID is the rivian_vehicle_id of uid's demo vehicle. Unique per user: leases and owner resolution key on it install-wide.
func Describe ¶ added in v0.36.6
Describe renders an error for a log line or a stored last_error: for an upstream error only its classification (operation, HTTP status, class, the GraphQL code, our reason phrase), never the cause, which can carry a slice of a response body or Rivian's message text. Context and session errors keep their text, as do our own gates (rate limit, breaker, operator pause); anything else is "error" with the Go type of the root cause.
func EncodePolyline ¶ added in v0.33.0
encodePolyline encodes a slice of (lat, lon) points using Google's polyline algorithm at precision 5 (~1.1m resolution). The result is the same format consumed by Leaflet's various polyline-decoding helpers and by Google Maps; see https://developers.google.com/maps/documentation/utilities/polylinealgorithm
We use it to persist a drive's GPS path on the drives row so the overview map can render a real on-road route instead of a straight line between start/end markers.
The input is expected to be an already-thinned trace (one point per recorder frame). At ~1 frame / few seconds and 5-decimal varint encoding this comes out to well under 10 KB even for multi-hour drives. EncodePolyline is encodePolyline for repair tools that rebuild a drive's route from stored samples.
func ErrCode ¶ added in v0.36.6
ErrCode returns the GraphQL extensions.code of an upstream error (a short identifier such as "FORBIDDEN"), or "". It is only ever the code, never the error message, which can carry response text.
func FieldSupport ¶ added in v0.37.0
FieldSupport says, per model, which live fields can be trusted, so the UI labels and hides from one place instead of checking models. One table to edit when a field is confirmed on a model or an R2 software change moves it (the evidence is in scratchpad r2-verified-design.md). Unknown models are treated as R1s. The map is a fresh copy.
func InferPackKWh ¶
InferPackKWh maps a (model, trim-option-id, modelYear) tuple to the best-known USABLE battery capacity in kWh, to seed a truck until it reports its own (the vehicle's batteryCapacity replaces this on its first frame, and that is what drives and overnight charging use). The GraphQL API exposes model / modelYear / trimOption.optionId but NOT pack size. Unknown combinations return DefaultPackKWh.
The figures are usable, not gross. Rivian does not publish a usable figure; these are the numbers owners and the press measured and compiled (Rivian Forums "battery size Gen 1 vs Gen 2", InsideEVs on the Max pack, 2026-10):
Gen 1 usable (gross) Gen 2 usable Standard 106 (105) 92.5 Standard+ 121 (92.5, named "Standard+" in some trim ids) Large 131 (135) 109.4 Max 141 (149) 141.5
An earlier version had Max at 180 kWh, which is neither the usable (141) nor the gross (149) figure, and Gen 2 Large at 141.5 (that is the Max pack). Gen 1 Standard is 105: sources give 105 and 106 for it, and a usable figure cannot exceed the gross one, so the lower, consistent value is used.
The optionIds seen on real trucks (prod, 2026-10) are "PKG-LCH", "PKG-ADV", "PKG-ASC", "BLD-DR1T", "BLD-TR1S" and "BLD-QR1S": they do not encode the pack, so most trucks reach the model-only default below. For a Gen 2 R1 that default is the Max figure, and a Standard or Large Gen 2 owner sees it until the truck reports its own capacity. Pack-size tokens in the trim ("MAX", "LRG", "STD") win when present.
Model year >= 2025 or a trim prefix of "G2-" indicates Gen 2.
func InferPackKWhKnown ¶ added in v0.37.7
InferPackKWhKnown is InferPackKWh plus whether the pack was actually identified: true when a pack token in the trim matched, or the trim is the R2's one seen on real trucks (BLD-PRF2); false for the model-only default and for an R2 trim not seen yet. Callers that show the figure as the truck's "spec" must use it, because a guessed default beside the truck's own reported capacity reads as a degradation that is not there (a Gen 2 Standard owner would see 141.5 kWh beside a reported ~92).
func IsAuthFlavoredReason ¶ added in v0.20.0
IsAuthFlavoredReason reports whether a UserAction reason names a real auth problem (expired session, rejected credentials, MFA challenge, locked account) versus a generic 4xx like "HTTP 404" or "HTTP 422" that ClassifyHTTP's catch-all also routes through ClassUserAction. Used to gate needs_reauth: a stray 404 should not poison the user's session, but a 401 should.
Keep this aligned with the reasons emitted by classifyFromBody + statusReason for 401/403 + ClassifyGraphQL — every reason string any of those produce for ClassUserAction must appear here.
func IsBackground ¶ added in v0.40.14
IsBackground reports whether ctx carries the background class (tests in the syncer packages check it).
func IsChargingState ¶ added in v0.38.15
IsChargingState reports whether a charger_state means energy is flowing (or the truck is waiting on the charger), the same test the recorder opens a charge on.
func IsDCFCSession ¶ added in v0.32.0
IsDCFCSession reports whether a charge was DC fast charging: peak at or above DCFCMinKW, at least DCFCMinKWh delivered, AND energy divided by active charging time (wall span when that's unknown) at or above DCFCMinEffectiveKW.
func IsNotAvailable ¶ added in v0.36.6
IsNotAvailable reports an upstream answer that says "not for this vehicle or account": a FORBIDDEN or not-found/unsupported code, or HTTP 403, 404, 405 or 501. It is an expected outcome (shared or authorized-driver accounts, a model without the feature), not a failure to retry daily.
func IsUpstream ¶
func IsUpstream(err error, class ErrorClass) bool
IsUpstream reports whether err is an *UpstreamError with the given class. Convenience for call sites that only care about one class (e.g. "is this a user-action? flip needs_reauth").
func OwnChargeETAMins ¶ added in v0.39.1
OwnChargeETAMins is our straight-line estimate: the energy still to add up to the charge limit, at the current power, in whole minutes (at least 1). 0 when anything is unknown or the charge is already at the limit.
func ParseBoolEnv ¶ added in v0.32.0
ParseBoolEnv is parseBoolEnv for other packages' env flags (main's live-client guard), so every switch reads truthiness the same way.
func PickChargeETA ¶ added in v0.39.1
func PickChargeETA(truckMins float64, validity string, socPct, limitPct, oursMins float64) (float64, string)
PickChargeETA chooses what to show. truckMins is the truck's figure (0 when it sent none), validity its charging_time_estimation_validity ("invalid" hides it), limitPct the charge limit (0 when unknown) and oursMins OwnChargeETAMins (0 when none). It returns the minutes and their source, or (0, "") when there is nothing to show.
func PlaceOf ¶ added in v0.32.0
func PlaceOf(lat, lon float64, home HomeLookup) string
PlaceOf classifies (lat, lon) against the saved home location: PlaceHome within HomeRadiusM, PlaceAway beyond it, "" when either the home location or the position is unknown.
func QSessionChargerForTest ¶ added in v0.36.4
func QSessionChargerForTest() string
QSessionChargerForTest exposes the detail document to the privacy test in internal/receipts.
func SampleTripPlanNames ¶ added in v0.34.0
func SampleTripPlanNames() []string
SampleTripPlanNames are the names of the sample owner's saved plans.
func SampleUserPrice ¶ added in v0.33.0
SampleUserPrice reports whether the sample truck's charge that started at startedAt (wall clock, as the replay feed recorded it) is the one its owner priced by hand (the Las Vegas hotel's L2, a flat $15), so the demo seeder can apply that price through the normal user-pricing path after the backfill: a price only a person can enter, which the recorder must not invent. Deterministic: startedAt is mapped back onto the looping corpus like the replay feed maps it forward.
func SessionLifetimeFromEnv ¶ added in v0.40.8
SessionLifetimeFromEnv reads RIVOLT_RIVIAN_SESSION_DAYS (1 to 3650 days); anything else is SessionLifetime.
func SessionVersionFrom ¶ added in v0.40.7
SessionVersionFrom is the session version a reauth sink call was made under (see db.RaiseNeedsReauthIfCurrent); zero when unknown.
func SetRoutePointObserver ¶ added in v0.40.19
func SetRoutePointObserver(f func(where string))
SetRoutePointObserver wires the counter for refused route points (rivolt_route_point_refused_total{where}).
func TireStatusLow ¶ added in v0.38.28
TireStatusLow reports whether Rivian's own per-wheel status is one of its low words.
func WithBackground ¶ added in v0.40.14
WithBackground returns a derived context for background Rivian reads. Priority beats it.
Types ¶
type Account ¶
type Account interface {
// Login accepts a first-leg (Email+Password) or second-leg
// (OTP) set of credentials. Must return ErrMFARequired when the
// server issued an MFA challenge.
Login(ctx context.Context, c Credentials) error
// Logout clears every authenticated-session field locally.
Logout()
// Authenticated reports whether a valid session exists.
Authenticated() bool
// MFAPending reports whether Login returned ErrMFARequired and
// the client is waiting for an OTP. Survives page reloads.
MFAPending() bool
// Email returns the email the current session is authenticated
// as, or "" if no session is active.
Email() string
// NeedsReauth reports whether a runtime classifier (errclass /
// session expiry / WS auth failure) flagged the stored session
// as no longer usable. The user must run a fresh Login (likely
// with OTP) to clear it. The bool is the gate; the string is a
// human-readable reason for surfacing in the UI.
NeedsReauth() (bool, string)
// Snapshot returns a copy of the current session. Persisted as
// JSON by secrets.SaveRivianSession so a restart doesn't drop
// the login.
Snapshot() Session
// Restore hydrates the client from a prior Snapshot. No I/O.
Restore(s Session)
// AuthReady returns a channel that is closed once the client
// holds usable session material. Background workers (the WS
// monitor in particular) select on it to avoid busy-looping
// against an unauthenticated client until a user happens to
// open the UI.
//
// On Logout the channel is replaced with a fresh open one, so
// receivers that took the closed signal previously will not see
// false positives after the next sign-in.
AuthReady() <-chan struct{}
}
Account is the auth-surface shared by every Rivian Client variant that can drive the Settings sign-in UI. Both *LiveClient and the *MockClient implement it, which lets RIVIAN_CLIENT=mock exercise exactly the same /api/settings/rivian flow the real deployment uses — you sign in, maybe clear an MFA prompt, then vehicles show up. The stub client deliberately does NOT implement this because there's nothing to sign into.
type AccountDBState ¶ added in v0.40.7
type AccountDBState struct {
// SessionVersion is the updated_at of the stored session; zero when none is stored.
SessionVersion time.Time
NeedsReauth bool
Reason string
}
AccountDBState is what the database says about one user's Rivian account.
type AccountRegistry ¶
type AccountRegistry interface {
// For returns the Account bound to userID, constructing one on
// first sight. Returns nil only when the registry was wired
// with a session-less client (rivian.Stub).
For(userID uuid.UUID) Account
// Loaded returns every userID currently held in the registry.
// Used by metrics + the boot hydrator. Order is unspecified.
Loaded() []uuid.UUID
}
AccountRegistry resolves a per-user Account. Each user owns an isolated *LiveClient (or *MockClient under RIVIAN_CLIENT=mock) so concurrent Login/Restore from different sessions cannot corrupt each other's tokens.
This replaces the singleton Account that v0.17.x carried at the top of api.Deps. The shared singleton was the root cause of two classes of bugs:
- rivianHydrateMW restoring the request user's session into a shared LiveClient — last-write-wins across concurrent users.
- Boot-time pods coming up with no session at all because no authenticated UI request had triggered hydration yet, leaving the StateMonitor goroutines busy-looping on ErrNotAuthenticated until someone opened the app.
Implementations must be safe for concurrent use; resolution is idempotent (the same userID returns the same Account instance for the lifetime of the registry).
func NewLiveAccountRegistry ¶
func NewLiveAccountRegistry(build func(uuid.UUID) *LiveClient) AccountRegistry
NewLiveAccountRegistry returns a registry whose For(uid) calls produce a fresh *LiveClient on first sight by invoking build(uid).
build is the operator-supplied factory (typically a closure over rivian.NewLive().WithRivoltVersion(...).WithBreaker(...)... etc.) so every per-user client gets the same breaker/limiter/kill-switch wiring that the singleton in main.go used to receive. The uid argument lets the closure thread it through user-scoped sinks (e.g. WithReauthSink that persists needs_reauth keyed by user_id).
func NewMockAccountRegistry ¶
func NewMockAccountRegistry(build func(uuid.UUID) *MockClient) AccountRegistry
NewMockAccountRegistry returns a registry that constructs a fresh *MockClient per user via build(uid). Used under RIVIAN_CLIENT=mock so the multi-user UI flow works in local dev without standing up real Rivian credentials.
func NewNopAccountRegistry ¶
func NewNopAccountRegistry() AccountRegistry
NewNopAccountRegistry returns a registry that always returns nil. Used with the stub client.
func NewReplayAccountRegistry ¶ added in v0.32.0
func NewReplayAccountRegistry(feed *ReplayFeed, planner TripPlanner) AccountRegistry
NewReplayAccountRegistry returns a registry handing each user a ReplayClient over feed.
type AccountStateSource ¶ added in v0.40.7
type AccountStateSource interface {
// States returns the state of each user asked about; a user it has no row for is
// left out of the map.
States(ctx context.Context, uids []uuid.UUID) (map[uuid.UUID]AccountDBState, error)
// Session returns the stored session, the zero value when there is none.
Session(ctx context.Context, uid uuid.UUID) (Session, error)
}
AccountStateSource reads the shared state. The Postgres-backed one is in package secrets.
type AccountSyncer ¶ added in v0.40.7
type AccountSyncer struct {
// contains filtered or unexported fields
}
An Account lives in memory, one per user per pod. The truth about it (the stored session and the needs_reauth flag) lives in Postgres, and any pod can change it: a sign-in, a code for the second step, a sign-out or a flag raised by a pod that saw Rivian reject the session. AccountSyncer brings every pod's copy back in line with the database, so a sign-in on one pod is the sign-in on all of them.
How: each user's state is a small version, the updated_at of the stored session row (a row that is deleted reads as "none") plus the flag and its reason. Reading it is one indexed query. The syncer runs it
- for every loaded account on a timer (SyncAll, a few seconds), which is how a pod that serves no web traffic, such as the one holding a vehicle's lease, learns of a new sign-in; and
- for one user right before the registry hands out that user's account (Touch, at most once per minGap per user), which is how a web request sees a change at once.
This is a poll, not LISTEN/NOTIFY, for the reasons internal/flags gives: a dedicated connection per pod and a reconnect path to lose notifications on, for a change that needs seconds, not milliseconds.
func NewAccountSyncer ¶ added in v0.40.7
func NewAccountSyncer(src AccountStateSource, get func(uuid.UUID) Account, log *slog.Logger) *AccountSyncer
NewAccountSyncer builds a syncer. get returns the account for a user WITHOUT triggering a sync (the registry's own lookup), or nil.
func (*AccountSyncer) Fresh ¶ added in v0.40.7
func (s *AccountSyncer) Fresh(ctx context.Context, uid uuid.UUID) (AccountDBState, bool)
Fresh reconciles one user's account now (no rate limit) and returns the state it read, for the user-facing status: whether the user needs to sign in again is answered from the database row, not from what this pod last remembered. ok is false when the database could not be read; the caller then falls back to the pod's copy.
func (*AccountSyncer) OnChange ¶ added in v0.40.7
func (s *AccountSyncer) OnChange(fn func(uuid.UUID, SyncChange))
OnChange installs the callback for SyncSessionReplaced and SyncLoggedOut. It runs after the account was updated and outside the syncer's lock.
func (*AccountSyncer) OnReconcile ¶ added in v0.40.8
func (s *AccountSyncer) OnReconcile(fn func(Reconcile))
OnReconcile installs the callback for Reconcile results (one per SyncAll).
func (*AccountSyncer) Run ¶ added in v0.40.7
Run calls SyncAll(loaded()) every interval until ctx ends.
func (*AccountSyncer) Superseded ¶ added in v0.40.7
Superseded reports whether the stored session is a different one from usedSess, the u-sess a rejected call was made with. A rejection of a session that was replaced elsewhere is not the user's session dying, so it must not raise needs_reauth. When it is superseded, this account is also brought up to date in the background. It does not touch the account itself: it runs with the client lock held.
func (*AccountSyncer) SyncAll ¶ added in v0.40.7
func (s *AccountSyncer) SyncAll(ctx context.Context, uids []uuid.UUID)
SyncAll reconciles every account the registry has loaded.
func (*AccountSyncer) Touch ¶ added in v0.40.7
func (s *AccountSyncer) Touch(uid uuid.UUID)
Touch reconciles one user's account if it was not checked within minGap. Failures are logged and leave the account as it was: a database blip must not sign anyone out.
type BatteryCharacteristics ¶ added in v0.36.3
type BatteryCharacteristics struct {
Chemistry uint64 // 1: 1 NCA, 2 LFP, 3 NMC
CellType uint64 // 2: 1 50G, 2 53G, 3 G124, 4 LG_4695
ModuleType uint64 // 3: 1 9M, 2 1M, 3 8M, 4 6M, 5 11M, 6 7M, 7 3M, 8 10M
PackCapacity uint64 // 4: see packCapacityKWh
UserTotalKWh float32 // 5
UserMaxKWh float32 // 6
}
BatteryCharacteristics is one energy.high_voltage.battery_characteristics frame. The enums are logged raw; packCapacityKWh names the capacity one.
type BatteryTemp ¶ added in v0.27.0
type BatteryTemp struct {
CellAvgC float64 `json:"cell_avg_c"`
CellMaxC float64 `json:"cell_max_c"`
CellMinC float64 `json:"cell_min_c"`
// SoCPct (charge_state.chargePercentage, field 1.1) and PackKWh
// (charge_state.pack_capacity, field 1.4) come from the same
// battery_state message. Zero when the frame omits charge_state.
SoCPct float64 `json:"soc_pct"`
PackKWh float64 `json:"pack_kwh"`
Timestamp string `json:"timestamp"`
RawB64 string `json:"raw_b64"`
}
BatteryTemp holds decoded high-voltage pack cell temperatures (°C).
type BatteryTempCallback ¶ added in v0.27.0
type BatteryTempCallback func(*BatteryTemp)
BatteryTempCallback receives decoded pack temperatures per frame.
type Breaker ¶
type Breaker struct {
// contains filtered or unexported fields
}
Breaker is a sliding-window circuit breaker over Rivian's classified upstream errors. Construct one per LiveClient.
Concurrency: every method takes the mutex; the breaker is on the hot path of every Rivian call, so contention is bounded by the outbound concurrency to Rivian (small — single-digit RPS at the scale we're targeting).
func NewBreaker ¶
func NewBreaker(cfg BreakerConfig, obs BreakerObserver) *Breaker
NewBreaker builds a breaker with the given config. Pass nil observer to skip metric/event emission. Zero-valued cfg fields are filled with DefaultBreakerConfig values.
func (*Breaker) Allow ¶
Allow is the upstream-gate hook. Returns ErrUpstreamBreakerOpen when the breaker is open. In half-open state the very first caller is admitted as a probe and the rest are rejected; the probe's outcome (Observe) decides the next state.
func (*Breaker) Observe ¶
func (b *Breaker) Observe(class ErrorClass)
Observe records the outcome of a Rivian call. Pass the ErrorClass returned by the classifier; success-equivalent outcomes (ClassUnknown after a 2xx, etc.) should call ObserveSuccess instead.
func (*Breaker) ObserveSuccess ¶
func (b *Breaker) ObserveSuccess()
ObserveSuccess marks a successful call. In half-open state this closes the breaker and resets the cooldown.
func (*Breaker) State ¶
func (b *Breaker) State() BreakerState
State returns the current breaker state. Lock-free read isn't worth the complexity at this call frequency; just take the lock.
type BreakerConfig ¶
type BreakerConfig struct {
// Window is the sliding time window over which failures are
// counted. Failures older than now-Window are pruned on every
// Observe call.
Window time.Duration
// RateLimitedThreshold is the number of ClassRateLimited
// outcomes within Window that trips the breaker. 429s are the
// most expensive failure mode — Rivian's throttle compounds
// the longer we ignore it — so this is intentionally low.
RateLimitedThreshold int
// OutageThreshold is the number of ClassOutage outcomes within
// Window that trips the breaker. Higher than rate-limited
// because a single 5xx is often a deploy-window blip we'd
// rather retry through.
OutageThreshold int
// Cooldown is how long the breaker stays open before
// transitioning to half-open. Doubled (capped at MaxCooldown)
// if a half-open probe fails.
Cooldown time.Duration
// MaxCooldown caps the exponential cooldown growth so a
// permanently degraded upstream still gets retried regularly
// instead of asymptotically never.
MaxCooldown time.Duration
// ProbeTimeout is how long a half-open probe may stay unreported before
// another is admitted. A probe can be admitted and never come back (the call
// was refused by a later gate, failed before it reached the network, or its
// outcome was never reported), and without a deadline the breaker would stay
// half-open, rejecting everything, for good. Defaults to Cooldown.
ProbeTimeout time.Duration
// Now is injectable for tests; defaults to time.Now.
Now func() time.Time
}
BreakerConfig tunes the breaker's tripping thresholds.
func DefaultBreakerConfig ¶
func DefaultBreakerConfig() BreakerConfig
DefaultBreakerConfig returns the production-ready defaults. Tuned for a single-region deployment against Rivian's gateway: 60s window, trip on 3 rate-limited or 8 outage events, 30s initial cooldown.
type BreakerObserver ¶
type BreakerObserver interface {
OnStateChange(from, to BreakerState)
OnTrip(reason string) // "rate_limited" or "outage"
}
BreakerObserver receives state-change notifications. Used to drive Prometheus gauges/counters from main.go without coupling the breaker to the metrics package.
type BreakerState ¶
type BreakerState int
BreakerState describes the breaker's gate behaviour.
BreakerClosed — fully open gate, requests flow normally.
BreakerOpen — gate closed, every Allow returns
ErrUpstreamBreakerOpen until the cooldown
expires.
BreakerHalfOpen — exactly one probe is allowed through; its
outcome decides whether to close or reopen
with a doubled cooldown.
const ( BreakerClosed BreakerState = iota BreakerHalfOpen BreakerOpen )
func (BreakerState) String ¶
func (s BreakerState) String() string
type ChargeCloseHook ¶
ChargeCloseHook is invoked asynchronously after a charge session closes (terminal charger state, plug pulled, or stale-session reaper). ctx is bounded; implementations must be safe for concurrent calls across vehicles.
type ChargeEnergyObserver ¶ added in v0.38.27
type ChargeEnergyObserver interface {
OnChargeEnergyFloor()
}
ChargeEnergyObserver is optionally implemented by the SleepObserver (main's metrics adapter): told once for each charge whose energy the floor replaced (rivolt_charge_energy_floor_total, no labels).
type ChargeNow ¶ added in v0.39.1
ChargeNow is what the open charge has recorded so far: the energy, the peak power (which says AC from fast charging) and where it is ("home", "away" or "" when unknown), for the live-session estimate.
func (ChargeNow) HomeRateApplies ¶ added in v0.39.1
HomeRateApplies says whether the user's home $/kWh rate is a fair price for this charge: an AC session (peak under the L2 threshold) that is not known to be away from home. A fast charge on another network, or an AC charge at a place that is not home, has a price of its own that we do not know, so no estimate is better than a home-rate one.
type ChargingFrame ¶
type ChargingFrame struct {
At time.Time `json:"at"`
VehicleID string `json:"vehicle_id"`
Event string `json:"event,omitempty"`
Raw string `json:"raw,omitempty"`
Err string `json:"err,omitempty"`
}
ChargingFrame is one raw ChargingSession push or lifecycle event. Event is set for lifecycle markers ("open", "error", "close"); Raw carries the JSON payload for push frames.
type ChargingSchedule ¶ added in v0.36.5
type ChargingSchedule struct {
// StartMin is the window start in minutes after local midnight.
StartMin int `json:"start_min"`
// DurationMin is the window length in minutes (1440 = all day).
DurationMin int `json:"duration_min"`
Enabled bool `json:"enabled"`
// WeekDays are the days the window starts on, "Monday".."Sunday".
WeekDays []string `json:"weekdays"`
// AmperageA is the charge current the schedule sets.
AmperageA int `json:"amperage_a,omitempty"`
// AppliesHere reports whether the truck was at the schedule's location
// (within scheduleRadiusM) at the last read. Reads happen on connect,
// daily, and when the truck is seen plugged in and not charging, so
// it's current whenever that's the question. nil when either position
// is unknown.
AppliesHere *bool `json:"applies_here,omitempty"`
// contains filtered or unexported fields
}
ChargingSchedule is one scheduled charging window.
type ChargingStatus ¶ added in v0.36.3
type ChargingStatus struct {
Plug uint64 // 1: 0 INIT, 1 NOT_CONNECTED, 2 CONNECTED, 3 ERROR, 4 V2L_CONNECTED
Display uint64 // 2: 0 SNA, 1 READY … 25 SMART_CHARGING_CHARGING (chargerStateFromParallax)
EVSE uint64 // 3: evse_type, enum not pinned
}
ChargingStatus is one charging.session.status frame, validated against vehicleState's chargerStatus on live frames. Absent fields are 0, the proto default: plug INIT, display SNA.
type Client ¶
type Client interface {
Login(ctx context.Context, creds Credentials) error
Vehicles(ctx context.Context) ([]Vehicle, error)
State(ctx context.Context, vehicleID string) (*State, error)
}
Client is the high-level API surface Rivolt uses against Rivian. Concrete implementations live in live.go (real upstream) and mock.go (offline fixture for tests and demos).
type Credentials ¶
type Credentials struct {
Email string
Password string
OTP string // optional: populated only for the initial MFA challenge
}
Credentials is what the operator submits via Settings → Rivian account. Stored encrypted at rest; never logged.
type DCFCDetector ¶ added in v0.33.0
type DCFCDetector func(lat, lon float64, rivianCharger bool, peakKW float64) (v DCFCVerdict, on bool)
DCFCDetector detects and prices the network of a DC fast charge Rivian didn't bill, at the charge's position (where it opened). on=false means auto-detection is off for this user: the recorder falls back to the DCFCPriceLookup. rivianCharger is Rivian's isRivianCharger for this charge's own session; peakKW is the session's peak so far.
type DCFCEstimate ¶ added in v0.38.8
type DCFCEstimate struct {
Rate float64
Currency string
Network string
// Month and URL name the report the figure came from ("2026-10",
// the dcfctracker.com page).
Month, URL string
}
DCFCEstimate is a fast-charging session's price taken from a typical public price (cost_source "observed_estimate"): never the user's own rate, which always wins, and replaced by Rivian's bill when it arrives.
type DCFCEstimator ¶ added in v0.38.8
DCFCEstimator returns a typical-price estimate for a fast charge that started at start, when the user has no rate for it. slug is the detected network's, or "" to find the network at (lat, lon). ok=false leaves the session unpriced. Wired for the live recorder only; `rivolt reprice` never sets one.
type DCFCPriceLookup ¶ added in v0.32.0
DCFCPriceLookup returns the user's $/kWh for a DC fast-charging session at (lat, lon), the currency, and the matched network's display name ("" when no station matched and a fallback rate was used). ok=false means no estimate is available.
type DCFCVerdict ¶ added in v0.33.0
type DCFCVerdict struct {
Priced bool
Rate float64
Currency string
Network string
State string
Confidence float64
Evidence json.RawMessage
// Slug is the matched network's slug ("" when nothing matched); the
// recorder hands it to the DCFCEstimator when the user has no rate.
Slug string
}
DCFCVerdict is a DC fast charge network detection, priced: the network, its rate for this user, and the verify state the charge row carries (contract dcfc-auto-network v1). Priced=false means nothing matched: the charge stays unpriced and needs the user.
type DriveCloseHook ¶
DriveCloseHook is invoked asynchronously after a D→P transition finishes upserting the drive row. ctx is bounded; the hook should honour it. Implementations must be safe for concurrent calls across vehicles.
type DriveDynamicsCallback ¶ added in v0.27.4
type DriveDynamicsCallback func(DriveDynamicsFrame)
DriveDynamicsCallback receives each decoded drive-dynamics frame.
type DriveDynamicsFrame ¶ added in v0.27.4
type DriveDynamicsFrame struct {
RVM string
Value uint64
ValueOK bool
Payload string
TimestampMs int64
}
DriveDynamicsFrame is one shadow frame. Value is the field-1 varint (when ValueOK) — interpret per RVM (gear enum, drive_mode enum, odometer in whole km). Payload is the raw base64 so a not-yet-RE'd topic (e.g. power.state) can still be logged and decoded offline.
type DriveLocationObserver ¶ added in v0.40.16
type DriveLocationObserver interface {
}
DriveLocationObserver is optionally implemented by the SleepObserver (main's metrics adapter): told once for each closed drive that moved without the truck reporting where it was (rivolt_drives_location_unavailable_total, no labels).
type DynamicsGNSS ¶ added in v0.27.0
type DynamicsGNSS struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
AltitudeM float64 `json:"altitude_m"`
SpeedMS float64 `json:"speed_ms"`
HeadingDeg float64 `json:"heading_deg"`
TimestampMs int64 `json:"timestamp_ms"`
}
DynamicsGNSS is a decoded dynamics.vehicle.gnss frame. Field numbers confirmed from a live frame (see parallax_test.go): lat/lon/alt are doubles, speed/heading floats, timestamp a varint (epoch ms). Fields 6-9 are GPS accuracy metrics we don't consume yet. speed (field 4) was absent (0) in the parked capture — mapped here, to be confirmed against a moving frame.
type DynamicsGNSSCallback ¶ added in v0.27.0
type DynamicsGNSSCallback func(*DynamicsGNSS)
DynamicsGNSSCallback receives a decoded gnss frame.
type ElevationLookup ¶
ElevationLookup is the minimal contract the recorder needs from an elevation resolver. Returns the altitude in meters, or ok=false when the tile is not (yet) cached -- the recorder writes NULL in that case rather than blocking on a network fetch. Implemented by *elevation.Resolver; abstracted here so the rivian package stays dep-free at the type-graph level and tests can stub it trivially.
type ErrRateLimited ¶
ErrRateLimited is returned by checkUpstream when the global upstream token bucket is empty. Carries the retry-after so the api layer can map it to a 503 Retry-After.
func (*ErrRateLimited) Error ¶
func (e *ErrRateLimited) Error() string
type ErrorClass ¶
type ErrorClass int
ErrorClass buckets every upstream failure into one of five categories. The classes drive very different downstream reactions: a Transient error should be retried with backoff; an Outage should be circuit-broken; RateLimited should back off globally; UserAction should raise the per-user needs_reauth flag and stop retrying for that user; Unknown is treated as Transient-with-a-logged-warning.
This enum is the single biggest determinant of support load at scale. Misclassifying UserAction as Transient means retrying a bad-password response until Rivian locks the account; misclassifying RateLimited as Transient means a reconnect storm that makes the throttle worse.
const ( // ClassUnknown is the default for anything we haven't seen // enough of to pattern-match confidently. Callers should // treat it as Transient but log at WARN so repeated unknowns // become a signal to extend Classify. ClassUnknown ErrorClass = iota // ClassTransient is network flaps, 5xx from a healthy // upstream (503 during a deploy), context timeouts on a // single call. Retry with jittered backoff. ClassTransient // ClassOutage is sustained 5xx, DNS failure, connection // refused. Distinct from Transient only in degree; the // circuit breaker trips on repeated Outage within a window. ClassOutage // ClassRateLimited is 429 or a GraphQL error whose extension // code / message names a rate limit. Must never be retried // without honoring a global Redis token bucket. ClassRateLimited // ClassUserAction means the user has to do something before // their calls will succeed again — log back in, accept an // MFA challenge, or contact Rivian support. Retrying is // actively harmful: repeated bad-password attempts escalate // to account lockouts. Raises the per-user needs_reauth // flag and suppresses further calls for that user. ClassUserAction )
func ClassifyGraphQL ¶
func ClassifyGraphQL(extCode, message string) (ErrorClass, string)
ClassifyGraphQL maps the first GraphQL error in a 200-OK response envelope to an ErrorClass. The gateway will happily return HTTP 200 with errors in the body — a password reject during Login looks exactly like this.
func ClassifyHTTP ¶
func ClassifyHTTP(status int, body string) (ErrorClass, string)
ClassifyHTTP maps an HTTP status + optional response body fragment to an ErrorClass. Body is inspected only when the status alone is ambiguous (400 can be either user-action or transient depending on message).
This is the narrow, pure-function seam so the classifier is trivially testable without a live gateway. doGraphQLAt calls it at one specific point in its pipeline.
func ClassifyNetwork ¶
func ClassifyNetwork(err error) (ErrorClass, string)
ClassifyNetwork maps a Go-side error from http.Client.Do, json.Unmarshal, or ctx cancellation into an ErrorClass. Called when the pipeline fails before we have an HTTP status to look at.
func (ErrorClass) String ¶
func (c ErrorClass) String() string
String returns a short label suitable for logs and metrics labels. Keep stable — dashboards and alerts key on these.
type FirstPlanner ¶ added in v0.37.0
type FirstPlanner []TripPlanner
FirstPlanner tries each planner in order and returns the first answer that is not ErrNoFixture (so a stored plan is used when one matches and the next planner otherwise). Any other error ends the search.
func (FirstPlanner) PlanTrip ¶ added in v0.37.0
func (f FirstPlanner) PlanTrip(ctx context.Context, in PlanTripInput) (*TripPlan, error)
PlanTrip implements TripPlanner.
type FixturePlanner ¶ added in v0.37.0
type FixturePlanner struct {
// Now stands in for "now" when the input has no departure time.
Now func() time.Time
// contains filtered or unexported fields
}
FixturePlanner answers a trip with a stored plan when one matches: origin and destination each within 1 km of a fixture's, no intermediate stops, and a starting SoC within 5 points of the fixture's. The stored plan is moved to the requested departure, and its SoC values to the requested start, and it is marked Simulated. No match is ErrNoFixture. Deterministic: the only clock is Now, which tests pin.
func NewFixturePlanner ¶ added in v0.37.0
func NewFixturePlanner(fx []PlanFixture, now func() time.Time) *FixturePlanner
NewFixturePlanner wraps the fixtures. now may be nil (time.Now).
func (*FixturePlanner) PlanTrip ¶ added in v0.37.0
func (p *FixturePlanner) PlanTrip(_ context.Context, in PlanTripInput) (*TripPlan, error)
PlanTrip implements TripPlanner.
type FrameSource ¶ added in v0.32.0
type FrameSource interface {
Stream(ctx context.Context, vehicleID string, emit func(st State, catchingUp bool)) error
}
FrameSource feeds a StateMonitor full-snapshot State frames in place of a live Rivian subscription (see NewSourceMonitor). Stream blocks until ctx is cancelled, calling emit once per frame in time order. catchingUp is true while the source is replaying frames whose time has already passed (backfill after a restart or first connect), so the monitor can hold back user-facing side effects such as push notifications for sessions that "closed" days ago. Stream returns ErrVehicleNotFound for a vehicle the source doesn't serve.
type GatedLiveStateStore ¶ added in v0.37.7
type GatedLiveStateStore struct {
// contains filtered or unexported fields
}
GatedLiveStateStore wraps a LiveStateStore for a Redis that may not be reachable yet, or any more. While ready() is false, or for gatedBackoff after a failed call, it answers as if nothing were stored (Load: not found, writes: nil) without touching Redis; otherwise it delegates with a short timeout. This is what lets the store be installed at boot before Redis answers (issue #603) and start working the moment it does, with the monitors already running.
func NewGatedLiveStateStore ¶ added in v0.37.7
func NewGatedLiveStateStore(inner LiveStateStore, ready func() bool) *GatedLiveStateStore
NewGatedLiveStateStore gates inner behind ready (nil: always ready).
func (*GatedLiveStateStore) Delete ¶ added in v0.37.7
func (g *GatedLiveStateStore) Delete(ctx context.Context, vehicleID string) error
func (*GatedLiveStateStore) Load ¶ added in v0.37.7
func (g *GatedLiveStateStore) Load(ctx context.Context, vehicleID string) (LiveStateSnapshot, bool, error)
func (*GatedLiveStateStore) Save ¶ added in v0.37.7
func (g *GatedLiveStateStore) Save(ctx context.Context, vehicleID string, snap LiveStateSnapshot, ttl time.Duration) error
type HomeLookup ¶ added in v0.32.0
HomeLookup returns the user's saved home location; ok=false when the user hasn't set one.
type LiveChargeSnapshot ¶
type LiveChargeSnapshot struct {
ID string `json:"id"`
Number int64 `json:"number"`
StartedAt time.Time `json:"started_at"`
StartSoC float64 `json:"start_soc"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
MaxPower float64 `json:"max_power"`
EndAt time.Time `json:"end_at"`
EndSoC float64 `json:"end_soc"`
FinalState string `json:"final_state"`
// LastMeaningfulAt anchors the stale-session guard across restarts.
// Older snapshots omit it; restore falls back to EndAt then.
LastMeaningfulAt time.Time `json:"last_meaningful_at"`
// LastProgressAt anchors it for a charge with no plug indicator;
// zero (older snapshots) restores as LastMeaningfulAt.
LastProgressAt time.Time `json:"last_progress_at,omitempty"`
// ActiveSeconds carries the accumulated real charging time across a
// pod handoff so the session duration doesn't reset when a peer
// takes over mid-charge.
ActiveSeconds float64 `json:"active_seconds"`
// EnergyIntKWh carries the energy integral across a handoff. Without it the peer's integral restarted at 0 and
// the charge ended with only the energy since the last handoff. Snapshots an older pod wrote lack it (0).
EnergyIntKWh float64 `json:"energy_int_kwh,omitempty"`
}
LiveChargeSnapshot mirrors liveCharge.
type LiveClient ¶
type LiveClient struct {
// contains filtered or unexported fields
}
LiveClient talks to the real Rivian Owner App GraphQL gateway. All exported methods are safe to call concurrently.
func (*LiveClient) AuthReady ¶
func (c *LiveClient) AuthReady() <-chan struct{}
AuthReady returns a channel closed once the client has a usable session. See Account.AuthReady. Replaced on Logout.
func (*LiveClient) Authenticated ¶
func (c *LiveClient) Authenticated() bool
Authenticated reports whether a userSessionToken is set locally.
func (*LiveClient) ChargingFieldProbe ¶
func (c *LiveClient) ChargingFieldProbe(ctx context.Context, field, vehicleID string) (map[string]any, error)
ChargingFieldProbe fires a deliberately malformed query for diagnostics.
func (*LiveClient) ChargingFieldProbeWithSelection ¶
func (c *LiveClient) ChargingFieldProbeWithSelection(ctx context.Context, field, vehicleID, selection string) (map[string]any, error)
ChargingFieldProbeWithSelection probes a field with a custom selection set.
func (*LiveClient) ChargingSchedules ¶ added in v0.36.5
func (c *LiveClient) ChargingSchedules(ctx context.Context, vehicleID string) ([]ChargingSchedule, error)
ChargingSchedules reads the vehicle's charging schedules. An empty list is a valid answer (no schedule set).
func (*LiveClient) ChargingSchemaProbe ¶
ChargingSchemaProbe runs an introspection query against the charging endpoint, returning __schema.queryType.fields for diagnostics.
func (*LiveClient) CompletedSessionSummaries ¶ added in v0.36.4
func (c *LiveClient) CompletedSessionSummaries(ctx context.Context) ([]SessionSummary, error)
CompletedSessionSummaries returns the user's completed charging sessions with what Rivian billed. Rows without a transaction id or a parseable window are dropped.
func (*LiveClient) Email ¶
func (c *LiveClient) Email() string
Email returns the authenticated email, or "" if not logged in.
func (*LiveClient) IntrospectEnum ¶ added in v0.19.4
IntrospectEnum returns the enumValues of a named GraphQL enum type. Separate query from IntrospectInputType because Rivian's gateway validates __type subselections strictly: a query that asks for both `inputFields` and `enumValues` fails with GRAPHQL_VALIDATION_FAILED. Splitting them keeps each query valid against whatever shape the gateway exposes.
func (*LiveClient) IntrospectInputType ¶
func (c *LiveClient) IntrospectInputType(ctx context.Context, typeName string) (map[string]any, error)
IntrospectInputType returns the field set of a named GraphQL input object via the gateway's __type introspection. Used to reverse-engineer the exact shape of inputs like CoordinatesInput that the iOS-app reverse-engineered docs got wrong (they conflated input/response shapes). Admin-only.
func (*LiveClient) LiveSession ¶
func (c *LiveClient) LiveSession(ctx context.Context, vehicleID string) (*LiveSession, error)
LiveSession returns the in-progress charging session, or an inactive one.
func (*LiveClient) Login ¶
func (c *LiveClient) Login(ctx context.Context, creds Credentials) error
Login performs the email/password → MFA dance. See ErrMFARequired for the two-call flow.
func (*LiveClient) Logout ¶
func (c *LiveClient) Logout()
Logout clears session fields but keeps the CSRF token (it survives logout).
func (*LiveClient) MFAPending ¶
func (c *LiveClient) MFAPending() bool
MFAPending reports whether Login is awaiting an OTP submission.
func (*LiveClient) NeedsReauth ¶
func (c *LiveClient) NeedsReauth() (bool, string)
NeedsReauth reports the in-memory needs_reauth flag and reason.
func (*LiveClient) OTAUpdateDetails ¶ added in v0.36.5
OTAUpdateDetails returns the current and available OTA releases for a vehicle. A release with no version is dropped; a release-notes URL that is not a plain https link is dropped (it ends up behind an <a>).
func (*LiveClient) PendingSnapshot ¶ added in v0.23.1
func (c *LiveClient) PendingSnapshot() (PendingMFA, bool)
PendingSnapshot returns the current OTP challenge, or false when no OTP submission is awaited. IssuedAt is left zero for the persistence layer to stamp.
func (*LiveClient) PlanTrip ¶
func (c *LiveClient) PlanTrip(ctx context.Context, in PlanTripInput) (*TripPlan, error)
PlanTrip runs the gateway's planTripWithMultiStop operation and returns a flattened, JSON-friendly TripPlan. Per-user — the authenticated session must own the vehicleId.
Schema source: jrgutier/rivian-python-client/src/rivian/schemas/gateway.graphql. The operation is `planTrip` (not `planTripMultiStop` — the docs at rivian-api.kaedenb.org described a non-existent or broken sibling), taking scalar origin+destination CoordinatesInput args (not a waypoints array), `bearing` (not `originBearing`), and required startingRangeMeters. driveMode/trailerProfile/avoidAdapterRequired/ supportedConnectorTypes don't exist on this schema at all.
func (*LiveClient) PlanTripRaw ¶
func (c *LiveClient) PlanTripRaw(ctx context.Context, variables map[string]any) (map[string]any, error)
PlanTripRaw posts the planTripWithMultiStop operation with a caller-supplied variables payload and returns the gateway's response data + errors verbatim. Bypasses the typed PlanTrip path so we can reverse-engineer schema/value mismatches without round- tripping through chart bumps. Admin-only (the wrapping HTTP handler enforces this) — gives the operator full control over every field in the request, including ones the typed path defaults.
Returns the parsed JSON `data` field on success. On a GraphQL `errors` envelope, returns the structured error from doGraphQLAt's classification (which preserves extension codes / messages) so callers can read the failure reason directly.
func (*LiveClient) ProbeBatteryTemperature ¶ added in v0.27.0
func (c *LiveClient) ProbeBatteryTemperature(ctx context.Context, vehicleID string) (*BatteryTemp, error)
ProbeBatteryTemperature opens a short-lived Parallax subscription for vehicleID, waits for the first battery_state frame, decodes the pack cell temperatures, and returns them. Proof-of-concept: it reuses the existing subscription websocket mux + auth, so no new transport.
func (*LiveClient) ProbeParallaxRaw ¶ added in v0.27.0
func (c *LiveClient) ProbeParallaxRaw(ctx context.Context, vehicleID string, rvms []string, maxFrames int) ([]ParallaxRawFrame, error)
ProbeParallaxRaw subscribes to the given RVM topics and returns up to maxFrames raw frames, or whatever arrived before the timeout. The vehicle must be awake (and, for dynamics.vehicle.gnss, moving) for frames to stream.
func (*LiveClient) RawGraphQL ¶
func (c *LiveClient) RawGraphQL(ctx context.Context, operationName, query string, variables map[string]any) (map[string]any, error)
RawGraphQL posts an arbitrary operation+query+variables to the gateway and returns the response data verbatim. Admin-only debugging primitive for trying different operation names, response selection sets, etc. without round-tripping through chart bumps.
func (*LiveClient) RecentActions ¶ added in v0.32.0
func (c *LiveClient) RecentActions(vehicleID string, from, to time.Time) []RivoltAction
RecentActions returns the vehicle's (and account-level) requests in [from, to], oldest first.
func (*LiveClient) RecentChargingFrames ¶
func (c *LiveClient) RecentChargingFrames(vehicleID string) []ChargingFrame
RecentChargingFrames returns a copy of the ring buffer, most recent last. Filter by vehicleID if non-empty.
func (*LiveClient) RecordChargingEvent ¶
func (c *LiveClient) RecordChargingEvent(vehicleID, event, errMsg string)
RecordChargingEvent appends a lifecycle marker to the ring buffer.
func (*LiveClient) RecordChargingFrame ¶
func (c *LiveClient) RecordChargingFrame(vehicleID string, raw []byte)
RecordChargingFrame appends a raw frame to the ring buffer.
func (*LiveClient) RefreshSession ¶ added in v0.24.5
func (c *LiveClient) RefreshSession(ctx context.Context) error
RefreshSession forces a fresh a-sess/csrf pair and then probes the account with a real authenticated call (getUserInfo via Vehicles). It's the manual counterpart to the auto-heal in doGraphQL: an admin can trigger it for a user stuck on needs_reauth to see whether the session is salvageable (stale csrf) or genuinely dead (expired u-sess, which no refresh can fix). Returns nil when the probe succeeds — the caller then clears needs_reauth.
func (*LiveClient) Restore ¶
func (c *LiveClient) Restore(s Session)
Restore hydrates the client from a Snapshot. No network I/O.
func (*LiveClient) RestorePending ¶ added in v0.23.1
func (c *LiveClient) RestorePending(p PendingMFA)
RestorePending re-arms an OTP challenge captured by PendingSnapshot on another pod so this client can complete the second leg.
func (*LiveClient) SessionCharger ¶ added in v0.36.4
func (c *LiveClient) SessionCharger(ctx context.Context, transactionID string) (*SessionCharger, error)
SessionCharger returns the charger name, vendor and isHomeCharger for a transaction, or nil when Rivian has no charger for it.
func (*LiveClient) SessionVersion ¶ added in v0.40.7
func (c *LiveClient) SessionVersion() time.Time
SessionVersion is what SetSessionVersion last set (zero when unknown).
func (*LiveClient) SessionVersionFor ¶ added in v0.40.16
func (c *LiveClient) SessionVersionFor(usedToken string) time.Time
SessionVersionFor is the stored version for a call that was made with this user token: the version if the client learnt it for that very token, zero (unknown) otherwise. A call still in flight with a replaced session, answered after this client followed a sign-in made elsewhere and relearnt the new version, must not carry the new version: its rejection says nothing about the new session. Zero makes the raise defer.
func (*LiveClient) SetNeedsReauth ¶
func (c *LiveClient) SetNeedsReauth(needs bool, reason string)
SetNeedsReauth sets the flag without firing the sink. Used to hydrate from storage at startup; runtime classification goes through doGraphQLAt.
func (*LiveClient) SetSessionVersion ¶ added in v0.40.7
func (c *LiveClient) SetSessionVersion(v time.Time)
SetSessionVersion records the stored session version (user_secrets updated_at) this client's session corresponds to; the zero time means unknown.
The version belongs to the session (the user token) the client holds when this is called, and is remembered together with that token: a rejection is attributed to the version only if the call that was rejected used that same token (see SessionVersionFor). Call it right after the Restore or the sign-in that put the session in the client.
func (*LiveClient) Snapshot ¶
func (c *LiveClient) Snapshot() Session
Snapshot returns the current Session, or zero if not authenticated.
func (*LiveClient) State ¶
State returns the current vehicle snapshot. Battery %, distances km, temps °C.
func (*LiveClient) StateRaw ¶
StateRaw returns the decoded vehicleState as generic JSON for debugging.
func (*LiveClient) SubscribeBatteryState ¶ added in v0.27.0
func (c *LiveClient) SubscribeBatteryState(ctx context.Context, vehicleID string, cb BatteryTempCallback) error
SubscribeBatteryState streams the energy.high_voltage.battery_state Parallax topic for vehicleID, invoking cb with decoded pack cell temperatures on every frame. Blocks until ctx is cancelled or auth fails; reconnects with exponential backoff on transient errors. The topic only pushes while the vehicle is awake.
This is the additive-Parallax pattern (see docs on the pack-temp migration): vehicleState stays the primary source, and a Parallax topic is layered on for a field it lacks. The next topic Rivolt wants (cold-weather SOC, richer thermal, …) follows this same shape.
func (*LiveClient) SubscribeChargingSession ¶
func (c *LiveClient) SubscribeChargingSession(ctx context.Context, vehicleID string, cb LiveSessionCallback) error
SubscribeChargingSession streams charging telemetry for vehicleID. Blocks until ctx is cancelled or auth fails; reconnects with exponential backoff on transient errors. Preferred over the REST LiveSession poll — this path carries home AC / L2 data.
func (*LiveClient) SubscribeDriveDynamics ¶ added in v0.27.4
func (c *LiveClient) SubscribeDriveDynamics(ctx context.Context, vehicleID string, extra func() []string, cb DriveDynamicsCallback) error
SubscribeDriveDynamics streams the Parallax dynamics.vehicle.{gear, drive_mode,odometer} topics over one multiplexed subscription, invoking cb per decoded frame. Blocks until ctx is cancelled or auth fails; reconnects with backoff. Phase 2 measurement — run in shadow alongside vehicleState to pin the gear enum and compare cadence before any field is made authoritative.
extra, if non-nil, is called before every (re)connect for the extra topics to carry, so a change (wake-safety capture switch) takes effect at the next reconnect without a forced one.
func (*LiveClient) SubscribeDynamicsGNSS ¶ added in v0.27.0
func (c *LiveClient) SubscribeDynamicsGNSS(ctx context.Context, vehicleID string, cb DynamicsGNSSCallback) error
SubscribeDynamicsGNSS streams the dynamics.vehicle.gnss Parallax topic for vehicleID, invoking cb with each decoded frame. Blocks until ctx is cancelled or auth fails; reconnects with backoff. Measurement step of the driving-stats migration — run alongside vehicleState to compare GPS cadence/accuracy before flipping the recorder.
func (*LiveClient) SubscribeParallaxCharging ¶
func (c *LiveClient) SubscribeParallaxCharging(ctx context.Context, vehicleID string, cb LiveSessionCallback) error
SubscribeParallaxCharging streams charge-session-breakdown RVM messages for vehicleID. Blocks until ctx is cancelled or auth fails; reconnects with exponential backoff on transient errors. This is the path that carries home-AC / L1 / L2 live power + energy data (the regular ChargingSession subscription returns nulls for those).
func (*LiveClient) SubscribeVehicleState ¶
func (c *LiveClient) SubscribeVehicleState(ctx context.Context, vehicleID string, cb StateCallback) error
SubscribeVehicleState opens a websocket to Rivian's subscription gateway, subscribes to the named vehicle's state, and invokes cb with every push. Blocks until ctx is cancelled or the connection fails terminally (unauthenticated, context deadline, etc.). Reconnects with exponential backoff on transient errors.
The caller owns the lifetime via ctx; there is no separate unsubscribe — ctx cancellation tears everything down cleanly.
func (*LiveClient) SubscribeVehicleStateHeld ¶ added in v0.32.0
func (c *LiveClient) SubscribeVehicleStateHeld(ctx context.Context, vehicleID string, hold ReconnectHold, cb StateCallback) error
SubscribeVehicleStateHeld is SubscribeVehicleState with a hold (wake-safety switch (c), see wake_safety.go). nil hold is SubscribeVehicleState.
func (*LiveClient) SupportedFeatures ¶ added in v0.27.0
SupportedFeatures returns the capability map for every vehicle on the authenticated account: vehicleID -> (feature name -> status). Backed by the same gateway endpoint as vehicleState. Callers typically look up one vehicle and one feature (see VehicleSupportsParallaxState).
func (*LiveClient) VehicleImages ¶
func (c *LiveClient) VehicleImages(ctx context.Context) ([]VehicleImage, error)
VehicleImages returns the configurator-rendered mobile images for every vehicle on the account. Filter on VehicleID client-side. Returns an empty slice (not an error) if Rivian has no images yet.
func (*LiveClient) VehicleSupportsParallax ¶ added in v0.27.0
VehicleSupportsParallax reports whether the given vehicle can stream Parallax topics at all (VEHICLE_CONNECTIVITY_PARALLAX = AVAILABLE) — the gate for cherry-picking individual topics like the gnss GPS feed. This is deliberately NOT the full-migration PX_STATE_ALL capability. A vehicle missing from the response, or reporting any other status, returns false.
func (*LiveClient) Vehicles ¶
func (c *LiveClient) Vehicles(ctx context.Context) ([]Vehicle, error)
Vehicles lists the vehicles on the authenticated account.
func (*LiveClient) WithBreaker ¶
func (c *LiveClient) WithBreaker(b *Breaker) *LiveClient
WithBreaker attaches a circuit breaker. Every classified outcome from doGraphQLAt is reported to b; when b is open, checkUpstream returns ErrUpstreamBreakerOpen before we hit the network.
func (*LiveClient) WithEndpoint ¶
func (c *LiveClient) WithEndpoint(url string) *LiveClient
WithEndpoint points the client at an alternate GraphQL URL.
func (*LiveClient) WithHTTPClient ¶
func (c *LiveClient) WithHTTPClient(h *http.Client) *LiveClient
WithHTTPClient installs a custom *http.Client.
func (*LiveClient) WithRateLimit ¶
func (c *LiveClient) WithRateLimit(rl RateLimiter) *LiveClient
WithRateLimit attaches a global token-bucket gate. Calls without an explicit class go through the "main" class; the account-aware paths (Login, LoginWithOTP, getUserInformation) can be promoted by the caller via doGraphQLPriority.
func (*LiveClient) WithReauthGuard ¶ added in v0.40.7
func (c *LiveClient) WithReauthGuard(superseded func(ctx context.Context, usedSess string) bool) *LiveClient
WithReauthGuard installs a check run before a rejected call raises needs_reauth. superseded gets the u-sess the failed call used and reports whether the stored session is a different, newer one (the user signed in again on another pod and this client has not caught up). Then the rejection says nothing about the user's real session, so the flag stays down. It runs on the failure path while the client lock may be held: it must not call back into the client.
func (*LiveClient) WithReauthSink ¶
func (c *LiveClient) WithReauthSink(sink func(ctx context.Context, reason string)) *LiveClient
WithReauthSink installs a callback fired on ClassUserAction errors.
func (*LiveClient) WithResultObserver ¶ added in v0.36.15
func (c *LiveClient) WithResultObserver(o ResultObserver) *LiveClient
WithResultObserver attaches o (nil to remove).
func (*LiveClient) WithRivoltVersion ¶
func (c *LiveClient) WithRivoltVersion(v string) *LiveClient
WithRivoltVersion stamps the build version into X-Rivolt-Version.
func (*LiveClient) WithUpstreamGate ¶
func (c *LiveClient) WithUpstreamGate(gate func(ctx context.Context) error) *LiveClient
WithUpstreamGate installs a pre-flight hook that short-circuits outbound calls when it returns non-nil. Pass nil to remove.
type LiveDrive ¶
type LiveDrive struct {
VehicleID string `json:"vehicle_id"`
Number int64 `json:"number"`
StartedAt time.Time `json:"started_at"`
EndedAt time.Time `json:"ended_at"`
ElapsedSec float64 `json:"elapsed_sec"`
StartSoCPct float64 `json:"start_soc_pct"`
EndSoCPct float64 `json:"end_soc_pct"`
SoCUsedPct float64 `json:"soc_used_pct"`
StartOdometerMi float64 `json:"start_odometer_mi"`
EndOdometerMi float64 `json:"end_odometer_mi"`
DistanceMi float64 `json:"distance_mi"`
MaxSpeedMph float64 `json:"max_speed_mph"`
AvgSpeedMph float64 `json:"avg_speed_mph"`
EnergyUsedKWh float64 `json:"energy_used_kwh"`
MiPerKWh float64 `json:"mi_per_kwh"`
PackKWh float64 `json:"pack_kwh"`
}
LiveDrive is the wire-friendly snapshot of an in-flight drive session — the recorder's internal `liveDrive` accumulator projected into the same style that /api/live-session returns for charges. Returned by ActiveDrive; fields are flat so the frontend can render without having to reach into recorder internals.
type LiveDriveSnapshot ¶
type LiveDriveSnapshot struct {
ID string `json:"id"`
Number int64 `json:"number"`
StartedAt time.Time `json:"started_at"`
StartSoC float64 `json:"start_soc"`
StartOdoMi float64 `json:"start_odo_mi"`
StartLat float64 `json:"start_lat"`
StartLon float64 `json:"start_lon"`
MaxSpeed float64 `json:"max_speed"`
SumSpeed float64 `json:"sum_speed"`
SpeedN int `json:"speed_n"`
EndAt time.Time `json:"end_at"`
EndSoC float64 `json:"end_soc"`
EndOdoMi float64 `json:"end_odo_mi"`
EndLat float64 `json:"end_lat"`
EndLon float64 `json:"end_lon"`
// The trailer connector's integral over the drive so far (trailer.go), so a restart keeps it.
TrailerPresentSec float64 `json:"trailer_present_sec,omitempty"`
TrailerKnownSec float64 `json:"trailer_known_sec,omitempty"`
TrailerLast string `json:"trailer_last,omitempty"`
TrailerAt time.Time `json:"trailer_at,omitempty"`
Path [][2]float64 `json:"path,omitempty"`
// Distinct location fixes seen so far (recorder.go liveDrive.locFixes), so a handover keeps the count.
LocFixes int `json:"loc_fixes,omitempty"`
LocKey string `json:"loc_key,omitempty"`
}
LiveDriveSnapshot mirrors liveDrive.
type LiveSession ¶
type LiveSession struct {
At time.Time `json:"at"`
VehicleID string `json:"vehicle_id"`
Active bool `json:"active"`
VehicleChargerState string `json:"vehicle_charger_state"`
StartTime string `json:"start_time"`
TimeElapsedSeconds int64 `json:"time_elapsed_seconds"`
TimeRemainingSeconds int64 `json:"time_remaining_seconds"`
PowerKW float64 `json:"power_kw"`
KilometersChargedPerHour float64 `json:"kilometers_charged_per_hour"`
RangeAddedKm float64 `json:"range_added_km"`
TotalChargedEnergyKWh float64 `json:"total_charged_energy_kwh"`
// PackKWh / ThermalKWh / OutletsKWh / SystemKWh come only from the
// Parallax ChargingSessionLiveData breakdown. The regular
// ChargingSession subscription leaves them at zero. Together they
// approximate where the wall energy went: into the pack, into
// thermal management, into 12V outlets / accessories, and into
// other vehicle systems. ThermalKWh is the closest thing Rivian
// gives us to a "battery temperature" signal — high values during
// a session mean the BMS is working hard to heat or cool the pack.
PackKWh float64 `json:"pack_kwh"`
ThermalKWh float64 `json:"thermal_kwh"`
OutletsKWh float64 `json:"outlets_kwh"`
SystemKWh float64 `json:"system_kwh"`
SoCPct float64 `json:"soc_pct"`
CurrentPrice string `json:"current_price"`
CurrentCurrency string `json:"current_currency"`
IsFreeSession bool `json:"is_free_session"`
IsRivianCharger bool `json:"is_rivian_charger"`
// RivianChargerKnown is true when a source reported isRivianCharger
// (only the REST getLiveSessionHistory fetch does), so a false
// IsRivianCharger means "another network", not "not reported".
RivianChargerKnown bool `json:"rivian_charger_known,omitempty"`
// Network is the display name of the charging network billing the
// session (e.g. "Rivian Adventure Network") when a source knows it.
// Rivian's live-session APIs carry no network name, so today only
// the sample corpus sets it; empty means unknown.
Network string `json:"network,omitempty"`
}
LiveSession is the snapshot of an in-progress charging session, pulled from Rivian's `chrg/user/graphql` endpoint via getLiveSessionData. All zero/empty when no session is active. Units: power in kW, energy in kWh, rate in km/h, SoC in percent, time fields in whole seconds.
type LiveSessionCallback ¶
type LiveSessionCallback func(*LiveSession)
LiveSessionCallback fires on every ChargingSession push. Delivered sessions are marked Active — the subscription only emits while live.
type LiveStatePurger ¶ added in v0.40.8
type LiveStatePurger struct {
Rdb *redis.Client
Prefix string
// Again is the delay of the second pass (default 10 s).
Again time.Duration
Log *slog.Logger
}
LiveStatePurger removes every live-state key of a user from Redis (and the planner quota counters, which carry the user id in their names): the in-flight drive and charge accumulators (<prefix>:livestate:<uid>:<vehicle>, which carry coordinates and a path, up to LiveStateTTL) and the published state (…:state). Account deletion calls it so nothing of the user outlives the account in Redis (privacy.md lists only logs and backups as copies kept after deletion).
It uses SCAN with MATCH on the user's own key prefix, in small batches, never KEYS. A monitor that is still running on another pod can write the keys once more in the moments before it notices the deletion and stops, so Purge runs a second pass after Again.
type LiveStateSnapshot ¶
type LiveStateSnapshot struct {
DriveCounter int64 `json:"drive_counter"`
ChargeCounter int64 `json:"charge_counter"`
Drive *LiveDriveSnapshot `json:"drive,omitempty"`
Charge *LiveChargeSnapshot `json:"charge,omitempty"`
}
LiveStateSnapshot is the wire format for a vehicle's open drive/charge accumulator. Fields mirror liveSessions / liveDrive / liveCharge with explicit JSON shapes so the format stays legible and is stable across process versions.
type LiveStateStore ¶
type LiveStateStore interface {
Save(ctx context.Context, vehicleID string, snap LiveStateSnapshot, ttl time.Duration) error
Load(ctx context.Context, vehicleID string) (LiveStateSnapshot, bool, error)
Delete(ctx context.Context, vehicleID string) error
// PutState publishes the vehicle's latest merged live State so a
// replica that doesn't own the WS lease can serve a complete
// /api/state (including subscription-only / Parallax-only fields)
// instead of a REST fallback that omits them. Short-TTL and
// refreshed by the owner on a timer, so a dead owner's snapshot
// ages out and readers fall back to REST.
PutState(ctx context.Context, vehicleID string, st *State, ttl time.Duration) error
// GetState returns the latest published live State for the vehicle,
// or (nil, nil) when none exists (the normal case on the owner pod,
// which never reads its own snapshot).
GetState(ctx context.Context, vehicleID string) (*State, error)
}
LiveStateStore persists per-vehicle in-flight session accumulators across pod restarts and lease handoffs. The recorder upserts the snapshot after every WS-driven frame and rehydrates on first contact for a vehicle, so a freshly elected lease owner picks up the open drive instead of opening a new one and fragmenting the trip.
Save/Load are best-effort: storage failures degrade to fragmentation but must never break the recorder hot path. Implementations must be safe for concurrent use across vehicles.
type MockClient ¶
type MockClient struct {
LoginReturnErr error // if non-nil, Login returns this error
// contains filtered or unexported fields
}
MockClient is a Client implementation backed by in-memory fixture data. It exists so the UI can be demoed without Rivian credentials and so handler-level tests can drive a predictable, offline Client.
The defaults model a stationary Rivian R1T at ~72% SoC. Callers can swap the state out via SetState before serving requests.
MockClient also implements Account — the same sign-in/MFA/logout surface LiveClient exposes — so the UI flow under RIVIAN_CLIENT=mock is identical to production. Any email + password combo succeeds, except:
- email containing "mfa" triggers an MFA challenge that any non-empty OTP clears; this exercises the two-leg flow locally.
- email containing "fail" or "bad" rejects the password so the error state is reachable without tripping real Rivian.
Snapshots round-trip through JSON so secrets.SaveRivianSession can persist them; on restart Restore brings the mock back to its authenticated state, matching the live client's behaviour.
func NewMock ¶
func NewMock() *MockClient
NewMock returns a MockClient primed with one fake vehicle. The client starts logged-out so the Settings UI shows the sign-in panel — call Login with any email+password (or Restore a Session) to unlock Vehicles/State.
func (*MockClient) AuthReady ¶
func (c *MockClient) AuthReady() <-chan struct{}
AuthReady returns a channel closed when the mock has a logged-in session. Replaced by Logout so callers see a fresh signal after the next sign-in.
func (*MockClient) Authenticated ¶
func (c *MockClient) Authenticated() bool
Authenticated reports whether the mock client has completed a successful Login (or been Restored from a persisted session).
func (*MockClient) Email ¶
func (c *MockClient) Email() string
Email returns the email the current session is authenticated as.
func (*MockClient) Login ¶
func (c *MockClient) Login(_ context.Context, cr Credentials) error
Login drives the two-leg auth dance. Returns LoginReturnErr if set so tests can exercise failure paths without patching transport.
func (*MockClient) Logout ¶
func (c *MockClient) Logout()
Logout clears every authenticated-session field.
func (*MockClient) MFAPending ¶
func (c *MockClient) MFAPending() bool
MFAPending reports whether Login returned ErrMFARequired and the client is waiting for an OTP submission.
func (*MockClient) NeedsReauth ¶
func (c *MockClient) NeedsReauth() (bool, string)
NeedsReauth is always (false, "") for the mock client — the mock has no concept of server-side session expiry, and tests that exercise the needs_reauth UI path drive the LiveClient directly.
func (*MockClient) Restore ¶
func (c *MockClient) Restore(s Session)
Restore hydrates the mock from a prior Snapshot.
func (*MockClient) SetState ¶
func (c *MockClient) SetState(vehicleID string, s State)
SetState replaces the fixture state for a vehicle ID. The timestamp is always refreshed to time.Now() so sequential polls look "live".
func (*MockClient) SetVehicles ¶
func (c *MockClient) SetVehicles(vs []Vehicle)
SetVehicles overwrites the fixture vehicle list. Useful for tests that want to assert list-rendering or multi-vehicle pickers.
func (*MockClient) Snapshot ¶
func (c *MockClient) Snapshot() Session
Snapshot returns a serialisable copy of the current session so secrets.SaveRivianSession can round-trip it through JSON. The mock uses UserSessionToken purely as the "we are logged in" sentinel — the contents are not real tokens.
type MonitorRegistry ¶
type MonitorRegistry struct {
// contains filtered or unexported fields
}
MonitorRegistry holds one *StateMonitor per user. Each monitor is wired to that user's *LiveClient and that user's per-user data-plane stores (drives/charges/samples/settings), so the recorder writes land under the correct user_id without any global "current user" assumption.
Lifecycle:
- Start(ctx, uid) is idempotent. It builds and starts a monitor on first call for that uid, no-ops on later calls.
- Stop(uid) cancels that user's monitor ctx and removes the entry. Used on logout / explicit reauth-required transitions.
- For(uid) returns the monitor (or nil if not started). Request handlers use this for /api/state, /api/live-session, etc.
- EnsureSubscribed/Unsubscribe route by rivian_vehicle_id → vehicle owner uid → that user's monitor. The lease coordinator stays user-agnostic; this registry resolves ownership at dispatch time.
- AllVehicleInfo unions every monitor's known vehicles so the coordinator's vehicleSource sees one flat set.
The registry records for live accounts and for replay accounts (RIVIAN_CLIENT=replay, source-fed monitors). Mock and stub paths don't have a recorder.
func NewMonitorRegistry ¶
func NewMonitorRegistry( pool *sql.DB, accounts AccountRegistry, drivesF *drives.Factory, chargesF *charges.Factory, samplesF *samples.Factory, settingsF *settings.Factory, logger *slog.Logger, ) *MonitorRegistry
NewMonitorRegistry constructs a registry. Start(ctx, uid) is what actually launches per-user monitors; the parent ctx passed there (typically the server ctx) bounds every monitor's lifetime.
func (*MonitorRegistry) AddChargeCloseHookFactory ¶ added in v0.36.4
func (r *MonitorRegistry) AddChargeCloseHookFactory(factory func(uid uuid.UUID) ChargeCloseHook)
AddChargeCloseHookFactory registers a further charge-close side effect next to the one SetChargeCloseHookFactory holds (push notifications), so independent features can each react to a closed charge. Every registered hook runs, in order registered, for each close.
func (*MonitorRegistry) AddDriveCloseHookFactory ¶ added in v0.37.5
func (r *MonitorRegistry) AddDriveCloseHookFactory(factory func(uid uuid.UUID) DriveCloseHook)
AddDriveCloseHookFactory registers a further drive-close side effect next to the one SetDriveCloseHookFactory holds (weather), the way AddChargeCloseHookFactory does for charges. Every registered hook runs, in order registered, for each close.
func (*MonitorRegistry) AllVehicleInfo ¶
func (r *MonitorRegistry) AllVehicleInfo() []Vehicle
AllVehicleInfo unions every running monitor's vehicle set. Used by the lease coordinator's vehicleSource so it sees one flat list across all users.
func (*MonitorRegistry) EnsureSubscribed ¶
func (r *MonitorRegistry) EnsureSubscribed(rivianVehicleID string)
EnsureSubscribed routes the rivian_vehicle_id to the owning user's monitor. Owner is resolved via vehicles.user_id; if the owner isn't tracked (no monitor running for that uid) the call is a no-op with a debug log — the lease will get re-evaluated once the owner signs in.
func (*MonitorRegistry) FirstFrameAt ¶ added in v0.40.13
func (r *MonitorRegistry) FirstFrameAt(vehicleID string) time.Time
FirstFrameAt is when the first vehicleState frame of this ownership of the vehicle arrived on any of this pod's monitors, or the zero time. The lease coordinator measures acquire-to-first-frame with it.
func (*MonitorRegistry) FlushPending ¶ added in v0.37.7
func (r *MonitorRegistry) FlushPending(ctx context.Context) int
FlushPending flushes every monitor the registry runs.
func (*MonitorRegistry) For ¶
func (r *MonitorRegistry) For(uid uuid.UUID) *StateMonitor
For returns the monitor for uid, or nil if not started. Safe for concurrent reads.
func (*MonitorRegistry) LateWirings ¶ added in v0.40.15
func (r *MonitorRegistry) LateWirings() int
LateWirings counts setters called after the first monitor was built.
func (*MonitorRegistry) LatestAt ¶ added in v0.40.12
func (r *MonitorRegistry) LatestAt(vehicleID string) time.Time
LatestAt is when the newest frame for the vehicle arrived on any of this pod's monitors, or the zero time when none has. The lease coordinator measures acquire-to-first-frame with it.
func (*MonitorRegistry) RequireFence ¶ added in v0.40.15
func (r *MonitorRegistry) RequireFence(on bool)
RequireFence says lead mode is on: until SetFence installs a source, every group reads as Lost (a write is refused, a Rivian call is not made) instead of Off (unfenced). Call it before any monitor starts, in the same breath as the registry is built.
func (*MonitorRegistry) SessionActivity ¶ added in v0.40.13
func (r *MonitorRegistry) SessionActivity(rivianVehicleID string) (open, asleep bool)
SessionActivity reports whether any monitor on this pod has an open drive or charge for the vehicle, and whether it is asleep (no monitor with an open session, and one whose latest state says sleep). No database lookup: the lease coordinator calls it in its pass.
func (*MonitorRegistry) SetBatteryCapacityHookFactory ¶ added in v0.22.1
func (r *MonitorRegistry) SetBatteryCapacityHookFactory(factory func(uid uuid.UUID) func(ctx context.Context, vehicleID string, kwh float64))
SetBatteryCapacityHookFactory wires a per-user write-through hook for the live-reported usable pack capacity. Each new monitor gets factory(uid) and installs it via SetBatteryCapacityHook; the hook runs whenever Rivian's vehicleState.batteryCapacity changes for one of the user's vehicles. Typically wires through to a SQL UPDATE on vehicles.pack_kwh so a future restart sees the right nameplate without having to wait for another live observation.
func (*MonitorRegistry) SetChargeCloseHookFactory ¶
func (r *MonitorRegistry) SetChargeCloseHookFactory(factory func(uid uuid.UUID) ChargeCloseHook)
SetChargeCloseHookFactory mirrors SetDriveCloseHookFactory for charge sessions. Used to fire per-user notifications and similar side-effects on charge close.
func (*MonitorRegistry) SetDCFCDetectorFactory ¶ added in v0.33.0
func (r *MonitorRegistry) SetDCFCDetectorFactory(factory func(uid uuid.UUID) DCFCDetector)
SetDCFCDetectorFactory wires per-user fast-charge network detection (on only where the dcfc_auto_network flag covers the user; the detector itself says when it's off). Safe to call at boot before any Start.
func (*MonitorRegistry) SetDCFCEstimatorFactory ¶ added in v0.38.8
func (r *MonitorRegistry) SetDCFCEstimatorFactory(factory func(uid uuid.UUID) DCFCEstimator)
SetDCFCEstimatorFactory wires the per-user typical-price fallback for fast charges the user has no rate for. Live recorder only. Safe to call at boot before any Start.
func (*MonitorRegistry) SetDCFCPriceLookupFactory ¶ added in v0.32.0
func (r *MonitorRegistry) SetDCFCPriceLookupFactory(factory func(uid uuid.UUID) DCFCPriceLookup)
SetDCFCPriceLookupFactory wires a per-user fast-charge pricer (the user's network memberships pick the rate). Safe to call at boot before any Start.
func (*MonitorRegistry) SetDriveCloseHookFactory ¶
func (r *MonitorRegistry) SetDriveCloseHookFactory(factory func(uid uuid.UUID) DriveCloseHook)
SetDriveCloseHookFactory wires a per-user DriveCloseHook factory. On each new monitor spawn, factory(uid) is called and the result (may be nil) is handed to the StateMonitor so the recorder can run async post-close enrichment (e.g. weather fetch) as drives are recorded — instead of deferring to a manual SPA backfill. Safe to call at boot before any Start.
func (*MonitorRegistry) SetElevationLookup ¶
func (r *MonitorRegistry) SetElevationLookup(e ElevationLookup)
SetElevationLookup wires an optional elevation resolver shared across every per-user monitor the registry spawns. Safe to call at boot before any Start, idempotent thereafter (the resolver itself is stateless w.r.t. user identity -- altitude is a pure function of lat/lon, not a per-user query).
func (*MonitorRegistry) SetFence ¶ added in v0.40.12
func (r *MonitorRegistry) SetFence(src fence.Source, onFenced func(kind string))
SetParent records the outer ctx all per-user monitors derive their lifetime from. Must be called once at boot before Start. SetFence wires the group-lease fence into every monitor the registry builds (see StateMonitor.SetFence). Call before monitors start.
Monitors do not keep a copy: each gets registryFence, which reads the source installed here at the moment of every check, so this reaches monitors the boot hydrate sweep already started (main installs the lease store after that sweep).
func (*MonitorRegistry) SetLiveStateStoreFactory ¶
func (r *MonitorRegistry) SetLiveStateStoreFactory(factory func(uid uuid.UUID) LiveStateStore)
SetLiveStateStoreFactory wires a per-user LiveStateStore factory. On each new monitor spawn, factory(uid) is called and the result (may be nil) is handed to the StateMonitor so the recorder can rehydrate / persist its in-flight session accumulators across pod restarts and lease handoffs. Safe to call at boot before any Start.
func (*MonitorRegistry) SetParent ¶
func (r *MonitorRegistry) SetParent(ctx context.Context)
func (*MonitorRegistry) SetRouteFiller ¶
func (r *MonitorRegistry) SetRouteFiller(rf RouteFiller)
SetRouteFiller wires the optional GPS-gap fill backend shared across every per-user monitor. Stateless w.r.t. user identity, so one client serves all users.
func (*MonitorRegistry) SetScheduleObserver ¶ added in v0.40.14
func (r *MonitorRegistry) SetScheduleObserver(fn func(outcome string))
SetScheduleObserver wires the schedule-read outcome counter into every monitor the registry builds, those already running included: they call through the registry, they do not keep a copy. (They used to: the boot hydrate sweep starts the monitors before main installs the counter, so on prod every boot-started monitor kept none and the counter read 0 beside real reads.)
func (*MonitorRegistry) SetSessionRehydrator ¶ added in v0.23.3
SetSessionRehydrator wires a callback that loads a user's persisted Rivian session from shared storage and restores it into their cached per-pod client, returning true when the client ends up authenticated. The lease coordinator can hand this pod a vehicle whose owner signed in on a peer pod, so the cached client here predates the session and reports "not authenticated" — the monitor would then wait on AuthReady forever and record nothing. EnsureSubscribed calls this to self-heal without waiting for a pod restart's boot hydrate. Returns false (and the subscribe stays a no-op) when no session is stored.
func (*MonitorRegistry) SetSleepObserver ¶ added in v0.31.2
func (r *MonitorRegistry) SetSleepObserver(o SleepObserver)
SetSleepObserver installs the sleep metrics hook handed to every monitor started afterwards.
func (*MonitorRegistry) SetSleepSafe ¶ added in v0.31.2
func (r *MonitorRegistry) SetSleepSafe(fn func(vehicleID string) bool)
SetSleepSafe installs the per-vehicle sleep-safe check handed to every monitor started afterwards (see StateMonitor.sleepGate).
func (*MonitorRegistry) SetVehicleInfoHookFactory ¶ added in v0.36.4
func (r *MonitorRegistry) SetVehicleInfoHookFactory(factory func(uid uuid.UUID) func(context.Context, []Vehicle))
SetVehicleInfoHookFactory wires the per-user persister for the vehicle metadata (trim, image, pack guess) each monitor fetches in RefreshVehicleInfo.
func (*MonitorRegistry) SetWakeSafety ¶ added in v0.32.0
func (r *MonitorRegistry) SetWakeSafety(fn func(vehicleID string) WakeSafety)
SetWakeSafety installs the wake-safety switch lookup handed to every monitor started afterwards (see wake_safety.go).
func (*MonitorRegistry) Start ¶
func (r *MonitorRegistry) Start(ctx context.Context, uid uuid.UUID) *StateMonitor
Start launches a monitor for uid if one is not already running. Returns the monitor (existing or new). Returns nil when the account is neither a LiveClient nor an authenticated FrameSource (mock/stub paths shouldn't be calling Start; this is a defensive guard).
func (*MonitorRegistry) Stop ¶
func (r *MonitorRegistry) Stop(uid uuid.UUID)
Stop tears down the monitor for uid. Idempotent.
func (*MonitorRegistry) Streaming ¶ added in v0.33.0
func (r *MonitorRegistry) Streaming(rivianVehicleID string) bool
Streaming reports whether this pod runs a subscription for the vehicle on any of its monitors (including one parked waiting for auth or re-auth). The lease coordinator re-subscribes a leased vehicle that isn't streaming (SetStreamingCheck). Any monitor, not just the current owner's: if the owner resolution changed (users sharing a VIN), the old owner's run still counts, so the heal can't start a second subscription beside it. No database lookup: it runs inside every reconcile.
func (*MonitorRegistry) Unsubscribe ¶
func (r *MonitorRegistry) Unsubscribe(rivianVehicleID string)
Unsubscribe tears the vehicle's subscription down on every monitor that runs it, not only its current owner's. After an owner flip (users sharing a VIN) the old owner's run is still the one streaming; routing by owner cancelled the new owner's (a no-op) and left the old one streaming on a pod that no longer holds the lease: a cross-pod duplicate. Unsubscribe on a monitor that doesn't run it is a no-op. No database lookup.
type NetworkPreference ¶
NetworkPreference biases the planner toward or away from a charging network. Preference is an integer rank: lower = more preferred.
type OTAInfo ¶ added in v0.36.5
type OTAInfo struct {
Current *OTAVersion `json:"current,omitempty"`
Available *OTAVersion `json:"available,omitempty"`
// Status is the live otaStatus from the vehicle state (idle,
// downloading, ready_to_install, ...), filled when served, never stored.
Status string `json:"status,omitempty"`
// FetchedAt is when Rivian was last asked.
FetchedAt time.Time `json:"fetched_at"`
}
OTAInfo is what /api/vehicles reports under "ota".
type OTAObserver ¶ added in v0.38.26
type OTAObserver interface {
OnOTAStatus(class string)
}
OTAObserver is optionally implemented by the SleepObserver (main's metrics adapter): told the class of each vehicleState frame's otaStatus.
type OTASource ¶ added in v0.36.5
type OTASource interface {
OTAUpdateDetails(ctx context.Context, vehicleID string) (*OTAInfo, error)
}
OTASource is what the OTA sync needs from a Rivian account; *LiveClient implements it.
type OTAVersion ¶ added in v0.36.5
type OTAVersion struct {
Version string `json:"version"`
ReleaseNotesURL string `json:"-"`
HasNotes bool `json:"has_notes,omitempty"`
Locale string `json:"locale,omitempty"`
}
OTAVersion is one software release: its version string, the release-notes page Rivian publishes for it and the page's locale. Rivian's link is a pre-signed, one-hour URL (it carries an X-Amz-Security-Token), so it is a credential that has expired long before it is tapped: ReleaseNotesURL lives in memory only (json "-": never stored, exported or served), and HasNotes says a link existed. The app asks for a fresh one when the user taps (GET /api/vehicles/{id}/ota/notes). Rivolt never fetches the page itself.
type ParallaxRawFrame ¶ added in v0.27.0
type ParallaxRawFrame struct {
RVM string `json:"rvm"`
Timestamp string `json:"timestamp"`
PayloadB64 string `json:"payload_b64"`
}
ParallaxRawFrame is one captured Parallax message: the RVM topic, its server timestamp, and the raw base64 protobuf payload. Used to reverse-engineer a topic's wire shape from live frames before writing a decoder (the reliable path: field layouts are confirmed against real frames, not assumed).
type ParallaxReplayObserver ¶ added in v0.33.0
type ParallaxReplayObserver interface {
OnParallaxReplayIgnored(topic string)
}
ParallaxReplayObserver is optionally implemented by the SleepObserver (main's metrics adapter): told each time a replayed or stale Parallax value is ignored instead of applied, per topic.
type PendingMFA ¶ added in v0.23.1
type PendingMFA struct {
Email string `json:"email"`
OTPToken string `json:"otp_token"`
CSRFToken string `json:"csrf_token"`
AppSessionToken string `json:"app_session_token"`
IssuedAt time.Time `json:"issued_at"`
}
PendingMFA is the in-flight OTP challenge, captured so a different pod than the one that ran the password leg can finish the second leg. The OTP submission replays the same csrf/appSession tokens and the server-issued otpToken; a pod that never saw the password leg has none of these in memory, so without sharing them the OTP lands on a pod that reports "no challenge in flight".
type PlanFixture ¶ added in v0.37.0
type PlanFixture struct {
Name string
Origin [2]float64 // lat, lon
Destination [2]float64
StartSoC float64
Plan TripPlan
}
PlanFixture is one stored plan and the trip it answers.
type PlanTripInput ¶
type PlanTripInput struct {
VehicleID string
StartingSoC float64
StartingRangeMeters float64 // 0 → derived from SoC by PlanTrip
OriginBearing float64 // degrees from true north; pass 0 if unknown
Waypoints []PlanTripWaypoint
TargetArrivalSocPercent *float64
NetworkPreferences []NetworkPreference
// DriveMode is one of "CONSERVE" / "SPORT" / "ALL_PURPOSE".
// Empty means "let the planner pick its default" (typically
// ALL_PURPOSE). CONSERVE produces materially different routes
// (more stops, longer legs at lower speeds) so this is a real
// user-facing knob.
DriveMode string
// HasAdapter signals whether the vehicle has the Tesla NACS
// adapter. Pointer so we can distinguish "unset" (rely on
// account defaults) from "explicitly false" (block any
// adapter-required Supercharger). Default unset.
HasAdapter *bool
// Fields kept on the input for SPA-side compat / future schemas;
// not forwarded to Rivian's current planTrip schema.
TrailerProfile string
AvoidAdapterRequired bool
SupportedConnectorTypes []string
// RoutePreferences asks the planner to avoid tolls, ferries or highways.
// nil means no preference, and then the request does not mention it.
RoutePreferences *RoutePreferences
// DepartureTime is when the trip leaves; the zero value means "now".
// The fixture and synthetic planners time their waypoints from it.
DepartureTime time.Time
}
PlanTripInput is the request shape for PlanTrip. Mirrors the arguments accepted by the Rivian gateway's planTripWithMultiStop operation. All required fields are non-pointer; optional fields (e.g. TargetArrivalSocPercent, TrailerProfile) are pointers so the zero value is distinguishable from "not provided".
Waypoints[0] is the origin, Waypoints[len-1] is the destination. Slice 1 of the planner only supports those two; intermediate stops aren't part of this schema's planTrip operation. The gateway computes charging stops between origin and destination.
type PlanTripWaypoint ¶
type PlanTripWaypoint struct {
Latitude float64
Longitude float64
WaypointType string
EntityID string
}
PlanTripWaypoint is one input waypoint. waypointType is one of "origin", "destination", or "waypoint" (intermediate stop). EntityID is optional and used when the user picked a specific POI from a places search; the gateway uses it as a hint.
type PlannedWaypoint ¶
type PlannedWaypoint struct {
WaypointType string
EntityID string
Name string
Latitude float64
Longitude float64
MaxPowerKW float64
ChargeDurationSec int
ArrivalSoC float64
DepartureSoC float64
ArrivalReachableMeters float64
DepartureReachableMeters float64
AdapterRequired bool
// UTC timestamps from Rivian's planner, computed from the request
// time as departure. Empty string when Rivian omitted them.
ArrivalTimeUTC string
DepartureTimeUTC string
// DistanceFromOriginMeters is Rivian's cumulative distance from
// the route origin to this waypoint. Powers the "180 mi from
// origin · +120 mi from prev" columns in the SPA stops table.
// Zero on legacy response shapes that don't include the field.
DistanceFromOriginMeters float64
}
PlannedWaypoint is one stop on the planned route. Origin and destination entries appear with WaypointType="origin"/"destination"; every other entry is a charger pick.
type PriceLookup ¶
PriceLookup returns the current home electricity rate and its ISO-4217 currency code. Rate of 0 means "not configured"; callers should leave the persisted cost at zero in that case so it doesn't show up as a misleading $0.00 in history.
type RateLimiter ¶
RateLimiter is the slice of ratelimit.Limiter the rivian package uses. Lives here as an interface to keep this package import-free of the limiter implementation (which depends on go-redis); main.go does the wiring.
Allow returns (true, 0) on grant and (false, retryAfter) when out of budget. retryAfter feeds ErrRateLimited so callers can build a Retry-After response.
type ReceiptSource ¶ added in v0.36.4
type ReceiptSource interface {
CompletedSessionSummaries(ctx context.Context) ([]SessionSummary, error)
SessionCharger(ctx context.Context, transactionID string) (*SessionCharger, error)
}
ReceiptSource is what the receipts sync needs from a Rivian account. *LiveClient implements it.
type Reconcile ¶ added in v0.40.8
Reconcile is what a SyncAll round found: how many loaded accounts still disagreed with the database after the round tried to correct them (by field), and whether the database could not be read at all. Sustained non-zero values mean the sync is not converging.
type ReconnectHold ¶ added in v0.32.0
ReconnectHold is consulted before a subscription the server ended is reopened. It may block; it returns held = true if it waited (the reopen then happens at once, without the backoff wait), and ctx's error if ctx ended while it waited.
type RedisLiveStateStore ¶
type RedisLiveStateStore struct {
// contains filtered or unexported fields
}
RedisLiveStateStore is the production LiveStateStore. Keys are scoped by user so the same Redis cluster is shared across tenants without cross-tenant reads.
func NewRedisLiveStateStore ¶
func NewRedisLiveStateStore(rdb *redis.Client, userID, keyPrefix string) *RedisLiveStateStore
NewRedisLiveStateStore binds rdb to the given userID. Pass the same *redis.Client constructed at boot for the rate limiter. keyPrefix is the install-wide Redis prefix (typically "rivolt" for prod, "rivolt-preview" for the preview env) — the livestate suffix is appended internally so two environments sharing one Redis still own disjoint keyspaces.
func (*RedisLiveStateStore) Delete ¶
func (s *RedisLiveStateStore) Delete(ctx context.Context, vehicleID string) error
Delete drops the snapshot for vehicleID. Used after a normal session close so a future P → D transition can't accidentally rehydrate a closed drive on top of the new one.
func (*RedisLiveStateStore) GetState ¶ added in v0.27.2
GetState reads the published live State for vehicleID. Returns (nil, nil) when no snapshot exists (owner hasn't published, or it aged out) so callers cleanly fall through to their REST path.
func (*RedisLiveStateStore) Load ¶
func (s *RedisLiveStateStore) Load(ctx context.Context, vehicleID string) (LiveStateSnapshot, bool, error)
Load reads the snapshot for vehicleID. The bool is false (with nil error) when no snapshot exists — the normal first-contact case.
func (*RedisLiveStateStore) PutState ¶ added in v0.27.2
func (s *RedisLiveStateStore) PutState(ctx context.Context, vehicleID string, st *State, ttl time.Duration) error
PutState serialises st and writes it under the per-user/vehicle state key with a short expiry. A zero ttl falls back to LiveStateSnapshotTTL.
func (*RedisLiveStateStore) Save ¶
func (s *RedisLiveStateStore) Save(ctx context.Context, vehicleID string, snap LiveStateSnapshot, ttl time.Duration) error
Save serialises snap and writes it under the per-user/vehicle key with an expiry. A zero ttl falls back to LiveStateTTL.
type ReplayClient ¶ added in v0.32.0
type ReplayClient struct {
// contains filtered or unexported fields
}
ReplayClient is the RIVIAN_CLIENT=replay Account: any sign-in succeeds and yields one demo vehicle per user that streams the shared corpus through the recorder. It never opens a network connection.
func NewReplayClient ¶ added in v0.32.0
func NewReplayClient(feed *ReplayFeed, uid uuid.UUID) *ReplayClient
NewReplayClient returns uid's logged-out replay account.
func (*ReplayClient) AuthReady ¶ added in v0.32.0
func (c *ReplayClient) AuthReady() <-chan struct{}
func (*ReplayClient) Authenticated ¶ added in v0.32.0
func (c *ReplayClient) Authenticated() bool
func (*ReplayClient) Email ¶ added in v0.32.0
func (c *ReplayClient) Email() string
func (*ReplayClient) Login ¶ added in v0.32.0
func (c *ReplayClient) Login(_ context.Context, cr Credentials) error
Login accepts any non-empty email and password; there is no MFA leg.
func (*ReplayClient) Logout ¶ added in v0.32.0
func (c *ReplayClient) Logout()
Logout clears the session.
func (*ReplayClient) MFAPending ¶ added in v0.32.0
func (c *ReplayClient) MFAPending() bool
func (*ReplayClient) NeedsReauth ¶ added in v0.32.0
func (c *ReplayClient) NeedsReauth() (bool, string)
func (*ReplayClient) PlanTrip ¶ added in v0.37.0
func (c *ReplayClient) PlanTrip(ctx context.Context, in PlanTripInput) (*TripPlan, error)
PlanTrip implements TripPlanner for the replay client from its planner: no network, no Rivian. Without one it answers ErrNoPlanner.
func (*ReplayClient) Restore ¶ added in v0.32.0
func (c *ReplayClient) Restore(s Session)
Restore re-authenticates from a replay Snapshot and ignores any other session (notably a real Rivian one persisted before the install switched to replay mode).
func (*ReplayClient) Snapshot ¶ added in v0.32.0
func (c *ReplayClient) Snapshot() Session
func (*ReplayClient) State ¶ added in v0.32.0
State returns what the demo vehicle is "reporting" right now — the REST-fallback read for a replica that doesn't own its lease.
func (*ReplayClient) Stream ¶ added in v0.32.0
func (c *ReplayClient) Stream(ctx context.Context, vehicleID string, emit func(st State, catchingUp bool)) error
Stream implements FrameSource for the demo vehicle only. Any other id — e.g. a real vehicle the same user owns, including the corpus vehicle itself — gets ErrVehicleNotFound, so replay can never write into it.
type ReplayCorpus ¶ added in v0.32.0
type ReplayCorpus interface {
// Frames returns the frames in [from, to), oldest first.
Frames(ctx context.Context, from, to time.Time) ([]State, error)
// LastAtOrBefore returns the newest frame at or before t, or nil.
LastAtOrBefore(ctx context.Context, t time.Time) (*State, error)
}
ReplayCorpus is a time-ordered store of recorded frames that a demo vehicle replays. Times are the corpus's own (recorded) times.
type ReplayFeed ¶ added in v0.32.0
type ReplayFeed struct {
// contains filtered or unexported fields
}
ReplayFeed plays a recorded corpus as if it were a live vehicle, time-shifted so the corpus's "now" is the wall clock's now. It is the RIVIAN_CLIENT=replay data source: preview gets moving demo vehicles through the real recorder without any Rivian connection.
func NewDBReplayFeed ¶ added in v0.32.0
func NewDBReplayFeed(ctx context.Context, pool *sql.DB, sourceVehicleID string, from, to time.Time, backfill time.Duration, logger *slog.Logger) (*ReplayFeed, error)
NewDBReplayFeed builds a feed that replays the recorded vehicle_state of the vehicle whose Rivian id is sourceVehicleID, restricted to [from, to) when either is non-zero. Demo vehicles resume from their own latest sample in the same database, falling back to backfill ago.
func NewSampleReplayFeed ¶ added in v0.32.0
NewSampleReplayFeed is the replay feed over the sample corpus (RIVIAN_CLIENT=replay with RIVOLT_REPLAY_SOURCE=sample). pool, when set, provides the resume point (the demo vehicle's latest sample).
func (*ReplayFeed) SetAllowedUsers ¶ added in v0.32.0
func (f *ReplayFeed) SetAllowedUsers(allow func(uid uuid.UUID) bool)
SetAllowedUsers restricts demo vehicles to the given users. nil (the default) allows everyone.
func (*ReplayFeed) Stream ¶ added in v0.32.0
func (f *ReplayFeed) Stream(ctx context.Context, vehicleID string, emit func(st State, catchingUp bool)) error
Stream replays frames for vehicleID from where its recording left off (or backfill ago, whichever is later) up to now as fast as the recorder accepts them, then keeps pace with the wall clock.
type Replayer ¶ added in v0.30.4
type Replayer struct {
// contains filtered or unexported fields
}
Replayer feeds a captured sequence of State frames through the real recorder lifecycle with persistence disabled, collecting the drives it would have produced. It lets recorder/lifecycle changes be regression- tested against real (DB-exported) or hand-authored frame streams offline — no database and no live Rivian feed (which preview can no longer provide; see docs/REPLAY_HARNESS.md and the preview-rivian-disconnected note).
Frames are full State snapshots (the shape a vehicle_state row exports to), fed in arrival order — NOT deltas — so each frame is applied as-is, exactly as the merged cache state the live callback hands the recorder.
func NewReplayer ¶ added in v0.30.4
NewReplayer builds a persistence-free monitor for one vehicle id. All stores are left nil, so the recorder's writes are no-ops (the SetStores contract) and only the in-memory lifecycle runs; closed drives are captured via the drive-close hook.
func (*Replayer) Charges ¶ added in v0.33.0
Charges returns the charge sessions the replay closed, after the same settle wait as Drives (the close hook also runs in a goroutine).
func (*Replayer) Drives ¶ added in v0.30.4
Drives returns the drives the replay produced. The recorder fires the close hook in a goroutine, so this waits for the collection to go quiet (no new drive for a short settle window) before reading it back — safe to call once after the last Feed.
type ResultObserver ¶ added in v0.36.15
type ResultObserver interface {
OnResult(op, class string)
}
ResultObserver receives the outcome of every call that reached Rivian's GraphQL gateway: op is the operation name (a fixed set), class is "success" or the ErrorClass label (transient, outage, rate_limited, user_action, unknown). main.go maps it onto rivolt_rivian_results_total, which the auth-failure alert keys on. Calls stopped before the network (kill switch, breaker open, rate limit) and our own cancellations are not reported.
type RivoltAction ¶ added in v0.32.0
type RivoltAction struct {
At time.Time
Kind string // "subscribe" | "unsubscribe" | "request"
Op string // GraphQL operation name
Detail string // subscription topics, if any
}
RivoltAction is one request Rivolt made to Rivian.
func (RivoltAction) String ¶ added in v0.32.0
func (a RivoltAction) String() string
type RouteFiller ¶
type RouteFiller interface {
RouteShape(ctx context.Context, from, to [2]float64) ([][2]float64, error)
}
RouteFiller fills GPS-lag gaps in a live drive with a routing-engine shape between the last good fix and the next one. Returned slice includes both endpoints. Implemented by *maps.Valhalla; abstracted so the rivian package doesn't depend on internal/maps.
type RoutePreferences ¶ added in v0.37.0
RoutePreferences are the road types a trip should avoid.
func (*RoutePreferences) Any ¶ added in v0.37.0
func (p *RoutePreferences) Any() bool
Any reports whether at least one preference is set.
type SampleCorpus ¶ added in v0.32.0
type SampleCorpus struct {
// contains filtered or unexported fields
}
SampleCorpus is the generated corpus.
func NewSampleCorpus ¶ added in v0.32.0
func NewSampleCorpus(seed int64) *SampleCorpus
NewSampleCorpus plans the corpus for seed.
The corpus starts and ends the same way, at home on a weekday morning, plugged in at 80 % with the car awake shortly before leaving, so the replay's wrap from one loop to the next (sampleLoopGap) reads as just another morning, not a jump.
func (*SampleCorpus) Frames ¶ added in v0.32.0
Frames implements ReplayCorpus: frames in [from, to), oldest first.
func (*SampleCorpus) LastAtOrBefore ¶ added in v0.32.0
LastAtOrBefore implements ReplayCorpus.
func (*SampleCorpus) Summary ¶ added in v0.32.0
func (c *SampleCorpus) Summary() SampleSummary
Summary describes the corpus.
func (*SampleCorpus) WriteJSONL ¶ added in v0.32.0
func (c *SampleCorpus) WriteJSONL(w io.Writer) error
WriteJSONL writes every frame as one replay-format JSON line.
type SamplePlace ¶ added in v0.34.0
SamplePlace is a named point of a sample plan.
type SampleSummary ¶ added in v0.32.0
type SampleSummary struct {
Frames int
Drives int
HomeL2 int
TopUps int
DCFC int
AwayAC int
FailedSleeps int
RoadTripMiles []float64
MinSoC, MaxSoC float64
Start, End time.Time
OdometerStartMi float64
OdometerEndMi float64
}
SampleSummary describes what a corpus contains, for tests and the CLI.
type SampleTripPlan ¶ added in v0.34.0
type SampleTripPlan struct {
Name string
Origin, Destination SamplePlace
DepartAt time.Time // planned departure
SavedAt time.Time // when the owner saved it
StartSoC float64
TargetSoC float64
PackKWh float64 // the sample truck's, for pricing the plan
// OutingStart and OutingEnd bound the whole road trip (home to
// home) the plan's leg belongs to, on this loop.
OutingStart, OutingEnd time.Time
Plan TripPlan
}
SampleTripPlan is one of the sample owner's saved plans, placed on the wall clock for one loop of the replay.
func SampleTripPlans ¶ added in v0.34.0
func SampleTripPlans(now, since time.Time) []SampleTripPlan
SampleTripPlans returns the sample owner's saved plans for the latest sample road trip that is over by now (its outing back home at or before now) and started at or after since, or nil when there is none. A trip's plans stay on that loop until the next loop's trip is over, so a plan never points at an outing still under way; since keeps it off a trip recorded only in part.
func SampleTwoDayPlan ¶ added in v0.37.0
func SampleTwoDayPlan(now, since time.Time) []SampleTripPlan
SampleTwoDayPlan returns the two days of the sample owner's two-day Las Vegas plan, placed on the same loop as SampleTripPlans' (nil when there is none). Day 1 is the way-out plan. Day 2 is the way home as a multi-day plan chains it: it leaves with the charge the hotel's L2 puts in overnight (SampleOvernightMaxSoC; the stay allows far more), not with what the truck had left after a day of driving around town.
type Session ¶
type Session struct {
Email string `json:"email"`
CSRFToken string `json:"csrf_token"`
AppSessionToken string `json:"app_session_token"`
UserSessionToken string `json:"user_session_token"`
AuthenticatedAt time.Time `json:"authenticated_at"`
}
Session is the persistable subset of LiveClient state.
type SessionCharger ¶ added in v0.36.4
type SessionCharger struct {
Name string
Vendor string
// IsHome is Rivian's isHomeCharger; nil when not reported.
IsHome *bool
}
SessionCharger is the little the detail call is allowed to return.
type SessionSummary ¶ added in v0.36.4
type SessionSummary struct {
TransactionID string
StartedAt time.Time
EndedAt time.Time
// TotalCost is what Rivian billed; nil when the session has no cost
// (the app shows nothing for it). Currency is its ISO code.
TotalCost *float64
Currency string
EnergyKWh *float64
RangeAddedKm *float64
Vendor string
City string
}
SessionSummary is one row of Rivian's Charging history.
type SleepObserver ¶ added in v0.31.2
SleepObserver is told about every completed sleep: how long the car stayed asleep and whether that counts as a failed sleep. main wires it to Prometheus; keeping it an interface keeps metrics out of this package.
type Stagger ¶ added in v0.37.6
type Stagger struct {
// contains filtered or unexported fields
}
Stagger hands out time slots at least interval apart. The monitors share one (MonitorRegistry.SetStagger), so the first connect of every subscription in the process is spaced, not only the ones of a single user. A nil or zero-interval Stagger never waits.
func NewStagger ¶ added in v0.37.6
NewStagger returns a Stagger with the given spacing (<= 0: none).
type State ¶
type State struct {
// At is wall-clock time when this snapshot was assembled by the
// recorder (REST or WS receive time). Always monotonic with the
// process clock. Use LocationFixAt for the actual GPS-fix age.
// Earlier code sourced this from GNSSLocation.TimeStamp which
// could be hours stale (parking garage, sleeping car) and caused
// drive/charge sessions to fragment because lifecycle gap checks
// saw spurious 38h "gaps".
At time.Time `json:"at"`
// LocationFixAt is the timestamp Rivian reports on the GNSS
// location field — i.e. when the GPS receiver last had a lock.
// Zero when not present in the payload. May lag At by hours
// when the car is parked indoors.
LocationFixAt time.Time `json:"location_fix_at,omitempty"`
VehicleID string `json:"vehicle_id"`
BatteryLevelPct float64 `json:"battery_level_pct"` // 0..100
// BatteryCapacityKWh is the vehicle's self-reported usable pack
// capacity in kWh (batteryCapacity field on vehicleState). Zero
// when the field wasn't included in the subscription push or
// when an older firmware doesn't emit it.
BatteryCapacityKWh float64 `json:"battery_capacity_kwh"`
DistanceToEmpty float64 `json:"distance_to_empty"` // kilometers
OdometerKm float64 `json:"odometer_km"`
Gear string `json:"gear"` // "P" | "R" | "N" | "D"
DriveMode string `json:"drive_mode"` // "everyday" | "sport" | ...
ChargerState string `json:"charger_state"` // "charging_active" | "charger_disconnected" | ...
ChargerPowerKW float64 `json:"charger_power_kw"`
ChargeTargetPct float64 `json:"charge_target_pct"` // 0..100
// ChargerStatus is the physical plug state ("chrgr_sts_connected_charging",
// "chrgr_sts_not_connected", ...) — different from ChargerState which is
// the session state.
ChargerStatus string `json:"charger_status"`
ChargePortState string `json:"charge_port_state"` // "open" | "closed" | ""
RemoteChargingAvailable string `json:"remote_charging_available"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
SpeedKph float64 `json:"speed_kph"`
HeadingDeg float64 `json:"heading_deg"`
AltitudeM float64 `json:"altitude_m"`
// Aggregate closures / locks.
Locked bool `json:"locked"` // all LOCK_STATE_ENTITIES not unlocked
DoorsClosed bool `json:"doors_closed"`
FrunkClosed bool `json:"frunk_closed"`
LiftgateClosed bool `json:"liftgate_closed"`
TailgateClosed bool `json:"tailgate_closed"`
TonneauClosed bool `json:"tonneau_closed"`
// Climate / power.
CabinTempC float64 `json:"cabin_temp_c"`
OutsideTempC float64 `json:"outside_temp_c"`
CabinPreconditioningStatus string `json:"cabin_preconditioning_status"`
// PowerState is the vehicle's high-level power mode reported by the
// gateway: "sleep" | "standby" | "ready" | "go" | "vehicle_reset" |
// "ota_update" | "shutdown" | "" (unknown). Mirrors home-assistant-rivian's powerState sensor.
PowerState string `json:"power_state"`
// ReplaySession is what Rivian's live charging session would report
// alongside this frame (the running billed price at a Rivian
// Adventure Network stop), carried only by replayed frames (the
// sample corpus). The replay source applies it as a live session
// after recording the frame; it never reaches the cache or JSON.
ReplaySession *LiveSession `json:"-"`
// PowerStateAt is when the car reported PowerState (Rivian's
// powerState.timeStamp, or the Parallax power.state frame time);
// zero when unknown. Anchors wake attribution (actions.go).
PowerStateAt time.Time `json:"-"`
// ChargingSchedules are the vehicle's scheduled charging windows
// (charging_schedule.go), read on connect, daily and at plug-in; nil
// until the first read. They explain a car that is plugged in and not
// charging.
ChargingSchedules []ChargingSchedule `json:"charging_schedules,omitempty"`
// vehicle's schedules with the account (shared or authorized-driver
// accounts get FORBIDDEN). Not an error: the Now line just has nothing
// to say. Re-read after a week.
ChargingSchedulesUnavailable bool `json:"charging_schedules_unavailable,omitempty"`
// ChargerStatusPx marks a ChargerStatus taken from Parallax
// charging.session.status, on a vehicle whose vehicleState sends none
// (an R2). The recorder trusts it for "plugged" but keeps its power
// inference as the fallback.
ChargerStatusPx bool `json:"-"`
// VehicleStateFrameAt rides only on the snapshot the lease owner
// publishes for peer pods (liveStatePublisher), so they can serve
// /now's vehicle_state_frame_at; never set on the cache.
VehicleStateFrameAt *time.Time `json:"vehicle_state_frame_at,omitempty"`
// Safety / maintenance.
AlarmSoundStatus string `json:"alarm_sound_status"`
TwelveVoltBatteryHealth string `json:"twelve_volt_battery_health"`
WiperFluidState string `json:"wiper_fluid_state"`
// Software (OTA). Available version may equal current when nothing is
// pending. Install progress is 0..100.
OtaCurrentVersion string `json:"ota_current_version"`
OtaAvailableVersion string `json:"ota_available_version"`
OtaStatus string `json:"ota_status"`
OtaInstallProgress float64 `json:"ota_install_progress"`
// Tires. Pressures reported in bar (per home-assistant-rivian's
// native unit declaration). Status is a string like "Normal" /
// "Low" / "" (unknown).
TirePressureFLBar float64 `json:"tire_pressure_fl_bar"`
TirePressureFRBar float64 `json:"tire_pressure_fr_bar"`
TirePressureRLBar float64 `json:"tire_pressure_rl_bar"`
TirePressureRRBar float64 `json:"tire_pressure_rr_bar"`
TirePressureStatusFL string `json:"tire_pressure_status_fl"`
TirePressureStatusFR string `json:"tire_pressure_status_fr"`
TirePressureStatusRL string `json:"tire_pressure_status_rl"`
TirePressureStatusRR string `json:"tire_pressure_status_rr"`
// High-voltage battery pack cell temperatures (°C). Not in the
// legacy vehicleState selection — merged in from the Parallax
// energy.high_voltage.battery_state topic (see parallax.go). Zero
// means "not yet reported" (the topic only streams while awake).
PackTempAvgC float64 `json:"pack_temp_avg_c"`
PackTempMaxC float64 `json:"pack_temp_max_c"`
PackTempMinC float64 `json:"pack_temp_min_c"`
// Charging context populated during an active session. Zero/empty
// outside a session — clients gate UI on ChargerState first.
// TimeToEndMins: minutes to a full charge at the
// current rate (0 when not charging or undefined).
// TripTargetSoCPct: Rivian's recommended target SoC
// for the active trip plan, 0..100.
// TripTargetMinsRemaining: minutes until the trip target
// SoC is hit at the current rate.
// ChargerDerateStatus: "active" / "inactive" / "" — set
// when Rivian flags the station as derated below nameplate.
// ChargingDisabledAll: "true" / "false" / "" — when
// "true", the car refuses to charge regardless of cable state.
// ChargingTimeEstimationValidity: "valid" / "invalid" / "" —
// SPA hides the time estimate when invalid.
TimeToEndMins float64 `json:"time_to_end_mins"`
// TimeToEndSource is "estimate" when TimeToEndMins came from Parallax
// charging.session.time_estimation (an R2's vehicleState reports none),
// "" when the vehicle's own vehicleState gave it.
TimeToEndSource string `json:"time_to_end_source,omitempty"`
TripTargetSoCPct float64 `json:"trip_target_soc_pct"`
TripTargetMinsRemaining float64 `json:"trip_target_mins_remaining"`
ChargerDerateStatus string `json:"charger_derate_status"`
ChargingDisabledAll string `json:"charging_disabled_all"`
ChargingTimeEstimationValidity string `json:"charging_time_estimation_validity"`
// TrailerState is whether a trailer is connected, from Parallax body.trailer.state: "present", "not_present" or ""
// (unknown). The connector, not the load (see trailer.go).
TrailerState string `json:"trailer_state,omitempty"`
// Driver identity + special modes. Empty when the field hasn't
// been emitted by the subscription yet (e.g. brand-new tab) or
// when the mode isn't active.
ActiveDriverName string `json:"active_driver_name"`
GearGuardLocked string `json:"gear_guard_locked"`
PetModeStatus string `json:"pet_mode_status"`
PetModeTemperatureStatus string `json:"pet_mode_temperature_status"`
CabinHoldStatus string `json:"cabin_hold_status"`
// Window-closed states. Surfaced as raw Rivian enum strings
// instead of bool because the closure vocabulary for windows
// is much richer than doors ("opened", "open_allowed",
// "obstructed_while_opening_close_allowed", "closed", …). The
// SPA title-cases the value so the user sees the truth without
// us guessing at a closed/open derivation.
WindowFrontLeftClosed string `json:"window_front_left_closed"`
WindowFrontRightClosed string `json:"window_front_right_closed"`
WindowRearLeftClosed string `json:"window_rear_left_closed"`
WindowRearRightClosed string `json:"window_rear_right_closed"`
}
State is a point-in-time snapshot of one vehicle. Units are metric at the wire; the UI converts for display based on user preference.
type StateCallback ¶
type StateCallback func(*State)
StateCallback fires on every state update pushed over the subscription. Called from the websocket receiver goroutine — the implementation should be quick (copy to a channel, update a cache, etc.) and must not block on I/O.
type StateMonitor ¶
type StateMonitor struct {
// contains filtered or unexported fields
}
StateMonitor maintains a websocket subscription per vehicle and caches the latest pushed State. Callers read via Latest(); the subscription keeps it fresh in the background. Use Start() once per process to kick off the monitoring goroutines.
Intended usage: one StateMonitor wrapping the live Client. The HTTP handler calls monitor.Latest(vehicleID) instead of client.State(), getting a cache-hit response without upstream cost. Missing-entry reads trigger a REST fallback to prime the cache while the subscription catches up.
func NewSourceMonitor ¶ added in v0.32.0
func NewSourceMonitor(src FrameSource, vehicles []Vehicle, logger *slog.Logger) *StateMonitor
NewSourceMonitor returns a monitor whose vehicles are fed by src instead of a Rivian subscription — the replay/demo path. It has no client, so nothing it does can reach Rivian. vehicles seeds the metadata cache that RefreshVehicleInfo would otherwise fetch.
func NewStateMonitor ¶
func NewStateMonitor(client *LiveClient, logger *slog.Logger) *StateMonitor
NewStateMonitor wraps a live client. Pass a logger (usually from main.go's structured logger). nil is allowed; events will be discarded.
func (*StateMonitor) ActiveDrive ¶
func (m *StateMonitor) ActiveDrive(vehicleID string) *LiveDrive
ActiveDrive returns a snapshot of the in-flight drive session for the given vehicle, or nil if no drive is currently open. The snapshot is derived from the recorder's accumulator under sessMu so callers see a consistent view even while telemetry frames are being folded in on another goroutine. Energy and efficiency are computed from the SoC delta × per-vehicle pack size — the same fallback the charge recorder uses for home-AC sessions.
func (*StateMonitor) AllVehicleInfo ¶
func (m *StateMonitor) AllVehicleInfo() []Vehicle
AllVehicleInfo returns a snapshot of every cached vehicle record. Used by the HTTP /api/vehicles endpoint.
func (*StateMonitor) EnsureSubscribed ¶
func (m *StateMonitor) EnsureSubscribed(vehicleID string)
EnsureSubscribed guarantees a background subscription exists for the given vehicle. Safe to call concurrently; the first caller wins, subsequent callers are no-ops. If the subscription dies (e.g. ctx cancelled after an unauthenticated error) it stays removed so a future call can retry.
func (*StateMonitor) FirstFrameAt ¶ added in v0.40.13
func (m *StateMonitor) FirstFrameAt(vehicleID string) time.Time
FirstFrameAt is when this ownership's first vehicleState frame for the vehicle arrived, or the zero time when none has since the subscription started or restarted.
func (*StateMonitor) FlushPending ¶ added in v0.37.7
func (m *StateMonitor) FlushPending(ctx context.Context) int
FlushPending writes every in-progress drive and charge whose periodic write was skipped by the throttle, so the stored rows are current. Called on shutdown (before the leases are released) and, per vehicle, when a lease is handed off, so a normal rollout loses nothing; only a kill -9 can lose up to liveTouchInterval of a session's totals. Returns how many rows it wrote.
func (*StateMonitor) Latest ¶
func (m *StateMonitor) Latest(vehicleID string) (*State, time.Time)
Latest returns the most recently pushed state for a vehicle, along with the wall-clock time it was received, or (nil, zero) if nothing has arrived yet.
func (*StateMonitor) LatestLiveSession ¶
func (m *StateMonitor) LatestLiveSession(vehicleID string) *LiveSession
LatestLiveSession returns the last charging-session snapshot observed for the vehicle, whichever source got there first — the WebSocket ChargingSession subscription or the REST getLiveSessionHistory poller. Callers should treat the result as read-only; it may be nil if no session has ever been seen for this vehicle.
func (*StateMonitor) LiveSessionForCharge ¶ added in v0.39.1
func (m *StateMonitor) LiveSessionForCharge(vehicleID string) (*LiveSession, ChargeNow)
LiveSessionForCharge is what the live-session endpoint serves: the cached charging session only when it belongs to the vehicle's open charge, and the energy that charge has recorded so far. The cache can hold the previous plug-in's session (Parallax's charge breakdown counts per plug-in and the cloud replays the last snapshot), whose total and power are not this charge's: the recorder already refuses to credit such a session to a row, and the endpoint now does the same. With no open charge (a charge that has not opened yet, or none) there is no session to serve.
func (*StateMonitor) PackKWhFor ¶
func (m *StateMonitor) PackKWhFor(vehicleID string) float64
PackKWhFor returns the best-known usable pack capacity for the vehicle, falling back to DefaultPackKWh when no metadata is cached. Used by the recorder's SoC-delta energy fallback.
func (*StateMonitor) Prime ¶
func (m *StateMonitor) Prime(vehicleID string, st *State)
Prime stores a state from an out-of-band source (typically a REST fallback on first request) so subsequent Latest() calls return it immediately while the subscription is still spinning up.
func (*StateMonitor) RefreshVehicleInfo ¶
func (m *StateMonitor) RefreshVehicleInfo(ctx context.Context) error
RefreshVehicleInfo pulls the vehicles list + configurator images from Rivian's gateway and caches a per-vehicle metadata record including model, trim, inferred pack kWh, and a 3/4 image URL. Called once at startup (best-effort); errors are returned to the caller so they can log but shouldn't be fatal. Missing images are not an error — PackKWh still gets populated from the vehicles query.
func (*StateMonitor) RemoteLatest ¶ added in v0.27.2
func (m *StateMonitor) RemoteLatest(ctx context.Context, vehicleID string) *State
RemoteLatest returns the live State a peer pod published to the shared store for this vehicle, or nil when none is available (no shared store wired, the owner hasn't published yet, or the snapshot aged out). Used by the /api/state read path on a replica that does NOT own the vehicle's WS lease: its local cache is empty and a direct REST fetch would omit every subscription-only field (pack temperature, numeric tire pressures, charging context, driver chips, windows). Reading the owner's published snapshot serves those fields pod-agnostically.
A snapshot read is reused for remoteLatestTTL: the owner republishes only every liveStateSnapshotInterval, so /now and /api/state polls (the public demo's visitors all poll the same vehicle) share one store read instead of one each. Misses aren't cached.
func (*StateMonitor) ServeState ¶ added in v0.33.0
ServeState decides what /api/state serves for vehicleID. It returns the state to serve and whether the caller should first fetch it directly (then Prime the result, or on failure serve st if non-nil):
- this pod streams the vehicle: its own cache, the freshest there is;
- otherwise the owner's published snapshot (RemoteLatest);
- otherwise the local entry (a past fetch, or the last frame before this pod lost the lease), with fetch=true at most once per stateRefetchEvery per vehicle on this pod, and never while that entry says the vehicle is asleep: this path has no sleep gate of its own, and a Redis outage must not fan out into Rivian calls;
- no entry: fetch=true under the same limit, else (nil, false).
Serving the local entry first, as Latest-first did, froze /api/state on a non-owner pod from its first fallback fetch until it restarted.
func (*StateMonitor) SessionActivity ¶ added in v0.40.13
func (m *StateMonitor) SessionActivity(vehicleID string) (open, asleep bool)
SessionActivity reports whether the recorder has an open drive or charge for vehicleID, and whether its latest state says it is asleep. The lease coordinator never sheds the group of a vehicle with an open session, and prefers a sleeping one (design 4.7).
func (*StateMonitor) SetBatteryCapacityHook ¶ added in v0.22.1
func (m *StateMonitor) SetBatteryCapacityHook(hook func(ctx context.Context, vehicleID string, kwh float64))
SetBatteryCapacityHook wires a write-through callback fired whenever observeBatteryCapacity records a new pack capacity for a vehicle. The hook is called outside the monitor lock and MUST be non-blocking — callers typically dispatch a goroutine that performs the DB UPDATE.
func (*StateMonitor) SetChargeCloseHook ¶
func (m *StateMonitor) SetChargeCloseHook(h ChargeCloseHook)
SetChargeCloseHook wires a post-close callback for charge sessions. nil disables. Safe to call before Start.
func (*StateMonitor) SetDCFCDetector ¶ added in v0.33.0
func (m *StateMonitor) SetDCFCDetector(fn DCFCDetector)
SetDCFCDetector wires fast-charge network detection (see dcfcDetect).
func (*StateMonitor) SetDCFCEstimator ¶ added in v0.38.8
func (m *StateMonitor) SetDCFCEstimator(fn DCFCEstimator)
SetDCFCEstimator wires the live typical-price fallback (see dcfcEstimate).
func (*StateMonitor) SetDCFCPriceLookup ¶ added in v0.32.0
func (m *StateMonitor) SetDCFCPriceLookup(fn DCFCPriceLookup)
SetDCFCPriceLookup wires the fast-charge pricer (see dcfcPrice).
func (*StateMonitor) SetDriveCloseHook ¶
func (m *StateMonitor) SetDriveCloseHook(h DriveCloseHook)
SetDriveCloseHook wires a post-close enrichment callback (e.g. weather fetch). nil disables. Safe to call before Start; racy if called after subscriptions are running.
func (*StateMonitor) SetElevationLookup ¶
func (m *StateMonitor) SetElevationLookup(e ElevationLookup)
SetElevationLookup wires an optional elevation resolver. nil disables altitude annotation; samples will be written with NULL altitude_m. Safe to call before Start; racy after.
func (*StateMonitor) SetFence ¶ added in v0.40.12
func (m *StateMonitor) SetFence(src fence.Source, onFenced func(kind string))
SetFence wires the group-lease fence: src says what this pod may write for a vehicle, and onFenced is told (with the write class) each time a write is refused or skipped because the lease is gone. Safe to call before Start.
func (*StateMonitor) SetHomeLookup ¶ added in v0.32.0
func (m *StateMonitor) SetHomeLookup(fn HomeLookup)
SetHomeLookup wires the home-location lookup (see homeLookup).
func (*StateMonitor) SetLiveStateStore ¶
func (m *StateMonitor) SetLiveStateStore(s LiveStateStore)
SetLiveStateStore wires the cross-restart accumulator persistence store. nil disables persistence and rehydration; the recorder runs purely from in-memory state (the pre-Redis behaviour). Safe to call before Start; racy if called after subscriptions are running.
func (*StateMonitor) SetPriceLookup ¶
func (m *StateMonitor) SetPriceLookup(fn PriceLookup)
SetPriceLookup wires a callback the recorder uses to stamp each closed charge with the current home $/kWh rate. Safe to call at any time; a nil value disables cost snapshotting.
func (*StateMonitor) SetRouteFiller ¶
func (m *StateMonitor) SetRouteFiller(r RouteFiller)
SetRouteFiller wires the GPS-gap fill backend. nil disables gap filling; long lags will leave a straight-line shortcut in the drive polyline (the pre-Valhalla behavior). Safe to call before Start; racy after.
func (*StateMonitor) SetScheduleObserver ¶ added in v0.40.14
func (m *StateMonitor) SetScheduleObserver(fn func(outcome string))
SetScheduleObserver wires the schedule-read outcome counter: ok, retried_ok (worked after at least one refusal by the shared budget), dropped (refused on every try) and error (any other failure). A vehicle whose account has no schedules ("not available") is not counted. Safe before Start.
func (*StateMonitor) SetSleepObserver ¶ added in v0.31.2
func (m *StateMonitor) SetSleepObserver(o SleepObserver)
SetSleepObserver installs the sleep metrics hook. nil is allowed.
func (*StateMonitor) SetSleepSafe ¶ added in v0.31.2
func (m *StateMonitor) SetSleepSafe(fn func(vehicleID string) bool)
SetSleepSafe installs the per-vehicle sleep-safe check (normally the flags store's SleepSafe().Covers). nil disables gating; the gate still observes.
func (*StateMonitor) SetStagger ¶ added in v0.37.6
func (m *StateMonitor) SetStagger(s *Stagger)
SetStagger replaces the monitor's slot clock; the registry passes one shared instance to every monitor. Call before Start.
func (*StateMonitor) SetStores ¶
func (m *StateMonitor) SetStores(samplesStore *samples.Store, drivesStore *drives.Store, chargesStore *charges.Store)
SetStores wires the recording stores. All three are optional — pass nil to disable that particular writer. Safe to call before Start; racy if called after subscriptions are running.
func (*StateMonitor) SetVehicleInfoHook ¶ added in v0.36.4
func (m *StateMonitor) SetVehicleInfoHook(h func(context.Context, []Vehicle))
SetVehicleInfoHook wires the persister RefreshVehicleInfo calls with the vehicles it just cached. nil disables. Safe to call before Start.
func (*StateMonitor) SetWakeSafety ¶ added in v0.32.0
func (m *StateMonitor) SetWakeSafety(fn func(vehicleID string) WakeSafety)
SetWakeSafety installs the per-vehicle switch lookup (normally the flags store's wake_safety). nil turns every switch off.
func (*StateMonitor) Start ¶
func (m *StateMonitor) Start(ctx context.Context)
Start binds the monitor to a parent context. All subscriptions started via EnsureSubscribed use a child context derived from this parent; cancelling parent tears them all down.
func (*StateMonitor) Subscribed ¶ added in v0.33.0
func (m *StateMonitor) Subscribed(vehicleID string) bool
Subscribed reports whether a subscription goroutine is running for vehicleID (EnsureSubscribed started it and it hasn't exited).
func (*StateMonitor) Unsubscribe ¶
func (m *StateMonitor) Unsubscribe(vehicleID string)
Unsubscribe tears down the background subscription for vehicleID if one is running, and is a no-op otherwise. Used by the lease coordinator when this pod loses ownership of a vehicle to a peer.
Cancellation is async — the run goroutine observes ctx.Done() the next time it loops around. Everything this pod knew about the vehicle is forgotten (forgetVehicle): the new owner is the source of state from here on, and /api/state on this pod follows its published snapshot.
func (*StateMonitor) VehicleInfo ¶
func (m *StateMonitor) VehicleInfo(vehicleID string) *Vehicle
VehicleInfo returns the cached per-vehicle metadata record, or nil if RefreshVehicleInfo hasn't been called (or hasn't seen this vehicle yet). The returned pointer is a copy; safe to read without holding the lock.
func (*StateMonitor) VehicleStateFrameAt ¶ added in v0.33.2
VehicleStateFrameAt is when the vehicleState feed last delivered a frame for the vehicle (GET /now's vehicle_state_frame_at), or nil when there is no live feed to judge: this pod doesn't stream the vehicle and the owner hasn't published one, the subscription hasn't delivered yet, or it is held while the car sleeps (wake-safety HoldAsleep). A non-nil time that's old means the feed is stuck. A pod that doesn't stream the vehicle reads the owner's published snapshot (refreshed every liveStateSnapshotInterval; gone after LiveStateSnapshotTTL once the owner stops).
type StubClient ¶
type StubClient struct{}
StubClient is a safe-to-boot placeholder so cmd/rivolt compiles and the HTTP server comes up even before the real client is wired.
func NewStub ¶
func NewStub() *StubClient
NewStub returns a StubClient. Use this in main.go until the live client is in.
func (*StubClient) Login ¶
func (*StubClient) Login(context.Context, Credentials) error
Login returns ErrNotImplemented.
type SyncChange ¶ added in v0.40.7
type SyncChange int
SyncChange says what the syncer did to an account, for the pod's other parts (the monitor for that user) to follow.
const ( // SyncSessionReplaced: another pod stored a different session and this pod now // holds it. A monitor running on the old tokens should restart on the new ones. SyncSessionReplaced SyncChange = iota + 1 // SyncLoggedOut: the session was removed elsewhere and the local copy dropped. SyncLoggedOut // SyncUserRemoved: the users row is gone (the account was deleted): the account was dropped // from the registry. The monitor stops, and whatever the pod holds of the user in Redis is // theirs to delete (after the monitor is stopped, or it writes it again). SyncUserRemoved )
func (SyncChange) String ¶ added in v0.40.7
func (c SyncChange) String() string
type TireObserver ¶ added in v0.38.28
type TireObserver interface {
OnTireStatus(class string)
}
TireObserver is optionally implemented by the SleepObserver (main's metrics adapter): told the class of each wheel's status on a frame that carries tire data.
type TirePressure ¶ added in v0.27.4
TirePressure is one decoded wheel from a dynamics.tires.state frame. Position 1=FL, 2=FR, 3=RL, 4=RR; Bar is the pressure in bar (matches vehicleState's tire_pressure_*_bar). Confirmed live 2026-07-14: pressures 3.25/3.28/3.25/3.25 bar vs vehicleState tire_min_bar 3.25.
type TripPlan ¶
type TripPlan struct {
Status string
ChargeStationsAvailable bool
SoCBelowLimit bool
Routes []TripRoute
// Simulated is true for a plan that did not come from the gateway
// (fixtures or the synthetic planner on preview). The SPA badges it.
Simulated bool `json:"simulated,omitempty"`
}
TripPlan is the cleaned-up response. The gateway returns one or more candidate routes; first is typically the recommended one.
type TripPlanner ¶ added in v0.37.0
type TripPlanner interface {
PlanTrip(ctx context.Context, in PlanTripInput) (*TripPlan, error)
}
TripPlanner is what the trip-plan handlers need from a client: one planned trip for one input. *LiveClient implements it against the gateway; the replay client implements it from fixtures and a synthetic planner. The stub and mock clients do not, so the handlers still answer 404 for them.
type TripRoute ¶
type TripRoute struct {
DestinationReached bool
TotalChargingDurationSec int
TotalDriveDurationSec int
TotalTripDurationSec int
TotalDriveDistanceMeters float64
ArrivalSoC float64
ArrivalReachableMeters float64
EnergyConsumptionKWh float64
BatteryEmptyToDestMeters float64
BatteryEmptyLat float64
BatteryEmptyLon float64
// RouteResponseRaw holds the gateway's geometry payload verbatim
// so the SPA / map renderer can decode it without a round-trip.
// Shape is opaque to us (provider-specific encoded polyline +
// metadata); we forward it as-is.
RouteResponseRaw any
Waypoints []PlannedWaypoint
}
TripRoute is one candidate route. Charging stops are inside Waypoints (filtered to entries whose waypointType identifies a charger).
type UpstreamError ¶
type UpstreamError struct {
Class ErrorClass
Op string // GraphQL operationName, e.g. "Login"
HTTPStatus int // 0 when the error is pre-HTTP (network, marshal)
ExtCode string // GraphQL extensions.code, when present
// Reason is a short human hint for the user-facing UI or for
// logs. For ClassUserAction it lands in users.needs_reauth_reason.
Reason string
// Cause is the underlying error. Preserved so callers can
// keep using errors.Is on standard sentinels.
Cause error
}
UpstreamError is the structured error every outbound Rivian call returns when the upstream rejects us. It wraps the underlying cause (so errors.Is / errors.As keep working with net.OpError, context.Canceled, etc.) and carries the extras that make the retry/needs_reauth logic trivial to write.
The struct is deliberately public so the api layer can unwrap it and turn 429/ClassRateLimited into a 503-with-Retry-After, ClassUserAction into a 401 that tells the client to open the Settings → Rivian pane, etc. Stringer is suitable for logs.
func (*UpstreamError) Error ¶
func (e *UpstreamError) Error() string
Error implements error. Format is terse on purpose; the Class and Op travel separately through structured logging. The wrapped Cause is included so string-matching tests and log scrapers that look for upstream GraphQL messages keep working.
func (*UpstreamError) Unwrap ¶
func (e *UpstreamError) Unwrap() error
Unwrap returns the underlying cause so errors.Is(err, context.Canceled), errors.Is(err, ErrUpstreamPaused), and similar continue to work through the UpstreamError wrapper.
type Vehicle ¶
type Vehicle struct {
ID string `json:"id"`
// ProLocked marks, on a free account with several vehicles, every vehicle but its primary: listed, recorded and alerting, but its history
// and detail are Pro. Set by the API, never by Rivian.
ProLocked bool `json:"pro_locked,omitempty"`
VIN string `json:"vin"`
Name string `json:"name"`
Model string `json:"model"` // "R1T" | "R1S"
ModelYear int `json:"model_year,omitempty"`
Make string `json:"make,omitempty"`
TrimID string `json:"trim_id,omitempty"` // e.g. "PKG-ADV", "LRG-DM-PRFM"
TrimName string `json:"trim_name,omitempty"` // e.g. "Adventure Package"
// PackKWh is the usable battery capacity inferred from model /
// trim. Used by the live recorder to estimate energy from SoC
// deltas when Rivian's live session feed doesn't report power.
PackKWh float64 `json:"pack_kwh,omitempty"`
// ImageURL is a pre-rendered 3/4 view of the configured vehicle,
// populated lazily via GetVehicleImages. Empty string when not
// yet fetched or when the Rivian image service didn't return one.
// This is a convenience pick (hero image) — the full set of
// rendered angles (front, rear, side, interior, etc.) is in
// Images.
ImageURL string `json:"image_url,omitempty"`
// Images is the full set of configurator-rendered images Rivian
// returns for this vehicle — typically 6-8 angles including
// 3/4 front, side profile, rear, interior cabin, wheel detail,
// etc. Each entry carries a `placement` tag describing the
// camera angle so the UI can build a gallery or swap the hero
// image on hover.
Images []VehicleImage `json:"images,omitempty"`
// OTA is the current and available software release with release-notes
// links (stored by the OTA sync, served by /api/vehicles); nil until the
// first fetch.
OTA *OTAInfo `json:"ota,omitempty"`
// FieldSupport says which live fields to trust on this model: reported,
// estimate, best_effort or unavailable (see FieldSupport).
FieldSupport map[string]string `json:"field_support,omitempty"`
}
Vehicle is a minimum-useful projection of a Rivian vehicle. More fields will be added as we flesh out the client.
type VehicleImage ¶
type VehicleImage struct {
VehicleID string `json:"vehicle_id"`
OrderID string `json:"order_id,omitempty"`
URL string `json:"url"`
Extension string `json:"extension,omitempty"`
Resolution string `json:"resolution,omitempty"`
Size string `json:"size,omitempty"`
Design string `json:"design,omitempty"`
Placement string `json:"placement,omitempty"`
}
VehicleImage is one pre-rendered configurator image.
Source Files
¶
- account.go
- accountsync.go
- actions.go
- breaker.go
- charge_eta.go
- charging_schedule.go
- client.go
- errclass.go
- expiry.go
- fixture_planner.go
- live.go
- livestate.go
- livestate_gate.go
- livestate_purge.go
- mock.go
- monitor.go
- monitor_registry.go
- ota.go
- ota_observe.go
- overlap_guard.go
- parallax.go
- parallax_next.go
- parked_dedupe.go
- polyline.go
- receipts.go
- reconnect.go
- recorder.go
- registry.go
- rejected_session.go
- replay.go
- replay_catchup.go
- replay_source.go
- sample_corpus.go
- sample_plan.go
- sample_profile.go
- sample_routes.go
- sample_routes_gen.go
- sleep_gate.go
- stagger.go
- tire_observe.go
- tire_status.go
- touch_gate.go
- trailer.go
- trip_planner.go
- vehicle_info.go
- wake_safety.go
- write_failures.go
- ws.go
- ws_charging.go
- ws_drops.go
- ws_mux.go
- ws_parallax.go