What EVOLT actually does, traced from the code: where each number comes from, how it is checked, and what it cannot tell you. Every figure here is read from the repository's own configuration and saved model when the site is built.
One Next.js application serves both experiences and every API from one PostgreSQL database. There are no external APIs: maps are drawn from a bundled outline, and distances are computed locally.
• Charging-session dataset (1,000 sessions, 19 stations) and labelled demo stations with health readings
• Arduino bench log (run 2): three connectors, stored unchanged
• Optional bench-sensor readings over /api/sensor-readings (supporting context only)
• Connector-health analysis: baselines, deviations, trends, faults, temperature correction, forecasts, repair checks
• Learning: session classifiers and the bench degradation model
• Bench scaling: Arduino → representative chargers
• Action-item rules, driver ranking, slot booking
Pure, unit-tested logic in src/features/*; route handlers in src/app/api
• Driver: find, compare and book chargers
• Company workspace: action items, fleet health, forecasts, repair checks, performance, bench, model
PostgreSQL via Drizzle ORM, forward-only SQL migrations. Deployed on Vercel with Neon PostgreSQL.
| Layer | What is used | Where |
|---|---|---|
| Frontend | Next.js 16 (App Router, partial prerendering), React 19, Tailwind CSS 4, Recharts | src/app, src/components |
| Backend | Next.js route handlers, Zod validation, one shared error format | src/app/api, src/lib |
| Database | PostgreSQL with Drizzle ORM; migrations in SQL | database/migrations, src/lib/db/schema.ts |
| Analysis & ML | Pure TypeScript (no ML service): robust statistics, Theil–Sen, logistic regression, ridge, least squares | src/features |
| Tests | Vitest (unit + real-PostgreSQL integration), Playwright (desktop + mobile); GitHub Actions CI | tests/ |
Two pipelines feed the same database. Neither ever falls back to invented data: if the database is missing, the app says so.
1 Records
Stations, chargers, health readings (contact resistance, voltage drop, faults), sessions.
Measured
2 Validated & stored
Units converted to canonical (mΩ, mV), implausible values and future timestamps rejected.
Measured
3 Per-charger analysis
Baseline from the earliest readings, robust deviations, Theil–Sen trends, fault counts.
Calculated estimate
4 Health status & alerts
Normal · Monitor · Inspection recommended · Insufficient data, each with its evidence.
Calculated estimate
5 Used twice
Company: action items, forecasts. Driver: ranking puts healthy evidence first.
1 Arduino log
127 time steps × 3 connectors, as delivered (xlsx), converted cell for cell to CSV.
Measured
2 bench_readings
Loaded unchanged with source row, file and SHA-256; a changed file is refused.
Measured
3 Per-connector analysis
Load, healthy level V_ref, sweeps, reading bands — measured from the readings.
Calculated estimate
4 Model & scaling
Relative-voltage model (tested on held-out data); law-by-law scaling to chargers.
Model output
5 Bench page
Every value tagged as measured, estimate or published reference.
The app keeps these kinds of numbers apart in code, in the database and on screen. This page uses the same tags.
| Kind | Meaning | Examples in EVOLT |
|---|---|---|
| Measured | Logged by our Arduino bench or present in an imported record | Bench voltage/current/degradation; imported session values; recorded health readings |
| Calculated estimate | Derived by a documented formula; not a measurement | Charger-scale heat and voltage loss; temperature-corrected resistance; derived session energy |
| Model output | From a fitted model, with its tested error | Bench degradation index from a voltage; power-loss category; service-date forecast |
| Published reference | Figure published by a standard or manufacturer | IEC 62196-1 ratings; MIDA/HARTING CCS2 datasheets; ADAC charging-loss tests |
| Demo data | Generated, labelled demo records — not real sites | 13 “Demo Site” stations and their readings, labelled Demo everywhere |
There is no live charger telemetry: availability is the last status recorded, and no figure is presented as a real-time measurement.
Three connectors (C1–C3), each with its own fixed load, logged every 0.5 s from 1.2 s to 64.2 s. Each connector is swept repeatedly from near-healthy to an open circuit and back. It is a low-voltage experimental circuit, not a commercial charger.
Per connector and time step: voltage (V), current (mA) and a degradation index (%). 127 time steps from 1.2 s to 64.2 s every 0.5 s = 381 readings, no blank cells.
The log defines degradation per connector as:
degradation % = 100 × (1 − V ÷ V_ref) V_ref = that connector's healthy (0 %) level
Reproduced to within 0.49 points. The ADC is 10-bit on 5 V (one step = 4.88 mV).
Raw workbook database/data/raw/arduino-bench-run-2.xlsx → CSV database/data/arduino-bench-run-2.csv → table bench_readings, run bench-run-2.
Each row keeps its source row, file name and the file's SHA-256. The loader is idempotent and refuses a changed file rather than merging it. Readings of earlier runs are deleted by a migration.
On every bench-page load, the model is re-fitted from the stored rows and checked against the saved model file; the page says whether they match.
Hardware: an Arduino sketch (hardware/evolt_bench) and a serial collector. No ESP32/ESP8266 code is in the repository.
Each connector is summarised from its own readings and never averaged with the others. They genuinely differ, which is why each needs its own reference.
| Parameter | C1 | C2 | C3 | How it is obtained |
|---|---|---|---|---|
| Load resistance | 493 Ω | 350 Ω | 388 Ω | Median V ÷ I, readings ≥ 1 mA |
| Healthy level V_ref | 3.455 V | 3.067 V | 3.193 V | Median V ÷ (1 − degradation/100) |
| Healthy current | 7.01 mA | 8.76 mA | 8.23 mA | V_ref ÷ load |
| Sweep to open circuit | every 15 s (5×) | every 20 s (3×) | every 22.5 s (3×) | Time between open-circuit episodes |
| Near healthy / moderate / severe / open | 8 / 32 / 59 / 28 | 9 / 31 / 63 / 24 | 11 / 30 / 71 / 15 | < 10 % · 10–40 % · ≥ 40 % · 0 V |
| Largest gap to the log's formula | 0.34 pts | 0.35 pts | 0.49 pts | Check that the definition holds |
| 50 % state at 50 kW DC | 200 V lost, 12.5 kW heat | 200 V lost, 12.5 kW heat | 200 V lost, 12.5 kW heat | Method A estimate (same ratio for all) |
| Real wear first visible on the bench (50 kW DC) | 10 × new (5 mΩ) | 20 × new (10 mΩ) | 10 × new (5 mΩ) | Method B vs one ADC step |
Healthy levels differ by 388 mV (12 %). Reading all three against one shared level would misstate degradation by about 1.7 points (median). The run is a repeated sweep — a controlled stress pattern — so it calibrates the voltage–degradation curve but gives no rate of wear over time.
Each quantity is scaled by the physical law that governs it — never by one common multiplier. The results are calculated estimates, not charger measurements.
k_V = V_path ÷ V_ref (voltage) k_I = I_rated ÷ I_healthy (current) k_R = k_V ÷ k_I (resistance, Ohm's law) k_P = k_V × k_I (power per path) ratios (degradation %, loss share) unchanged temperature: not scaled (no calibrated K/W)
Method A keeps the bench's resistance ratio at a chosen degradation level: R_extra = R_load × (V_ref ÷ V − 1); heat = paths × I² × R_extra. Method B starts from a new CCS2 contact (0.5 mΩ, datasheet) × illustrative wear multiples (1, 2, 5, 10, 20) at the charger's current.
Representative operating points, checked against IEC 62196-1 ratings in tests:
AC power factor 0.99; balanced three-phase; 1 h session for energy. The bench supply voltage was not logged; each connector's V_ref stands in for it.
| Quantity | Bench (C1) | Scaled to 7.4 kW AC | Published reference |
|---|---|---|---|
| Voltage across one current path (healthy, 0 %) | 3.455 V | 230 V | IEC 62196-1 maximum AC: 690 V |
| Current in one conductor (healthy, 0 %) | 7.013 mA | 32 A | IEC 62196-1 max 250 A AC |
| Extra series resistance at 50 % degradation | 492.593 Ω | 7.188 Ω | New CCS2 contact: ≤ 0.5 mΩ (MIDA datasheet) |
| Voltage lost in the connector at 50 % | 1.727 V | 115 V | At ≤ 0.5 mΩ and 32 A: ≤ 16 mV per contact (I × R) |
| Heat generated in the connector at 50 % | 6.057 mW | 1,840 W | New contact at 32 A: ≤ 0.51 W per contact (I² × 0.5 mΩ) |
| Share of power lost in the connector | 50 % | 50.251 % | ADAC: 5.1–7 % (11 kW AC wallbox, whole chain) |
A 50 % bench state is a fault at charger scale (115 V lost, ≈ 1.8 kW of heat in one connector at 7.4 kW AC): the bench proves the direction of the effect, not its size. Early real-world wear changes resistance by milliohms and stays below the bench's ADC resolution. ADAC figures cover the whole charging chain, so that comparison is not like-for-like and is labelled as such.
Calibration still needed: a slow one-way wear test, a logged supply voltage, an independent current sensor, four-wire contact-resistance measurement, and a thermal resistance (K/W) from a thermocouple.
Two questions, answered separately and honestly: can degradation be predicted from the bench, and can it be predicted from the session dataset?
degradation % = 100.11 − 100.23 × (V ÷ V_ref) clipped 0–100 V_ref: C1 3.455 V · C2 3.067 V · C3 3.193 V
Feature: voltage relative to the connector's healthy level. Least squares on 314 conducting readings below 100 % (open circuit is 100 % by definition and excluded). The coefficients rediscover the log's own definition; what the data adds is that V_ref must be per connector.
Inputs above 5 V are refused: they are not bench readings, and scaling electrical values does not scale degradation.
| Model | Held-out sweeps | Unseen connector |
|---|---|---|
| One voltage line for all | 2.28 | 1.05–4.52 |
| A line per connector | 0.09 | needs full history |
| Relative voltage (deployed) | 0.09 | 0.09–0.12 |
Held-out sweeps: each sweep of each connector left out in turn. Unseen connector: trained on the other two, given only that connector's first sweep (commissioning) to set V_ref.
The 1000-row unified training file has Arduino-style columns derived from the session data (its degradation equals clip(100 × power_loss ÷ power_consumed)). A ridge regression study with station-grouped cross-validation and a time-ordered hold-out shows why no predictor is deployed from it:
| Features | Split | Baseline MAE | Ridge MAE | R² |
|---|---|---|---|---|
| Loss and consumption (the target's own inputs) | Held-out stations | 17.08 | 0.07 | 1 |
| Loss and consumption (the target's own inputs) | Latest 20 % in time | 16.38 | 0.07 | 1 |
| All session measurements except power loss | Held-out stations | 17.08 | 12.53 | 0.547 |
| All session measurements except power loss | Latest 20 % in time | 16.38 | 12.05 | 0.549 |
| …and without current or power (which sit in the target's denominator) | Held-out stations | 17.08 | 18.29 | -0.007 |
| …and without current or power (which sit in the target's denominator) | Latest 20 % in time | 16.38 | 18.25 | -0.048 |
With the target's own inputs the fit is perfect because it is the definition; without them, nothing beats the baseline. Deployed from this dataset: nothing.
• The model maps a bench voltage to the bench's index; it does not measure or forecast real connector wear, and no field wear rate is used.
• Errors are on one run of one rig; a different rig needs its own commissioning sweep. Near open circuit (V ÷ V_ref below the fitted range) estimates are flagged as extrapolated.
The supplied charging-session dataset (1,000 sessions, 19 stations, 1–11 Jan 2024) is checked, not trusted blindly. Its origin is unverified, so findings describe the file, not a real network.
• Values validated and units converted; duplicates refused.
• Inconsistencies are surfaced, not silently fixed: sessions losing more power than they consume, and sessions delivering more energy than the battery holds, become data-quality action items.
• Fields the data lacks (availability, downtime, price) are not shown, and the app says why.
• Features: numeric columns present in every row plus one-hot area and charging type; identifiers excluded.
• Models: majority baseline, an explainable threshold model, multinomial logistic regression.
• Stratified 5-fold cross-validation; a label counts as predictable only if the best model beats guessing by ≥ 5 points and by more than two fold standard deviations.
• Fitted parameters are stored in model_runs and reloaded for each prediction (/api/models/predict).
• Predictions are refused for labels that were not predictable, and inputs outside the training range are flagged.
| Question | Cross-validated accuracy | Guessing | Verdict |
|---|---|---|---|
| Power-loss category | 91.7 % | 37.4 % | Predictable — rule: ≤ 1.88 kW Low · ≤ 3.67 kW Medium · else High (it recovers the labelling rule) |
| …without the power-loss reading | 37.0 % | 37.4 % | Not predictable: loss must be measured |
| Voltage-stability category | 41.5 % | 40.4 % | Not predictable — no prediction offered |
Results of the training run on the supplied dataset, as recorded in docs/machine-learning.md; the live figures are on the Learned model page.
Charger analytics turn readings into a short list of what needs attention, each item with the rule that fired, the evidence, why it matters and the next step.
| Rule | Fires when (all thresholds from the analysis config) | Severity |
|---|---|---|
| Persistent deviation | 3 of the last 5 readings exceed the baseline median by > 3.5 robust spreads (median/MAD) and by ≥ 8 % | Monitor; inspection when ≥ 30 % above baseline |
| Sustained trend | Theil–Sen slope over the last 10 readings: ≥ 75 % of pairwise slopes adverse and a change ≥ 15 % of baseline (and ≥ 3 spreads) | Monitor; inspection at ≥ 40 % |
| Fault recurrence | Connector faults in 30 days ≥ 3 (above the historical rate) | Monitor; inspection at ≥ 8 |
| Insufficient data | Fewer than 6 baseline or 3 recent readings — never treated as healthy | Insufficient data |
Metrics: contact resistance (mΩ), connector voltage drop (mV), connector faults (count). Readings older than 45 days are labelled historical.
Temperature correction restates resistance at 20 °C (copper, 0.393 %/°C), all-or-nothing per metric so it never creates jumps.
Theil–Sen rate over up to 12 readings since the last maintenance, with an 80 % Sen interval, to the operator's limit (default baseline +50 %). No date unless the rise is statistically clear; within 30 days it becomes an action item.
After recorded work, later readings decide: held, relapsed, partly improved or no effect (3+ readings over 14+ days).
Action items: high = inspection recommended; medium = statistically clear station excess or significant data issues; low = data gaps.
Reviewers mark items reviewed / resolved / dismissed (admin token on the live site). “Resolved” records a decision; it never claims a repair.
An anomaly is a reason to inspect, not proof of a fault.
Find a charger you can rely on, see why it ranks where it does, and optionally book it.
1 Location
Browser location (with permission), a city from the data, or typed coordinates. A denied permission is never replaced by a guess.
2 Radius filter
Default 25 km (choices 5–100). Stations without recorded coordinates are listed separately, never ranked by distance.
3 Distance
Haversine great-circle distance on stored coordinates — straight-line, not driving distance.
Calculated estimate
4 Hard constraints
Not offline; your connector (or vehicle profile); minimum power; optionally ‘available only’; inspection-flagged chargers excluded unless included.
5 Ranking
Health tier → reported availability → distance in 5 km bands → usable power → exact distance. Each result explains itself.
• Compatibility uses the charger's recorded connector type; chargers with no recorded connector are left out when a connector is chosen, because compatibility cannot be confirmed.
• Usable power = rated power capped by the vehicle profile's DC limit.
• Availability is the last status recorded with its time — not live. “Unknown” is never shown as available. Price and opening hours are not in the data, so they are not used.
• Compare up to four chargers side by side; only objective bests are highlighted.
• Book a slot (today + 7 days, quarter-hour starts) or join the queue. A 10-minute gap is kept after every booking so a late finish never runs into the next driver; several vehicles can be planned across a station's chargers. Times are IST. No account: a private booking code, stored only as a hash.
• Offline and inspection-flagged chargers do not take bookings. The booking does not know whether the previous driver has actually left.
All endpoints are same-origin JSON. Reads are open; operator changes need the admin token in token mode (the live site); driver bookings are protected by same-site checks, validation and booking codes.
| Area | Endpoints | Database |
|---|---|---|
| Driver | GET /api/recommendations · /api/stations · /api/stations/[id] · /api/recommendations/vehicles | stations, chargers, alerts, vehicle_profiles |
| Bookings | GET /api/bookings/availability · POST /api/bookings · /fleet · /lookup · /cancel | bookings (per-charger advisory lock) |
| Company | GET /api/analytics/overview · /api/insights · /api/alerts · /api/chargers/[id] · /api/maintenance · /api/company/performance; PUT/POST review, alert, maintenance, service limit (admin) | alerts, alert_events, health_observations, maintenance_events, insight_reviews, charging_sessions |
| Models | GET /api/models · POST /api/models/predict | model_runs |
| Bench | GET /api/bench (scenario, connector, level) · GET /api/bench/estimate (voltage, connector or referenceV) | bench_readings + models/degradation-report.json |
| Platform | GET /api/health · POST /api/sensor-readings (optional bench sensor, supporting only) | sensor_readings |
| Situation | Response |
|---|---|
| Invalid input (Zod) | 400 validation_error with the field and reason; unknown query parameters are ignored, unknown body fields refused |
| Not found / conflict | 404 not_found · 409 conflict (e.g. a booking overlapping another booking or its gap, with the next free start) |
| Not allowed | 401 unauthorized: cross-site requests, or operator changes without the admin token |
| Database missing or down | 503 configuration_error / database_unavailable — never fake data; /api/health reports not_configured, not_migrated or unreachable |
| Anything else | 500 internal_error without internal details |
Stated plainly, so nobody has to guess.
They typically alert on a threshold. EVOLT follows each connector against its own baseline, separates real rises from hot-day effects (temperature correction), forecasts when a connector reaches its service limit with a range, and checks afterwards whether a repair held.
No. Readings come from the stored records and the Arduino log. Availability is the last recorded status with its time. The app never presents a figure as real-time.
On the bench's own index: 0.09 points mean absolute error on sweeps it never saw, 0.09–0.12 on an unseen connector given one commissioning sweep. Its accuracy on real connector wear is not known — that has not been measured.
Their healthy levels differ by 12 %. One shared voltage line errs by 2.28 points on held-out sweeps; using each connector's own reference cuts that to 0.09.
Quantity by quantity: voltage by k_V, current by k_I, resistance by k_V ÷ k_I, power by k_V × k_I, ratios unchanged. The result shows the bench's worn states are faults at charger scale — the bench demonstrates the direction of the effect, not its size.
Yes. Every alert carries a confidence note: a deviation from a charger's own history is not proof of degradation; method changes, sensor faults or conditions can cause it. It is a reason to inspect.
Missing data is never treated as healthy: the charger is shown as “Insufficient data”, and drivers see that label rather than a guess.
Unit tests check every rule, formula and model against independent pandas/SciPy figures; integration tests run the real API handlers on PostgreSQL; Playwright runs both journeys on desktop and mobile; GitHub Actions runs it all on every pull request.
Sources in the repository: src/config (analysis, scaling, booking), models/degradation-report.json, database/data, docs/. Health categories describe recorded evidence and are not safety certifications.