Help us shape the future of Go! Take the 2026 Go Developer Survey

charges

package
v0.40.22 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package charges persists derived charge sessions.

Index

Constants

View Source
const EstimateURLPrefix = "https://dcfctracker.com/"

EstimateURLPrefix is the only place an estimate's report link may point.

View Source
const LocationRecorded = "recorded"

LocationRecorded is the location override "where the truck was" (the charge's own lat/lon).

View Source
const MaxChartRows = 5000

MaxChartRows bounds one chart answer (newest rows win when a window holds more).

Variables

View Source
var ErrNoRestamp = errors.New("no restamp to undo")

ErrNoRestamp is returned by UndoRestamp when there's nothing to undo.

View Source
var ErrNoSuchCharge = errors.New("no such charge")

ErrNoSuchCharge: no charge of this user with that id.

View Source
var ErrRangeTooLarge = errors.New("window holds more rows than one answer carries")

ErrRangeTooLarge: a window holds more rows than one answer may carry.

View Source
var ErrRestampNotLatest = errors.New("only the latest restamp can be undone")

ErrRestampNotLatest is returned by UndoRestamp for a correction that is still undoable but isn't the user's latest: undoing it would put back places a newer correction already changed. Undo the newer ones first.

Functions

func CleanEstimate added in v0.38.8

func CleanEstimate(month, url string) (string, string)

CleanEstimate returns the estimate report month and URL as they may be stored: the month only as YYYY-MM, the URL only on dcfctracker.com over https. Anything else becomes "" so no writer (the recorder, a restored backup) can store a link the app would render.

func IsOpenLiveFinalState

func IsOpenLiveFinalState(s string) bool

IsOpenLiveFinalState reports whether s names a charges.final_state value that means the session is physically still in progress. Kept in sync with the inline IN-list in LatestOpenLive et al.

Types

type Charge

type Charge struct {
	ID             string
	VehicleID      string
	StartedAt      time.Time
	EndedAt        time.Time
	StartSoCPct    float64
	EndSoCPct      float64
	EnergyAddedKWh float64
	MilesAdded     float64
	MaxPowerKW     float64
	AvgPowerKW     float64
	FinalState     string
	Lat, Lon       float64
	Source         string
	Cost           float64
	Currency       string
	PricePerKWh    float64
	// ThermalKWh is the energy the BMS spent on pack heating / cooling
	// during the session, surfaced by Rivian's Parallax
	// ChargingSessionLiveData protobuf. Nullable: legacy rows
	// recorded before this column existed leave it nil so the UI
	// can render "—" instead of misleading "0 kWh thermal".
	ThermalKWh *float64
	// ChargeLimitPct is the charge limit the truck was set to during the session
	// (the last reported value, 1 to 100); nil when unknown. A later save that carries
	// none keeps the stored one.
	ChargeLimitPct *float64
	// PlaceOverride is the user's own "home" or "away" for this charge (empty: automatic). When
	// set, Place holds the same value and nothing re-classifies the row.
	PlaceOverride string
	// LocationOverride is "recorded" when the user says the charge happened where the truck was
	// (Lat/Lon), not at the nearest public charger; maps then never snap. Empty: as recorded.
	LocationOverride string
	// ActiveSeconds is the wall-clock time the session actually spent
	// charging (power above the idle floor), as opposed to
	// EndedAt-StartedAt which spans the whole plugged-in period
	// (including charging_ready / battery-conditioning idle). Zero on
	// legacy rows and non-live sources; the UI falls back to the wall
	// span in that case.
	ActiveSeconds float64
	// CostSource says where Cost came from: "rivian" (billed),
	// "home_rate", "network_estimate" (DCFC priced from the matched
	// network's rate), "observed_estimate" (DCFC priced from a typical
	// public price, no rate of the user's) or "user". Empty on legacy rows.
	CostSource string
	// Network is the matched DCFC network's display name for
	// network_estimate rows; empty otherwise.
	Network string
	// Place is where the session happened relative to the user's home
	// when it was recorded: "home", "away", or "" (unknown: no saved
	// home or no position). Migration 0044.
	Place string
	// NetworkState is DC fast charge auto-detection's verify state
	// (migration 0051): "estimated", "needs_confirmation", "confirmed",
	// or "" (not auto-detected: AC, Rivian-billed, user-priced, legacy).
	NetworkState string
	// NetworkConfidence is the detection's confidence, 0..1; nil when
	// there was no detection.
	NetworkConfidence *float64
	// NetworkEvidence is what the detection saw (JSON, contract v1);
	// nil when there was no detection.
	NetworkEvidence json.RawMessage
	// RivianCharger is Rivian's isRivianCharger for the session: nil
	// when not reported. Written by the live recorder; a later upsert
	// without it keeps the stored value. Not read back yet. Migration
	// 0050.
	RivianCharger *bool
	// ReceiptCost is what Rivian billed for the session (its Charging
	// history receipt), kept next to Cost (our estimate), which a receipt
	// never overwrites. Nil when no receipt matched. ReceiptCurrency and
	// ReceiptTxnID go with it. Migration 0055; written only by the
	// receipts sync.
	ReceiptCost     *float64
	ReceiptCurrency string
	ReceiptTxnID    string
	// EstimateMonth and EstimateURL say which public report priced a
	// CostSource "observed_estimate" session ("2026-10", the dcfctracker.com
	// page). Empty on every other row. Migration 0075.
	EstimateMonth string
	EstimateURL   string
	// EnergyEstimated says EnergyAddedKWh was not metered: the recorder's integral had lost energy (a handoff or a
	// restart took it) and the energy was estimated from the SoC gain. Stored in metadata ("energy_estimated"), set
	// and never cleared by an upsert (a row read back and re-saved keeps it).
	EnergyEstimated bool
	// GapKWh is the part of EnergyAddedKWh credited for stretches the recorder had no power frames for (a dropout
	// longer than its integration cap) from the SoC the pack gained across them, not metered. 0 when none. Stored
	// in metadata ("gap_kwh"); an upsert with 0 leaves a stored value alone.
	GapKWh float64
}

Charge is a single charging session. ID is the stable external id ("live_<vid>_c_<unix>", "electrafi_<vid>_c_<unix>") and maps to the charges.external_id column. VehicleID is the Rivian gateway string.

type ChartCharge added in v0.38.16

type ChartCharge struct {
	StartedAt   time.Time `json:"started_at"`
	EndedAt     time.Time `json:"ended_at"`
	StartSoCPct float64   `json:"start_soc_pct"`
	EndSoCPct   float64   `json:"end_soc_pct"`
	// ChargeLimitPct is the limit the truck was set to for the charge (1 to 100),
	// null when unknown (older charges are backfilled where the samples still exist).
	ChargeLimitPct *float64 `json:"charge_limit_pct"`
}

ChartCharge is the part of a charge the iPhone's charts need: when and the charge at each end. No ids, costs, places or coordinates.

type Factory

type Factory struct {
	// contains filtered or unexported fields
}

Factory hands out per-user *Store instances. See drives.Factory for the rationale; this is the same shape, scoped to the charges table.

func NewFactory

func NewFactory(d *sql.DB, resolvers *db.VehicleResolverFactory) *Factory

NewFactory wraps the shared pool + resolver factory.

func (*Factory) For

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

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

type LiveOverlap added in v0.32.0

type LiveOverlap struct {
	EndedAt   time.Time
	EndSoCPct float64
	EnergyKWh float64
}

LiveOverlap describes the live session another recorder wrote over the same time range: where it ends and what it already covers.

type RestampOptions added in v0.32.0

type RestampOptions struct {
	HomeLat, HomeLon float64
	RadiusM          float64
	// HomeRate and Currency price AC charges that become home and are
	// unpriced; HomeRate <= 0 prices nothing.
	HomeRate float64
	Currency string
	// IsDCFC is the shared fast-charging rule (rivian.IsDCFCSession);
	// fast charges are never priced at the home rate. nil treats every
	// session as AC.
	IsDCFC func(peakKW, energyKWh, activeSeconds float64, span time.Duration) bool
	DryRun bool
}

RestampOptions configures RestampPlace.

type RestampResult added in v0.32.0

type RestampResult struct {
	DryRun            bool `json:"dry_run"`
	Changed           int  `json:"changed"`
	ToHome            int  `json:"to_home"`
	ToAway            int  `json:"to_away"`
	Unchanged         int  `json:"unchanged"`
	SkippedUserPriced int  `json:"skipped_user_priced"`
	// SkippedOverridden: charges whose home/away the user set by hand, never re-checked.
	SkippedOverridden    int        `json:"skipped_overridden"`
	WouldPriceAtHomeRate int        `json:"would_price_at_home_rate"`
	TotalCostDelta       float64    `json:"total_cost_delta"`
	Currency             string     `json:"currency"`
	RestampID            *uuid.UUID `json:"restamp_id"`
}

RestampResult counts what RestampPlace changed (or would change).

type StaleOpenCandidate

type StaleOpenCandidate struct {
	ExternalID      string
	RivianVehicleID string
	FinalState      string
	EndedAt         time.Time
}

StaleOpenCandidate is one row the janitor is considering for abandonment. The rivian-side vehicle ID is denormalised here so the caller can REST-poll the gateway without re-resolving from the internal vehicle UUID.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store wraps the charges table.

func OpenStore

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

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

func (*Store) AbandonOpenLive

func (s *Store) AbandonOpenLive(ctx context.Context, externalID string) (int, error)

AbandonOpenLive closes one specific open live row as abandoned. The smart janitor calls this only after a REST poll has confirmed the vehicle is no longer in a charging state. Same guarded WHERE as RefreshOpenLive so we never abandon a row that's already been closed legitimately by a terminal-state push between the list and the close.

func (*Store) ApplyNetworkRate added in v0.38.8

func (s *Store) ApplyNetworkRate(ctx context.Context, slug, network, currency string, pricePerKWh float64) (int64, error)

ApplyNetworkRate prices the user's past sessions at their own rate for one network, only those that had no rate of theirs: the sessions priced from a typical public price for that network (cost_source observed_estimate), and the unpriced fast charges the detection put at that network (slug is the first network with a slug in network_evidence, as the ledger's rate_slug). Their price becomes the rate times the session's energy, cost_source network_estimate, the estimate's report cleared; network_state is left as the detection set it so the user can still confirm. Sessions with a Rivian receipt, a user price (including free) or a Rivian-billed price are never touched, and neither is a session still being recorded (it ended within the last 30 minutes). Returns the number of sessions priced.

