Documentation
¶
Overview ¶
Package ledger builds the server-side money statement (the Money tab and, eventually, every cost/energy total in the SPA) from recorded charges and drives.
Everything here is read-only and pure: Build takes rows already loaded from Postgres (see Load) and returns the response. Three rules keep the numbers honest:
- Charges are the ledger. "Spend" is what was paid for energy; driving cost is spend ÷ miles, never a sum of per-drive guesses.
- Overlapping duplicates (two recorders on one vehicle, or an import overlapping live data) are resolved at query time and excluded from every total. No rows are modified.
- Unknown stays unknown: unpriced sessions, missing energy and inferred provenance are counted and surfaced, not papered over.
The wire contract lives in the Money tab's ledger-contract.md (v1).
Index ¶
- Constants
- Variables
- func BilledCost(c Charge, cfg Config) (float64, bool)
- func CostSource(c Charge, cfg Config) (string, bool)
- func CountToConfirm(ctx context.Context, d *sql.DB, userID uuid.UUID) (open, attention int, err error)
- func EffectiveCharges(cs []Charge) ([]Charge, []Superseded)
- func Load(ctx context.Context, d *sql.DB, userID uuid.UUID, vehicleID string, ...) ([]Charge, []Drive, error)
- func LoadWindow(ps ...Period) (time.Time, time.Time)
- func PlanWindow(p Plan) (time.Time, time.Time)
- type Charge
- type Classification
- type Config
- type DataQuality
- type Drive
- type Driving
- type Energy
- type HomeSession
- type Input
- type KindLine
- type LatLon
- type MonthSummary
- type Monthly
- type Period
- type Plan
- type PlanCandidate
- type PlanComparison
- type PlanLeg
- type PlanMetric
- type PlanReason
- type PlanSignals
- type PlanStopActual
- type PlanStopPair
- type PlanStopPlanned
- type PlannedStop
- type Range
- type Receipt
- type ReceiptSession
- type Savings
- type Spend
- type Superseded
- type Trip
- type TripPlan
- type TripsResponse
Constants ¶
const ( SourceRivian = "rivian" SourceHomeRate = "home_rate" SourceNetworkEstimate = "network_estimate" // SourceObservedEstimate is a fast charge priced from a typical public // price (no rate of the user's): priced and in the total, but always // reported separately and replaced by a receipt or the user's rate. SourceObservedEstimate = "observed_estimate" SourceUser = "user" SourceHomeRateMisapplied = "home_rate_misapplied" SourceUnknown = "unknown" SourceMixed = "mixed" )
Cost sources. Rows closed by the pricing-aware recorder persist one of rivian / home_rate / network_estimate / user in charges.cost_source (migration 0040). Legacy rows (NULL) are derived at query time, which adds home_rate_misapplied: the old recorder's home rate stamped on a DC fast session, shown as "estimate pending" and kept out of spend until `rivolt reprice` backfills it.
const ( FlagFragmentsFolded = "fragments_folded" FlagHomeRateOnDCFC = "home_rate_on_dcfc" FlagHomeRateAway = "home_rate_away" FlagUnpriced = "unpriced" FlagNullEnergy = "null_energy" FlagOpenSession = "open_session" FlagAbandoned = "abandoned" FlagContinuesNextMonth = "continues_next_month" FlagContinuedFromPrev = "continued_from_previous_month" FlagCurrencyMismatch = "currency_mismatch" FlagEstimatePending = "estimate_pending" // FlagSuperseded marks a row the ledger drops as an overlapping // duplicate of another (EffectiveCharges); it isn't in any total. FlagSuperseded = "superseded" // FlagFree marks a session the user priced at $0 (cost_source=user, // cost 0): priced, counted in spend at zero, shown as "Free". FlagFree = "free" )
Flags on receipts and sessions.
const ( ReasonTooBrief = "plugged_in_too_briefly" // no single stretch of the plug-in could hold the charge ReasonNoCheaperHours = "no_cheaper_hours" // the cheapest hours it could use cost no less than it paid )
Reasons a considered session is left out of the saving.
const CrumbKWh = 0.05
CrumbKWh is the energy below which a charge session is a crumb (a plug-in blip, an abandoned stub): nothing to price, counted as null_energy rather than "needs a price". Seen on preview: a 4.7e-7 kWh abandoned row counted as unpriced.
const FastMinKW = 50.0
FastMinKW is the session peak at or above which a charge is DC fast charging. Same threshold as analytics.FastMinKW (the Charges page's "Fast" bucket) so the two surfaces agree.
const HomeRadiusKM = rivian.HomeRadiusM / 1000
HomeRadiusKM is how close to the saved home location a session must start to count as "home". Matches the ~200 m DBSCAN epsilon the Charges page uses, with a little GPS slack.
const MinMilesForCostPerMile = 100.0
MinMilesForCostPerMile is the driven distance below which driving.cost_per_mile is null: with only a few miles, one charge dominates and the ratio is meaningless (D2 F6).
const MinSavingsSessions = 3
MinSavingsSessions is the fewest home sessions an estimate needs before a number is shown.
const TripMaxParked = 12 * time.Hour
TripMaxParked is the longest a car can sit parked at home (or where we can't tell) inside one outing; a longer park ends it. Away from home a multi-night stay stays one trip (arriving home ends it), up to TripMaxParkedAway as a safety valve.
const TripMaxParkedAway = 14 * 24 * time.Hour
TripMaxParkedAway caps a park away from home inside one trip.
const TripMinKMFromHome = 150.0
TripMinKMFromHome is how far from home an outing must go to count as a trip (when the home location is known).
const Version = 1
Version is the response contract version. Additive changes keep it; breaking changes bump it.
Variables ¶
var ErrPlanUnusable = errors.New("ledger: saved trip has no origin and destination")
ErrPlanUnusable is returned by ParsePlan when the saved trip has neither an origin and destination in its inputs nor a planned route to take them from.
Functions ¶
func BilledCost ¶ added in v0.38.8
billedCost is the session's Rivian-billed amount when the user has the feature on and the receipt is in the statement currency (an empty receipt currency is unknown, so not counted). BilledCost is what Rivian billed for the session when the user's receipts are on (cfg.Billed) and the receipt is in the statement currency. It is the amount Build counts instead of the stored estimate.
func CostSource ¶
CostSource reports where a session's cost came from and whether the answer was derived at query time rather than read from the row.
- A persisted rivian / network_estimate / observed_estimate / user source wins (a row with no cost is still unknown).
- Legacy rows (NULL source) with no cost are unknown.
- A persisted home_rate wins, except AC away from home → unknown (owner decision 2026-10-13: away AC is unknown until priced).
- A legacy rate within homeRateTolerance of the user's home rate: DC fast charging → home_rate_misapplied (estimate pending); AC away from home → unknown, needs a price; otherwise → home_rate.
- Any other legacy price → rivian (billed, or a manual edit / an older home rate).
func CountToConfirm ¶ added in v0.33.2
func CountToConfirm(ctx context.Context, d *sql.DB, userID uuid.UUID) (open, attention int, err error)
CountToConfirm counts the user's auto-detected charges still open for verification, across all time and vehicles: open is both open states, attention only needs_confirmation. Zero on a database without migration 0051.
func EffectiveCharges ¶
func EffectiveCharges(cs []Charge) ([]Charge, []Superseded)
EffectiveCharges resolves overlapping duplicates per vehicle. Sessions are accepted in priority order (live over imports, then has energy, has a cost, longer, created earlier, id); a session overlapping one already accepted is superseded by it. Output is sorted by start.
func Load ¶
func Load(ctx context.Context, d *sql.DB, userID uuid.UUID, vehicleID string, from, to time.Time) ([]Charge, []Drive, error)
Load reads the user's charges and drives whose time range intersects [from, to), optionally narrowed to one Rivian vehicle id. Read-only: plain SELECTs filtered by user_id (RLS is the backstop, not the filter). Rows keep NULLs as nil so the ledger can tell "not recorded" from zero.
func LoadWindow ¶
LoadWindow is the [from, to) range Load must cover for the periods, padded so boundary-straddling sessions are seen.
func PlanWindow ¶ added in v0.33.0
PlanWindow is the [from, to) to Load the ledger Input over for p: the candidate window padded by the longest an outing can stay away on both sides, so every outing whose leg starts in the window is loaded whole. A partly loaded outing would be named after its first loaded drive, a trip_id that doesn't match /api/ledger/trips and moves with the window (a return-leg plan's outing can have left days earlier).
Types ¶
type Charge ¶
type Charge struct {
// EnergyEstimated: the energy was estimated from the SoC gain because its metered count had been lost (charges.metadata
// "energy_estimated"); the cost, priced from it, is estimated with it.
EnergyEstimated bool
ID string // charges.external_id, the id /api/charges exposes
VehicleID string // Rivian vehicle id
StartedAt time.Time
EndedAt time.Time
StartSoC *float64
EndSoC *float64
EnergyKWh *float64
PeakKW *float64
Cost *float64
Currency string
PricePerKWh *float64
FinalState string
Source string
Lat, Lon *float64
CreatedAt time.Time
// ActiveSeconds is charges.active_seconds (time actually drawing
// power); nil when not recorded. Feeds the shared DCFC rule.
ActiveSeconds *float64
// Place is charges.place ("home" / "away") recorded with the row
// (migration 0044); "" when not recorded.
Place string
// CostSource is charges.cost_source; "" when NULL (legacy row) or
// the column doesn't exist yet.
CostSource string
// Network is charges.network (e.g. "Electrify America") for
// network-estimated sessions; "" when unknown.
Network string
// NetworkState/-Confidence/-Evidence are DC fast charge
// auto-detection's verify record (migration 0051); empty/nil when
// the charge wasn't auto-detected.
NetworkState string
NetworkConfidence *float64
NetworkEvidence json.RawMessage
// Billed* is what Rivian billed for the session (its charging-history
// receipt, matched by the receipts sync; migration 0055), beside Cost,
// which stays our own figure. BilledCost is nil without a match.
// BilledVendor / BilledCharger are the network and charger name;
// BilledHome is Rivian's isHomeCharger (vendor and charger are then
// never surfaced).
BilledCost *float64
BilledCurrency string
BilledVendor string
BilledCharger string
BilledHome bool
// EstimateMonth and EstimateURL name the report behind an
// observed_estimate price (migration 0075); "" otherwise.
EstimateMonth string
EstimateURL string
}
Charge is the projection of a charges row the ledger needs. Pointer fields are nullable columns: nil means "not recorded", which the ledger keeps distinct from zero.
func FromCharge ¶
FromCharge converts a charges.Charge as the store lists it (zeros for NULL columns) into the ledger's nullable shape. A zero cost stays 0 only on user-priced rows, where it means Free.
type Classification ¶
type Classification struct {
CostSource string `json:"cost_source_effective"`
CostSourceDerived bool `json:"cost_source_derived"`
Kind string `json:"kind"`
Place string `json:"place"`
KMFromHome *float64 `json:"km_from_home"`
// CountsInSpend is true when the session's cost is part of
// spend.total (priced, same currency).
CountsInSpend bool `json:"counts_in_spend"`
Flags []string `json:"ledger_flags"`
// EstimateMonth / EstimateURL name the report behind an
// observed_estimate price. NeedsRate marks an unpriced fast charge the
// user's own rate would price; RateSlug is the network it most likely
// was (from the detection), "" when unknown.
EstimateMonth string `json:"estimate_month,omitempty"`
EstimateURL string `json:"estimate_url,omitempty"`
NeedsRate bool `json:"needs_rate,omitempty"`
RateSlug string `json:"rate_slug,omitempty"`
}
Classification is the ledger's view of one charge: the same cost source, kind, place and flags the Money tab uses, for other surfaces (the Charges list, the charge page, the Activity feed) so they don't re-derive them with their own rules.
func Classify ¶
func Classify(c Charge, cfg Config) Classification
Classify returns the ledger's classification of c under cfg.
func ClassifyAll ¶
func ClassifyAll(cs []Charge, cfg Config) []Classification
ClassifyAll is Classify over a list, with the ledger's dedupe applied: a row EffectiveCharges drops as an overlapping duplicate gets CountsInSpend=false and the superseded flag, so a list built from it agrees with the Money tab about what's in spend. Results are in the input order.
type Config ¶
type Config struct {
// Currency is the user's home currency (ISO-4217). Sessions in
// another currency are excluded from spend and flagged.
Currency string
// HomeRate is the user's configured home $/kWh; 0 = not set.
HomeRate float64
// Home is the saved home location; nil = not set.
Home *LatLon
// Billed turns on the billed_* fields (Rivian receipts) for this user:
// the rivian_receipts flag. Off, none of them appear.
Billed bool
}
Config is the per-user context the statement is priced in.
type DataQuality ¶
type DataQuality struct {
// CostSourceInferred is true when any session in the month had its
// cost_source derived (legacy NULL); CostSourceDerivedSessions counts
// them.
CostSourceInferred bool `json:"cost_source_inferred"`
CostSourceDerivedSessions int `json:"cost_source_derived_sessions"`
SupersededExcluded int `json:"superseded_excluded"`
Superseded []Superseded `json:"superseded,omitempty"`
DrivesSupersededExcluded int `json:"drives_superseded_excluded"`
OpenSessions int `json:"open_sessions"`
HomeRateOnDCFC int `json:"home_rate_on_dcfc"`
HomeRateAway int `json:"home_rate_away"`
CurrencyMismatchExcluded int `json:"currency_mismatch_excluded"`
}
DataQuality carries the counts behind the Money tab's quality notes.
type Drive ¶
type Drive struct {
ID string
VehicleID string
StartedAt time.Time
EndedAt time.Time
DistanceMi *float64
EnergyKWh *float64
Source string
CreatedAt time.Time
// Start and end positions; nil when not recorded. Used only to
// measure distance from home (trips); never returned.
StartLat, StartLon *float64
EndLat, EndLon *float64
// Start and end state of charge (%); nil when not recorded. Used
// for plan vs actual arrival SoC; never returned.
StartSoC, EndSoC *float64
}
Drive is the projection of a drives row the ledger needs.
type Driving ¶
type Driving struct {
Drives int `json:"drives"`
Miles float64 `json:"miles"`
MilesWithEnergy float64 `json:"miles_with_energy"`
KWhUsedKnown float64 `json:"kwh_used_known"`
Coverage *float64 `json:"coverage"`
CostPerMile *float64 `json:"cost_per_mile"`
CostPerMileBasis string `json:"cost_per_mile_basis"`
}
Driving summarises drives and the per-mile allocation of spend.
type Energy ¶
type Energy struct {
Sessions int `json:"sessions"`
KWhAdded float64 `json:"kwh_added"`
NullEnergySessions int `json:"null_energy_sessions"`
ByKind map[string]KindLine `json:"by_kind"`
ByPlace map[string]KindLine `json:"by_place"`
// ByPlaceKind is the place × kind cross-tab of the same per-session
// classification: summed over places it equals ByKind, over kinds
// ByPlace. All six cells are always present.
ByPlaceKind map[string]map[string]KindLine `json:"by_place_kind"`
// IncludesEstimated is true when kwh_added includes a charge whose energy was estimated from the SoC gain.
IncludesEstimated bool `json:"includes_estimated"`
}
Energy is energy added by charging.
type HomeSession ¶ added in v0.38.10
type HomeSession struct {
ID string
Start, End time.Time // plugged in
KWh float64
ActiveSec float64 // time actually drawing power; 0 when not recorded
PeakKW float64 // 0 when not recorded
Cost float64 // what the statement counts
}
HomeSession is one home AC session as the Money statement counts it.
func HomeACSessions ¶ added in v0.38.10
func HomeACSessions(in Input, p Period) []HomeSession
HomeACSessions returns the sessions Build counts into energy.by_place_kind.home.ac with a cost for period p: the same dedupe, billed-cost rule, month and currency checks, so their costs add up to that line.
type Input ¶
Input is everything Build needs for one or more months: rows loaded over LoadWindow(periods...).
type KindLine ¶
type KindLine struct {
Sessions int `json:"sessions"`
KWh float64 `json:"kwh"`
Cost float64 `json:"cost"`
PeakKW *float64 `json:"peak_kw"`
}
KindLine is one by_kind / by_place bucket. Sessions are classified one by one (session peak for kind, session location for place), so each map's lines sum to the Energy totals.
type MonthSummary ¶
MonthSummary is a Monthly without receipts, for GET /api/ledger/summary. The outer Receipts field shadows the embedded one in JSON and is always empty, so the key is omitted.
type Monthly ¶
type Monthly struct {
Version int `json:"version"`
Period string `json:"period"`
TZ string `json:"tz"`
TZSource string `json:"tz_source"`
Range Range `json:"range"`
Currency string `json:"currency"`
VehicleID *string `json:"vehicle_id"`
IsCurrentMonth bool `json:"is_current_month"`
FirstActivityAt *time.Time `json:"first_activity_at"`
LastActivityAt *time.Time `json:"last_activity_at"`
// BilledSessions / EstimatedSessions split the month's sessions (with
// energy) into those with a Rivian receipt and those without. Present
// only when Config.Billed.
BilledSessions *int `json:"billed_sessions,omitempty"`
EstimatedSessions *int `json:"estimated_sessions,omitempty"`
Spend Spend `json:"spend"`
Energy Energy `json:"energy"`
Driving Driving `json:"driving"`
Receipts []Receipt `json:"receipts"`
DataQuality DataQuality `json:"data_quality"`
}
Monthly is the GET /api/ledger/monthly response.
type Period ¶
type Period struct {
Month string // YYYY-MM
Loc *time.Location
Start time.Time // inclusive, UTC instant
End time.Time // exclusive, UTC instant
}
Period is one statement month in a location.
func LastMonths ¶
LastMonths returns the n months ending with the one containing now, oldest first.
type Plan ¶ added in v0.33.0
type Plan struct {
CreatedAt time.Time
Departure *time.Time // planned departure; nil when not set
Origin LatLon
Destination LatLon
Stops []PlannedStop // charging stops, in route order
EnergyKWh *float64 // route energy
DistanceMi *float64
DurationMin *float64
ArrivalSoC *float64
StartSoC *float64 // the charge the plan leaves with; nil when not set
Cost *float64 // DC fast spend at the user's own member rates
Currency string
}
Plan is a saved trip plan, parsed from saved_trips.inputs and .plan (or a link-time snapshot of them).
type PlanCandidate ¶ added in v0.33.0
type PlanCandidate struct {
TripID string `json:"trip_id"`
StartedAt time.Time `json:"started_at"`
EndedAt time.Time `json:"ended_at"`
Miles float64 `json:"miles"`
Confidence float64 `json:"confidence"`
Band string `json:"band"`
// InProgress: the outing is still under way (not back home, last
// activity within TripMaxParked) and hasn't reached the plan's
// destination yet. Its band is at most medium.
InProgress bool `json:"in_progress"`
Signals PlanSignals `json:"signals"`
}
PlanCandidate is one outing that may have carried out the plan (§4).
func MatchPlan ¶ added in v0.33.0
func MatchPlan(p Plan, in Input) []PlanCandidate
MatchPlan scores the outings in `in` against p and returns those with confidence ≥ 0.50, best first (ties: earlier start), at most 3.
type PlanComparison ¶ added in v0.33.0
type PlanComparison struct {
// Status is "in_progress" while the outing hasn't reached the
// plan's destination or is still under way (not home yet), else
// "complete". Metrics are what's known so far either way.
Status string `json:"status"`
Currency string `json:"currency"`
PlanFrom string `json:"plan_from"`
Leg PlanLeg `json:"leg"`
Metrics []PlanMetric `json:"metrics"`
Stops []PlanStopPair `json:"stops"`
}
PlanComparison is the planned-vs-actual comparison of one linked (or suggested) outing (contract §5). PlanFrom is set by the caller ("snapshot" for a link, "current" for a suggestion).
func ComparePlan ¶ added in v0.33.0
func ComparePlan(p Plan, in Input, tripID string) (PlanComparison, bool)
ComparePlan compares p with the outing tripID in `in` (§5). False when no outing with that id is in `in`.
type PlanLeg ¶ added in v0.33.0
type PlanLeg struct {
StartedAt time.Time `json:"started_at"`
// ArrivedAt is when the leg reached the destination; the time of
// the latest drive's end while still on the way.
ArrivedAt time.Time `json:"arrived_at"`
Miles float64 `json:"miles"`
DriveIDs []string `json:"drive_ids"`
ChargeIDs []string `json:"charge_ids"`
}
PlanLeg is the part of the outing compared: from leaving through the first arrival at the plan's destination.
type PlanMetric ¶ added in v0.33.0
type PlanMetric struct {
Metric string `json:"metric"`
Unit string `json:"unit"`
Planned *float64 `json:"planned"`
Actual *float64 `json:"actual"`
Delta *float64 `json:"delta"`
ActualComplete *bool `json:"actual_complete,omitempty"`
ActualKnown *float64 `json:"actual_known,omitempty"`
UnpricedSessions *int `json:"unpriced_sessions,omitempty"`
ActualSources map[string]float64 `json:"actual_sources,omitempty"`
Reasons []PlanReason `json:"reasons"`
}
PlanMetric is one planned/actual pair. Delta is actual − planned; any of the three is null when unknown. The cost-only fields are omitted on other metrics.
type PlanReason ¶ added in v0.33.0
type PlanReason struct {
Code string `json:"code"`
Amount *float64 `json:"amount"`
Detail string `json:"detail"`
Network string `json:"network,omitempty"` // the actual session's network
PlanNetwork string `json:"plan_network,omitempty"` // the planned stop's network
Stop string `json:"stop,omitempty"` // planned stop label
Rate *float64 `json:"rate,omitempty"` // actual price per kWh
PlanRate *float64 `json:"plan_rate,omitempty"`
Estimated bool `json:"estimated,omitempty"` // Rate is a network estimate, not a bill
KWh *float64 `json:"kwh,omitempty"`
PlanKWh *float64 `json:"plan_kwh,omitempty"`
Count *int `json:"count,omitempty"` // unpriced sessions, extra or skipped stops
Percent *float64 `json:"percent,omitempty"` // drove_further, less_efficient
SoC *float64 `json:"soc,omitempty"` // start_soc_differed: the charge the drive left with
PlanSoC *float64 `json:"plan_soc,omitempty"`
Miles *float64 `json:"miles,omitempty"`
PlanMiles *float64 `json:"plan_miles,omitempty"`
MiPerKWh *float64 `json:"mi_per_kwh,omitempty"`
PlanMiPerKWh *float64 `json:"plan_mi_per_kwh,omitempty"`
}
PlanReason explains part of a difference. Amount (cost metric only) is that part of the cost delta, in the comparison currency.
Detail is ready-to-show copy (plan-vs-actual-copy.md); the structured fields, set where they apply, let a client word it itself. Stop and network names are public chargers only, never a home.
type PlanSignals ¶ added in v0.33.0
type PlanSignals struct {
Destination float64 `json:"destination"`
Origin float64 `json:"origin"`
Time float64 `json:"time"`
RouteOverlap *float64 `json:"route_overlap"`
DestinationKM float64 `json:"destination_km"`
OriginKM *float64 `json:"origin_km"`
StartOffsetMin *float64 `json:"start_offset_min"`
}
PlanSignals are the per-signal scores (0..1) behind a confidence.
type PlanStopActual ¶ added in v0.33.0
type PlanStopActual struct {
ChargeID string `json:"charge_id"`
Kind string `json:"kind"` // dcfc | ac
Network string `json:"network"`
KWh float64 `json:"kwh"`
Cost *float64 `json:"cost"` // null when not priced
CostSource string `json:"cost_source"`
StartedAt time.Time `json:"started_at"`
Minutes float64 `json:"minutes"`
}
PlanStopActual is the actual session at a stop.
type PlanStopPair ¶ added in v0.33.0
type PlanStopPair struct {
Match string `json:"match"`
Planned *PlanStopPlanned `json:"planned"`
Actual *PlanStopActual `json:"actual"`
}
PlanStopPair pairs a planned stop with the actual session there. Match is same_station / same_network / other_network / nearby (paired, network unknown on one side) / skipped / extra.
type PlanStopPlanned ¶ added in v0.33.0
type PlanStopPlanned struct {
Name string `json:"name"`
Network string `json:"network"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
ArrivalSoC *float64 `json:"arrival_soc"`
ChargeMin *float64 `json:"charge_min"`
EstKWh *float64 `json:"est_kwh"`
EstCost *float64 `json:"est_cost"`
}
PlanStopPlanned is the planned side of a stop.
type PlannedStop ¶ added in v0.33.0
type PlannedStop struct {
Name string
Network string // display name; "" when unknown
Lat, Lon float64
ArrivalSoC *float64
DepartureSoC *float64
ChargeMin *float64
EstKWh *float64 // from the plan's cost breakdown
EstRate *float64 // the user's rate there, per kWh
EstCost *float64
}
PlannedStop is one charging stop on the planned route.
type Receipt ¶
type Receipt struct {
ReceiptID string `json:"receipt_id"`
VehicleID string `json:"vehicle_id"`
Kind string `json:"kind"`
Place string `json:"place"`
KMFromHome *float64 `json:"km_from_home"`
StartedAt time.Time `json:"started_at"`
EndedAt time.Time `json:"ended_at"`
LocalDate string `json:"local_date"`
SpansDays int `json:"spans_days"`
SoCStart *float64 `json:"soc_start"`
SoCEnd *float64 `json:"soc_end"`
KWh float64 `json:"kwh"`
PeakKW *float64 `json:"peak_kw"`
Cost *float64 `json:"cost"`
CostSource string `json:"cost_source"`
Network *string `json:"network"`
// EnergyEstimated: some session's energy (and so cost) was estimated from the SoC gain.
EnergyEstimated bool `json:"energy_estimated"`
// NetworkState is the most urgent verify state among the receipt's
// sessions (needs_confirmation > estimated > confirmed); the
// confidence and evidence are that session's. null when none.
NetworkState *string `json:"network_state"`
NetworkConfidence *float64 `json:"network_confidence"`
NetworkEvidence json.RawMessage `json:"network_evidence"`
Rate *float64 `json:"rate"`
SessionCount int `json:"session_count"`
UnpricedSessions int `json:"unpriced_sessions"`
Flags []string `json:"flags"`
Sessions []ReceiptSession `json:"sessions"`
// Billed*: what Rivian billed for the plug-in (Config.Billed only).
// BilledCost is the sum of the sessions' billed amounts and is set only
// when every session with energy has one; vendor and charger come from
// the first session that has them and are never set for a home charger.
BilledCost *float64 `json:"billed_cost,omitempty"`
BilledCurrency *string `json:"billed_currency,omitempty"`
BilledVendor *string `json:"billed_vendor,omitempty"`
BilledCharger *string `json:"billed_charger,omitempty"`
BilledSessions *int `json:"billed_sessions,omitempty"`
EstimatedSessions *int `json:"estimated_sessions,omitempty"`
// EstimateMonth / EstimateURL: the report behind the receipt's price
// when its cost source is observed_estimate. NeedsRate / RateSlug /
// Network: an unpriced fast charge the user's own rate would price.
EstimateMonth string `json:"estimate_month,omitempty"`
EstimateURL string `json:"estimate_url,omitempty"`
NeedsRate bool `json:"needs_rate,omitempty"`
RateSlug string `json:"rate_slug,omitempty"`
}
Receipt is one plug-in: consecutive charges with no drive between.
type ReceiptSession ¶
type ReceiptSession struct {
ChargeID string `json:"charge_id"`
StartedAt time.Time `json:"started_at"`
EndedAt time.Time `json:"ended_at"`
SoCStart *float64 `json:"soc_start"`
SoCEnd *float64 `json:"soc_end"`
KWh *float64 `json:"kwh"`
PeakKW *float64 `json:"peak_kw"`
Cost *float64 `json:"cost"`
PricePerKWh *float64 `json:"price_per_kwh"`
CostSource string `json:"cost_source"`
// EnergyEstimated: the energy (and so the cost) was estimated from the SoC gain.
EnergyEstimated bool `json:"energy_estimated"`
// CostSourceDerived is true when cost_source was derived at query
// time (legacy row) rather than persisted by the recorder.
CostSourceDerived bool `json:"cost_source_derived"`
Network *string `json:"network"`
NetworkState *string `json:"network_state"`
NetworkConfidence *float64 `json:"network_confidence"`
NetworkEvidence json.RawMessage `json:"network_evidence"`
Kind string `json:"kind"`
Place string `json:"place"`
FinalState string `json:"final_state"`
Source string `json:"source"`
Flags []string `json:"flags"`
EstimateMonth string `json:"estimate_month,omitempty"`
EstimateURL string `json:"estimate_url,omitempty"`
NeedsRate bool `json:"needs_rate,omitempty"`
RateSlug string `json:"rate_slug,omitempty"`
// Billed*: Rivian's receipt for this session (Config.Billed and a
// match only); vendor and charger are never set for a home charger.
BilledCost *float64 `json:"billed_cost,omitempty"`
BilledCurrency *string `json:"billed_currency,omitempty"`
BilledVendor *string `json:"billed_vendor,omitempty"`
BilledCharger *string `json:"billed_charger,omitempty"`
}
ReceiptSession is one charge inside a receipt.
type Savings ¶ added in v0.38.10
type Savings struct {
// Considered is how many home AC sessions were looked at (priced, counted in
// spend). Moved of them would have cost less in a cheaper window; LeftOut
// could not move or would not have been cheaper (Reasons says why).
Considered int `json:"sessions_considered"`
Moved int `json:"sessions_moved"`
LeftOut int `json:"sessions_left_out"`
Reasons map[string]int `json:"left_out_reasons"`
// AsChargedTotal is what Money counts for all considered sessions;
// IfMovedTotal is the same sessions with the moved ones at their cheaper rate.
// MovedKWh is the energy of the moved sessions (the kWh the saving is about).
MovedKWh float64 `json:"moved_kwh"`
AsChargedTotal float64 `json:"as_charged_total"`
IfMovedTotal float64 `json:"if_moved_total"`
// CouldHaveSaved is AsChargedTotal - IfMovedTotal, never negative. Absent
// (with Needs set) when the estimate cannot be made yet.
CouldHaveSaved *float64 `json:"could_have_saved,omitempty"`
// OffpeakSharePct is the share, by energy, of the home charging that happened
// in off-peak hours (windows priced below the flat home rate), over the
// sessions whose plug-in lies wholly inside such hours or wholly outside them:
// when in a plug-in the charge ran is not known, so a session that straddles
// the edge is left out rather than guessed (OffpeakShareSessions counts the
// sessions that were used). Absent below MinSavingsSessions of them, or with no
// schedule or home rate.
// Needs says what is missing: "home_rate", "offpeak_schedule" or
// "more_home_sessions". When set, no figure is given.
Needs string `json:"needs,omitempty"`
}
Savings is the "what moving home charging to your cheapest hours could have saved" estimate for a stretch of months. It looks only at the user's own home AC sessions, exactly the ones the Money statement counts as home AC charging (so AsChargedTotal equals that line), prices each at the cheapest time its plug-in could have held the charge, and leaves out the sessions that could not move. It is an estimate: the plug-in span is the best we know of when the charge could have run, and the user's real bill can differ.
func EstimateSavings ¶ added in v0.38.10
func EstimateSavings(sessions []HomeSession, sch offpeak.Schedule, baseRate float64) Savings
EstimateSavings prices sessions at the cheapest hours of sch they could have used. baseRate is the flat home rate that applies outside every window.
type Spend ¶
type Spend struct {
Total float64 `json:"total"`
BySource map[string]float64 `json:"by_source"`
PricedSessions int `json:"priced_sessions"`
UnpricedSessions int `json:"unpriced_sessions"`
UnpricedKWh float64 `json:"unpriced_kwh"`
UnpricedHomeRateEstimate *float64 `json:"unpriced_home_rate_estimate"`
EstimatePendingSessions int `json:"estimate_pending_sessions"`
EstimatePendingKWh float64 `json:"estimate_pending_kwh"`
// ObservedEstimateCost / -Sessions are the part of Total (and of
// PricedSessions) that is fast charging priced from a typical public
// price, not from the user's rate or a bill. Never blended into the
// user's own figures: shown as "includes $X estimated from typical
// prices".
ObservedEstimateCost float64 `json:"observed_estimate_cost"`
ObservedEstimateSessions int `json:"observed_estimate_sessions"`
// UnpricedFastSessions is the fast-charging subset of UnpricedSessions:
// the sessions "set your rate" would price.
UnpricedFastSessions int `json:"unpriced_fast_sessions"`
// BilledTotal is what Rivian billed over the month's sessions that have
// a receipt in the statement currency; EstimatedTotal is our own cost
// over the other sessions counted in Total. With Config.Billed each
// session counts once, as its receipt when it has one, so
// Total == BilledTotal + EstimatedTotal. Present only when Config.Billed;
// off, Total is our own cost per session as before.
BilledTotal *float64 `json:"billed_total,omitempty"`
EstimatedTotal *float64 `json:"estimated_total,omitempty"`
}
Spend is money paid for energy.
type Superseded ¶
type Superseded struct {
ChargeID string `json:"charge_id"`
SupersededBy string `json:"superseded_by"`
}
Superseded names a duplicate excluded in favour of its winner.
type Trip ¶
type Trip struct {
TripID string `json:"trip_id"`
VehicleID string `json:"vehicle_id"`
StartedAt time.Time `json:"started_at"`
EndedAt time.Time `json:"ended_at"`
Miles float64 `json:"miles"`
DriveIDs []string `json:"drive_ids"`
ChargeIDs []string `json:"charge_ids"`
FastSessions int `json:"fast_sessions"`
MaxKMFromHome *float64 `json:"max_km_from_home"`
KWhUsed float64 `json:"kwh_used"`
KWhAdded float64 `json:"kwh_added"`
Cost float64 `json:"cost"`
CostSourceMix map[string]float64 `json:"cost_source_mix"`
UnpricedSessions int `json:"unpriced_sessions"`
EfficiencyMiKWh *float64 `json:"efficiency_mi_per_kwh"`
Plan *TripPlan `json:"plan"`
}
Trip is one auto-detected trip.
type TripPlan ¶
type TripPlan struct {
PlanID string `json:"plan_id"`
PlannedCost float64 `json:"planned_cost"`
PlannedArrivalAt time.Time `json:"planned_arrival_at"`
PlannedMiles float64 `json:"planned_miles"`
}
TripPlan links a saved planned trip. Always null in v1.
type TripsResponse ¶
type TripsResponse struct {
Version int `json:"version"`
TZ string `json:"tz"`
Currency string `json:"currency"`
Trips []Trip `json:"trips"`
}
TripsResponse is the GET /api/trips body.
func BuildTrips ¶
BuildTrips detects trips among the effective (deduped) drives and charges in in, returning those that start in [from, to), newest first. in should be loaded over a window padded beyond [from, to) so a trip straddling the edges is seen whole.