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

charges

package
v0.36.21 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package charges persists derived charge sessions.

Index

Constants

This section is empty.

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 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 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
	// 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) 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
}

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

ListAll returns every charge for this user, newest first. Used by the Settings → Backup endpoint.

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 row; use EachAll for large result sets.

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) 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) 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) 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 within the (user_id, vehicle_id) scope.

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