func (*Store) Close

func (s *Store) Close() error

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

func (*Store) CloseStaleOpenLive

func (s *Store) CloseStaleOpenLive(ctx context.Context, rivianVehicleID, keepID string) (int, error)

CloseStaleOpenLive marks every live-sourced charge for the vehicle whose external_id is NOT keepID and whose final_state is still a non-terminal "charging_*" as abandoned. Returns rows updated.

func (*Store) CloseStaleOpenLiveBefore

func (s *Store) CloseStaleOpenLiveBefore(ctx context.Context, before time.Time) (int, error)

CloseStaleOpenLiveBefore is the unattended janitor variant: marks every open live charge for THIS user whose ended_at is older than `before` as abandoned. Used by the recorder's periodic sweep to self-heal sessions left open by a previous process that died mid-charge and never came back. Returns rows updated.

func (*Store) ConfirmNetwork added in v0.33.0

func (s *Store) ConfirmNetwork(ctx context.Context, externalID string) (int64, error)

ConfirmNetwork records that the user agreed with an auto-detected DC fast charge network: network_state becomes 'confirmed'; cost, network and cost_source stay. Only an estimate can be confirmed (cost_source 'network_estimate' in an open state); a charge nothing matched has no estimate to confirm, so the user picks a network instead. Returns rows affected: 0 means not found for this user or not confirmable.

