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 DemoVehicleID(uid uuid.UUID) string
- func Describe(err error) string
- func EncodePolyline(points [][2]float64) string
- func ErrCode(err error) string
- func InferPackKWh(model, trimID string, modelYear int) float64
- func IsAuthFlavoredReason(reason 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 ParseBoolEnv(v string) bool
- 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 WithPriority(ctx context.Context) context.Context
- type Account
- type AccountRegistry
- type BatteryCharacteristics
- type BatteryTemp
- type BatteryTempCallback
- type Breaker
- type BreakerConfig
- type BreakerObserver
- type BreakerState
- type ChargeCloseHook
- type ChargingFrame
- type ChargingSchedule
- type ChargingStatus
- type Client
- type Credentials
- type DCFCDetector
- type DCFCPriceLookup
- type DCFCVerdict
- type DriveCloseHook
- type DriveDynamicsCallback
- type DriveDynamicsFrame
- type DynamicsGNSS
- type DynamicsGNSSCallback
- type ElevationLookup
- type ErrRateLimited
- type ErrorClass
- type FrameSource
- 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) SetNeedsReauth(needs bool, reason string)
- 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) WithReauthSink(sink func(ctx context.Context, reason string)) *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 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) AllVehicleInfo() []Vehicle
- func (r *MonitorRegistry) EnsureSubscribed(rivianVehicleID string)
- func (r *MonitorRegistry) For(uid uuid.UUID) *StateMonitor
- func (r *MonitorRegistry) SetBatteryCapacityHookFactory(factory func(uid uuid.UUID) func(vehicleID string, kwh float64))
- func (r *MonitorRegistry) SetChargeCloseHookFactory(factory func(uid uuid.UUID) ChargeCloseHook)
- func (r *MonitorRegistry) SetDCFCDetectorFactory(factory func(uid uuid.UUID) DCFCDetector)
- 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) SetLiveStateStoreFactory(factory func(uid uuid.UUID) LiveStateStore)
- func (r *MonitorRegistry) SetParent(ctx context.Context)
- func (r *MonitorRegistry) SetRouteFiller(rf RouteFiller)
- 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([]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 OTASource
- type OTAVersion
- type ParallaxRawFrame
- type ParallaxReplayObserver
- type PendingMFA
- type PlanTripInput
- type PlanTripWaypoint
- type PlannedWaypoint
- type PriceLookup
- type RateLimiter
- type ReceiptSource
- 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) 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 RivoltAction
- type RouteFiller
- type SampleCorpus
- type SamplePlace
- type SampleSummary
- type SampleTripPlan
- type Session
- type SessionCharger
- type SessionSummary
- type SleepObserver
- 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) Latest(vehicleID string) (*State, time.Time)
- func (m *StateMonitor) LatestLiveSession(vehicleID string) *LiveSession
- 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) SetBatteryCapacityHook(hook func(vehicleID string, kwh float64))
- func (m *StateMonitor) SetChargeCloseHook(h ChargeCloseHook)
- func (m *StateMonitor) SetDCFCDetector(fn DCFCDetector)
- func (m *StateMonitor) SetDCFCPriceLookup(fn DCFCPriceLookup)
- func (m *StateMonitor) SetDriveCloseHook(h DriveCloseHook)
- func (m *StateMonitor) SetElevationLookup(e ElevationLookup)
- 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) SetSleepObserver(o SleepObserver)
- func (m *StateMonitor) SetSleepSafe(fn func(vehicleID string) bool)
- func (m *StateMonitor) SetStores(samplesStore *samples.Store, drivesStore *drives.Store, ...)
- func (m *StateMonitor) SetVehicleInfoHook(h func([]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 TirePressure
- type TripPlan
- type TripRoute
- type UpstreamError
- type Vehicle
- type VehicleImage
- type WakeSafety
Constants ¶
const ( PlaceHome = "home" PlaceAway = "away" )
Values of charges.Charge.Place (migration 0044).
const ( CostSourceRivian = "rivian" CostSourceHomeRate = "home_rate" CostSourceNetworkEstimate = "network_estimate" CostSourceUser = "user" )
Values of charges.Charge.CostSource (migration 0040).
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 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.
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 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 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 ( // 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.
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 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; anything else is "error".
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 InferPackKWh ¶
InferPackKWh maps a (model, trim-option-id, modelYear) tuple to the best-known usable battery capacity in kWh. Data comes from Rivian's owner-facing spec sheets and community teardowns — the GraphQL API exposes model / modelYear / trimOption.optionId but NOT pack size. Unknown combinations return DefaultPackKWh so the SoC-delta fallback keeps working.
Rivian trim optionId strings observed in the wild:
Gen 1 (2022–2024): "LRG-DM-PRFM" R1T/R1S Large Dual-Motor Performance (131 kWh usable) "LRG-DM-STD" R1T/R1S Large Dual-Motor Standard (131 kWh usable) "LRG-QM" R1T/R1S Large Quad-Motor (131 kWh usable) "STD-DM" R1T/R1S Standard Dual-Motor (LFP ~105 kWh usable) "MAX-DM" R1T/R1S Max pack Dual-Motor (~180 kWh usable) "MAX-QM" R1T/R1S Max pack Quad-Motor (~180 kWh usable) Gen 2 (2025+): "G2-STD-DM" Gen2 Standard+ (~92.5 kWh usable) "G2-LRG-DM" Gen2 Large (~141.5 kWh usable) "G2-MAX-DM" Gen2 Max (~180 kWh usable) "G2-TRI-MTR" Gen2 Tri-Motor "G2-QUAD" Gen2 Quad-Motor (Max pack)
Model year ≥ 2025 or a trim prefix of "G2-" indicates Gen 2. For unambiguously-Gen1 Large trims (2022–2024 model year) we return 131, not 141.5 — those are the same trim code shared across generations only because Rivian reused the configurator option IDs.
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 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 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 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.
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 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) AccountRegistry
NewReplayAccountRegistry returns a registry handing each user a ReplayClient over feed.
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
// 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 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 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
}
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 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 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 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"`
}
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) 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) 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, RefreshToken, getUserInformation) can be promoted by the caller via doGraphQLPriority.
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) 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"`
Path [][2]float64 `json:"path,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 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) 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) 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) SetBatteryCapacityHookFactory ¶ added in v0.22.1
func (r *MonitorRegistry) SetBatteryCapacityHookFactory(factory func(uid uuid.UUID) func(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) 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) 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)
SetParent records the outer ctx all per-user monitors derive their lifetime from. Must be called once at boot before Start.
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) 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([]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 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:"release_notes_url,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. ReleaseNotesURL is an outbound link for the UI to show; Rivolt never fetches it.
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 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
}
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 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) 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 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 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.
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"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_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 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"`
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"`
// 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) 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) 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) SetBatteryCapacityHook ¶ added in v0.22.1
func (m *StateMonitor) SetBatteryCapacityHook(hook func(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) 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) 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) 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) 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([]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 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
}
TripPlan is the cleaned-up response. The gateway returns one or more candidate routes; first is typically the recommended one.
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"`
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"`
}
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
- actions.go
- breaker.go
- charging_schedule.go
- client.go
- errclass.go
- live.go
- livestate.go
- mock.go
- monitor.go
- monitor_registry.go
- ota.go
- overlap_guard.go
- parallax.go
- parallax_next.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
- trip_planner.go
- vehicle_info.go
- wake_safety.go
- ws.go
- ws_charging.go
- ws_mux.go
- ws_parallax.go