Add persistent car performance analytics
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { CarTelemetryStore } from "../src/car-telemetry.js";
|
||||
import { openDatabase } from "../src/database.js";
|
||||
import type { CarTelemetry } from "../src/adb.js";
|
||||
|
||||
function sample(
|
||||
recordedAt: number,
|
||||
values: { speed?: number; soc?: number; range?: number; charging?: number; volts?: number } = {}
|
||||
): CarTelemetry {
|
||||
const available = <T>(value: T) => ({ available: true as const, value });
|
||||
const missing = { available: false as const, reason: "Unavailable in test" };
|
||||
return {
|
||||
available: true,
|
||||
collectedAt: new Date(recordedAt).toISOString(),
|
||||
speedKph: values.speed === undefined ? missing : available(values.speed),
|
||||
gear: available("4"),
|
||||
wheelAngleDegrees: available(0),
|
||||
batteryPercent: values.soc === undefined ? missing : available(values.soc),
|
||||
rangeKm: values.range === undefined ? missing : available(values.range),
|
||||
batteryVoltage: values.volts === undefined ? missing : available(values.volts),
|
||||
totalConsumptionKwh: available(4.2),
|
||||
chargingStatus: values.charging === undefined ? missing : available(values.charging)
|
||||
};
|
||||
}
|
||||
|
||||
describe("car telemetry history", () => {
|
||||
it("keeps bounded raw samples, hourly rollups, lifetime records, and an observed range estimate", () => {
|
||||
const database = openDatabase(":memory:");
|
||||
const store = new CarTelemetryStore(database);
|
||||
const startedAt = Date.now() - 2 * 60 * 60_000;
|
||||
|
||||
store.record(sample(startedAt, { speed: 0, soc: 80, range: 320, charging: 0, volts: 398 }));
|
||||
for (let index = 1; index <= 400; index += 1) {
|
||||
store.record(sample(startedAt + index * 10_000, {
|
||||
speed: 100,
|
||||
soc: 80 - index * 0.02,
|
||||
range: 320 - index * 0.3,
|
||||
charging: 0,
|
||||
volts: 390
|
||||
}));
|
||||
}
|
||||
|
||||
const chargingAt = startedAt + 401 * 10_000;
|
||||
for (let index = 0; index <= 13; index += 1) {
|
||||
store.record(sample(chargingAt + index * 10_000, {
|
||||
speed: 0,
|
||||
soc: 60 + index * 0.2,
|
||||
range: 240 + index,
|
||||
charging: 1,
|
||||
volts: 405
|
||||
}));
|
||||
}
|
||||
|
||||
const overview = store.overview();
|
||||
expect(overview.records.peakSpeedKph).toBe(100);
|
||||
expect(overview.records.peakChargingPowerKw).toBeGreaterThan(30);
|
||||
expect(overview.records.maxObservedRangeKm).toBe(320);
|
||||
expect(overview.lifetime.trackedDistanceKm).toBeGreaterThan(10);
|
||||
expect(overview.lifetime.ownRangeEstimateKm).toBeGreaterThan(100);
|
||||
expect(overview.lifetime.chargeSessions).toBe(1);
|
||||
expect(overview.storage.rawSamples).toBe(415);
|
||||
expect(store.history("24h").points.length).toBeGreaterThan(1);
|
||||
expect(store.history("all").points.length).toBeGreaterThan(0);
|
||||
|
||||
store.prune(chargingAt + 91 * 24 * 60 * 60_000);
|
||||
const pruned = store.overview();
|
||||
expect(pruned.storage.rawSamples).toBe(0);
|
||||
expect(pruned.records.peakSpeedKph).toBe(100);
|
||||
expect(pruned.lifetime.trackedDistanceKm).toBeGreaterThan(10);
|
||||
database.close();
|
||||
});
|
||||
|
||||
it("leaves estimates empty until enough real observations exist", () => {
|
||||
const database = openDatabase(":memory:");
|
||||
const store = new CarTelemetryStore(database);
|
||||
store.record(sample(Date.now(), { speed: 0, soc: 75, range: 280, charging: 0 }));
|
||||
expect(store.overview().lifetime.ownRangeEstimateKm).toBeNull();
|
||||
expect(store.history("24h").points).toHaveLength(1);
|
||||
database.close();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user