func (*Store) Count

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

Count returns the total number of charges for this user.

func (*Store) Dedupe

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

Dedupe is a no-op on Postgres — UNIQUE (vehicle_id, external_id) prevents the SQLite-era duplicates the old code had to clean up.

func (*Store) DeleteBefore added in v0.40.16

func (s *Store) DeleteBefore(ctx context.Context, cutoff time.Time) (int64, error)

DeleteBefore deletes this user's charges that started before cutoff. Returns the number deleted.

func (*Store) DeleteByExternalID

func (s *Store) DeleteByExternalID(ctx context.Context, externalID string) (int64, error)

DeleteByExternalID removes a single charge row owned by this user. Returns the number of rows deleted (0 if no match — caller should treat that as 404). Scoped by user_id so a malicious external_id can't reach into another user's data.

func (*Store) EachAll added in v0.36.17

func (s *Store) EachAll(ctx context.Context, fn func(Charge) error) error

EachAll streams EVERY stored charge of this user, newest first, with no display filter: the backup and the data export must hold every row, including the sub-minute ones the lists hide. Callers that price or show charges use EachListed.

func (*Store) EachListed added in v0.38.0

func (s *Store) EachListed(ctx context.Context, fn func(Charge) error) error

EachListed is EachAll without the charges the lists hide: those under a minute.

func (*Store) GetByExternalID added in v0.33.2

