← back to Charge And Explore

backend/src/core/vehicles.ts

80 lines

// EV catalog — lets a non-Tesla driver pick their car so charge estimates use a
// real per-model battery + DC peak + taper curve (not the generic default).
// Curves are representative planning values, versioned here; never presented as
// official manufacturer specs.

import type { ChargeCurveBand } from "./types.ts";

// Two representative taper shapes: 800V/high-peak cars hold power longer;
// 400V/value cars taper sooner.
const CURVE_800V: ChargeCurveBand[] = [
  { minSoc: 0, maxSoc: 30, powerMultiplier: 1.0 },
  { minSoc: 30, maxSoc: 50, powerMultiplier: 0.88 },
  { minSoc: 50, maxSoc: 70, powerMultiplier: 0.62 },
  { minSoc: 70, maxSoc: 85, powerMultiplier: 0.4 },
  { minSoc: 85, maxSoc: 100, powerMultiplier: 0.22 },
];
const CURVE_400V: ChargeCurveBand[] = [
  { minSoc: 0, maxSoc: 20, powerMultiplier: 1.0 },
  { minSoc: 20, maxSoc: 40, powerMultiplier: 0.85 },
  { minSoc: 40, maxSoc: 55, powerMultiplier: 0.62 },
  { minSoc: 55, maxSoc: 70, powerMultiplier: 0.45 },
  { minSoc: 70, maxSoc: 85, powerMultiplier: 0.3 },
  { minSoc: 85, maxSoc: 100, powerMultiplier: 0.18 },
];

export interface VehicleModel {
  id: string;
  make: string;
  model: string;
  usableBatteryKWh: number;
  maxDcKw: number;
  curve: ChargeCurveBand[];
}

export const VEHICLE_CATALOG: VehicleModel[] = [
  // Tesla (for manual entry when not OAuth-connected)
  { id: "tesla-m3-lr", make: "Tesla", model: "Model 3 Long Range", usableBatteryKWh: 75, maxDcKw: 250, curve: CURVE_400V },
  { id: "tesla-my-lr", make: "Tesla", model: "Model Y Long Range", usableBatteryKWh: 75, maxDcKw: 250, curve: CURVE_400V },
  { id: "tesla-ms", make: "Tesla", model: "Model S", usableBatteryKWh: 95, maxDcKw: 250, curve: CURVE_400V },
  // Non-Tesla — the whole point of this feature
  { id: "ford-mache", make: "Ford", model: "Mustang Mach-E ER", usableBatteryKWh: 91, maxDcKw: 150, curve: CURVE_400V },
  { id: "ford-lightning", make: "Ford", model: "F-150 Lightning ER", usableBatteryKWh: 131, maxDcKw: 155, curve: CURVE_400V },
  { id: "chevy-bolt", make: "Chevrolet", model: "Bolt EV", usableBatteryKWh: 65, maxDcKw: 55, curve: CURVE_400V },
  { id: "chevy-blazer", make: "Chevrolet", model: "Blazer EV", usableBatteryKWh: 85, maxDcKw: 190, curve: CURVE_400V },
  { id: "hyundai-ioniq5", make: "Hyundai", model: "Ioniq 5", usableBatteryKWh: 77, maxDcKw: 235, curve: CURVE_800V },
  { id: "hyundai-ioniq6", make: "Hyundai", model: "Ioniq 6", usableBatteryKWh: 77, maxDcKw: 235, curve: CURVE_800V },
  { id: "kia-ev6", make: "Kia", model: "EV6 Long Range", usableBatteryKWh: 77, maxDcKw: 235, curve: CURVE_800V },
  { id: "kia-ev9", make: "Kia", model: "EV9", usableBatteryKWh: 99, maxDcKw: 210, curve: CURVE_800V },
  { id: "rivian-r1t", make: "Rivian", model: "R1T Large Pack", usableBatteryKWh: 135, maxDcKw: 220, curve: CURVE_400V },
  { id: "vw-id4", make: "Volkswagen", model: "ID.4", usableBatteryKWh: 77, maxDcKw: 135, curve: CURVE_400V },
  { id: "nissan-ariya", make: "Nissan", model: "Ariya", usableBatteryKWh: 87, maxDcKw: 130, curve: CURVE_400V },
  { id: "nissan-leaf", make: "Nissan", model: "Leaf e+", usableBatteryKWh: 62, maxDcKw: 100, curve: CURVE_400V },
  { id: "polestar-2", make: "Polestar", model: "2 Long Range", usableBatteryKWh: 79, maxDcKw: 155, curve: CURVE_400V },
  { id: "bmw-i4", make: "BMW", model: "i4 eDrive40", usableBatteryKWh: 81, maxDcKw: 205, curve: CURVE_400V },
  { id: "mercedes-eqb", make: "Mercedes-Benz", model: "EQB", usableBatteryKWh: 66, maxDcKw: 100, curve: CURVE_400V },
  // Catalog expansion (Dust2026 2026-08-04): every major US-market EV maker
  // gets at least one profile, so the picker covers the whole brand landscape.
  { id: "tesla-cybertruck", make: "Tesla", model: "Cybertruck AWD", usableBatteryKWh: 123, maxDcKw: 250, curve: CURVE_800V },
  { id: "lucid-air-p", make: "Lucid", model: "Air Pure", usableBatteryKWh: 84, maxDcKw: 250, curve: CURVE_800V },
  { id: "porsche-taycan", make: "Porsche", model: "Taycan", usableBatteryKWh: 89, maxDcKw: 270, curve: CURVE_800V },
  { id: "audi-q4", make: "Audi", model: "Q4 e-tron", usableBatteryKWh: 77, maxDcKw: 135, curve: CURVE_400V },
  { id: "toyota-bz4x", make: "Toyota", model: "bZ4X", usableBatteryKWh: 64, maxDcKw: 100, curve: CURVE_400V },
  { id: "subaru-solterra", make: "Subaru", model: "Solterra", usableBatteryKWh: 64, maxDcKw: 100, curve: CURVE_400V },
  { id: "honda-prologue", make: "Honda", model: "Prologue", usableBatteryKWh: 85, maxDcKw: 155, curve: CURVE_400V },
  { id: "cadillac-lyriq", make: "Cadillac", model: "Lyriq", usableBatteryKWh: 102, maxDcKw: 190, curve: CURVE_400V },
  { id: "chevy-equinox", make: "Chevrolet", model: "Equinox EV", usableBatteryKWh: 85, maxDcKw: 150, curve: CURVE_400V },
  { id: "volvo-ex30", make: "Volvo", model: "EX30", usableBatteryKWh: 64, maxDcKw: 153, curve: CURVE_400V },
  { id: "rivian-r1s", make: "Rivian", model: "R1S Large Pack", usableBatteryKWh: 135, maxDcKw: 220, curve: CURVE_400V },
  { id: "bmw-ix", make: "BMW", model: "iX xDrive50", usableBatteryKWh: 105, maxDcKw: 195, curve: CURVE_400V },
];

export function vehicleById(id: string): VehicleModel | undefined {
  return VEHICLE_CATALOG.find((v) => v.id === id);
}

/** Compact list for the picker (no curve payload). */
export function catalogForPicker(): Array<{ id: string; make: string; model: string; maxDcKw: number; usableBatteryKWh: number }> {
  return VEHICLE_CATALOG.map(({ id, make, model, maxDcKw, usableBatteryKWh }) => ({ id, make, model, maxDcKw, usableBatteryKWh }));
}