func (s *Store) GetByExternalID(ctx context.Context, externalID string) (*Charge, error)

GetByExternalID returns one of this user's charges, or (nil, nil).

func (*Store) LatestOpenLive

func (s *Store) LatestOpenLive(ctx context.Context, rivianVehicleID string) (*Charge, error)

LatestOpenLive returns the most recent live-sourced charge for this vehicle whose final_state is still a non-terminal "charging_*" — i.e. the recorder was mid-session when the process stopped.

func (*Store) ListAll

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

ListAll returns every stored row; use EachAll for large result sets.

func (*Store) ListAllListed added in v0.38.0

func (s *Store) ListAllListed(ctx context.Context) ([]Charge, error)

ListAllListed returns the charges the lists show (a minute or longer).

func (*Store) ListChartPoints added in v0.38.16

func (s *Store) ListChartPoints(ctx context.Context, since, until time.Time, rivianVehicleID string) ([]ChartCharge, error)

ListChartPoints returns the user's charges that started in [since, until), oldest first, narrowed to ChartCharge; the same rows the list shows (at least a minute). rivianVehicleID "" means every truck; at most MaxChartRows, the newest if more.

func (*Store) ListRange added in v0.40.14

func (s *Store) ListRange(ctx context.Context, from, to time.Time) ([]Charge, error)

ListRange returns the user's listed charges that started in [from, to), newest first, from every truck. A window with more than maxRangeRows returns ErrRangeTooLarge rather than a partial list.

func (*Store) ListRecent

func (s *Store) ListRecent(ctx context.Context, limit int) ([]Charge, error)

ListRecent returns the most recent N charges, newest first.

func (*Store) ListRecentForVehicle added in v0.38.16

func (s *Store) ListRecentForVehicle(ctx context.Context, rivianVehicleID string, limit int) ([]Charge, error)

ListRecentForVehicle is ListRecent for one of the user's trucks, by its Rivian vehicle id. An id the user doesn't have returns no rows.

func (*Store) ListStaleOpenLive

func (s *Store) ListStaleOpenLive(ctx context.Context, before time.Time) ([]StaleOpenCandidate, error)

ListStaleOpenLive returns every open live-sourced charge row whose ended_at is older than `before`. Same WHERE shape as CloseStaleOpenLiveBefore but read-only — used by the smarter janitor that wants to REST-confirm the vehicle is actually no longer charging before tagging the row abandoned. WS feeds for long L1/L2 sessions can go silent for hours without the charge having ended.

func (*Store) LiveCovers added in v0.32.0

func (s *Store) LiveCovers(ctx context.Context, rivianVehicleID string, start, end time.Time) (bool, error)

LiveCovers reports whether a live (recorder) charge for this vehicle overlaps [start, end). Importers use it to leave sessions the recorder already captured alone instead of double-counting them.

func (*Store) LiveOverlap added in v0.32.0

func (s *Store) LiveOverlap(ctx context.Context, rivianVehicleID, excludeID string, start, end time.Time) (*LiveOverlap, error)

LiveOverlap returns the latest-ending live charge for this vehicle, other than excludeID, whose time range overlaps [start, end), or nil. The recorder uses it when the overlap guard (migration 0041) rejected one of its sessions: the session's tail after that row is still worth writing if the other recorder stopped.

func (*Store) MarkFree added in v0.32.0

func (s *Store) MarkFree(ctx context.Context, externalID, currency string) (int64, error)

MarkFree records that the user paid nothing for a session: cost and price_per_kwh are stored as 0 (not NULL), cost_source = 'user'. It is deliberately separate from UpdatePricing, where a zero still means "clear the price". Upsert keeps user-priced rows, so a session marked free while the recorder still has it open stays free. currency may be empty. Returns rows affected (0 → not found for this user).

func (*Store) NearestVerifiedNetwork added in v0.33.0

func (s *Store) NearestVerifiedNetwork(ctx context.Context, lat, lon, radiusM float64) (VerifiedNetwork, bool, error)

NearestVerifiedNetwork returns the user's nearest DC fast charge within radiusM of (lat, lon) whose network is settled: confirmed, or a user price that names a network. It is auto-detection's site memory, so a co-located site the user already resolved isn't asked about again. ok=false when there is none (or no position).

func (*Store) NetworkToConfirm added in v0.33.0

func (s *Store) NetworkToConfirm(ctx context.Context) (toConfirm, needsAttention int, err error)

NetworkToConfirm counts this user's auto-detected charges still open for verification: toConfirm covers both open states, needsAttention only 'needs_confirmation'.

func (*Store) RefreshOpenLive

func (s *Store) RefreshOpenLive(ctx context.Context, externalID, finalState string) (int, error)

RefreshOpenLive bumps ended_at = NOW() on a single open live row and optionally advances its final_state. Used by the smart janitor when a REST poll confirms the vehicle is still actively charging — keeps the row live for another sweep window without abandoning it. The WHERE clause is intentionally narrow (still open, still live) so a concurrent terminal-state push that closed the row legitimately can't be undone here.

func (*Store) Reset

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

Reset deletes every charge for this store's user. See drives.Store.Reset.

func (*Store) RestampPlace added in v0.32.0

func (s *Store) RestampPlace(ctx context.Context, o RestampOptions) (RestampResult, error)

RestampPlace is the "my old home pin was wrong" correction: it recomputes the recorded place ('home' within RadiusM of the home, else 'away') on this user's charges that have a position, and prices the AC charges that become home and were unpriced (no cost, no cost source, some energy) at the home rate with cost_source 'home_rate', as the recorder would have with the right pin. Rows the user priced are never touched; no other price changes. One transaction; each changed row is logged in charge_restamps under a new restamp id so UndoRestamp can revert it. DryRun counts without writing.

func (*Store) SetLocationOverride added in v0.38.22

func (s *Store) SetLocationOverride(ctx context.Context, externalID string, recorded bool) error

SetLocationOverride says whether one of the user's charges happened where the truck was (recorded true: maps pin its stored lat/lon and never snap to a public charger) or is shown as recorded with the usual snap (false).

func (*Store) SetNetworkPrice added in v0.33.2

func (s *Store) SetNetworkPrice(ctx context.Context, externalID, network, currency string, pricePerKWh float64) (int64, error)

SetNetworkPrice records the user's own choice of DC fast charge network: cost = pricePerKWh × the session's energy, cost_source 'user', network_state 'confirmed' (the user picked it; confidence and evidence stay for audit). Returns rows affected (0: not found for this user, or the session has no energy to price).

func (*Store) SetPlaceOverride added in v0.38.22

func (s *Store) SetPlaceOverride(ctx context.Context, externalID, place string, homeSet bool, homeLat, homeLon, radiusM float64) error

SetPlaceOverride records the user's own "home" or "away" for one of their charges (by its external id) and sets place to it; nothing re-classifies the row afterwards. "" clears the override and gives the place back to the home location: "home" within radiusM of (homeLat, homeLon), else "away"; with no home (homeSet false) or no position, no place.

func (*Store) UndoRestamp added in v0.32.0

func (s *Store) UndoRestamp(ctx context.Context, restampID *uuid.UUID) (UndoResult, error)

UndoRestamp reverts the user's latest correction not yet undone (restampID, if given, must be that one: ErrRestampNotLatest for an older pending one, ErrNoRestamp for an unknown or undone one): every logged charge gets its previous place back, and a home-rate price the correction added is removed only if the row still carries exactly that price (a price the user set or edited since is kept). One transaction; a correction can be undone once.

func (*Store) UpdatePricing

func (s *Store) UpdatePricing(ctx context.Context, externalID string, cost float64, currency string, pricePerKWh float64) (int64, error)

UpdatePricing overwrites the cost / currency / price-per-kWh of a single charge row, scoped to the current user. Used when the user edits a row in the UI to correct a missing or wrong DCFC price (e.g. a session paid outside the Rivian app). Zeros clear the stored value and let the API-layer fallbacks (home rate, etc.) take over again.

func (*Store) Upsert

func (s *Store) Upsert(ctx context.Context, c Charge) error

Upsert inserts or replaces a charge by external_id. With a fence token in the context (the live recorder of a pod that holds the group lease) the write is guarded by the lease in the same statement and returns fence.ErrFenced when the lease is gone; without one it is the plain statement, as for imports, restores and repairs.

type UndoResult added in v0.32.0

type UndoResult struct {
	RestampID     uuid.UUID `json:"restamp_id"`
	RestoredPlace int       `json:"restored_place"`
	Unpriced      int       `json:"unpriced"`
}

UndoResult reports what UndoRestamp reverted.

type VerifiedNetwork added in v0.33.0

type VerifiedNetwork struct {
	Network string
	DistM   float64
}

VerifiedNetwork is a DC fast charge of the user's own whose network they confirmed or set themselves, and how far it is from a point.

Jump to

Keyboard shortcuts

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