EV Charging Optimizer

Full-Stack
2026-05-01T16:27:54Z
TypeScriptNode.jsExpressAngularIonic

EV Charging Optimizer is a hybrid mobile web app that picks the cheapest, cleanest hours to charge an electric vehicle before a stated departure time. Wallbox and Tesla apps schedule by clock; this one schedules by the grid. By blending Nord Pool spot prices with grid carbon intensity and the user's deadline, it can shift the same kWh into the cheapest, lowest-CO₂ window — typically 20–60% cheaper than 'plug in now until full,' which is the headline number returned as `savingsVsNaive` on every optimize response.

The optimizer itself is a few dozen lines of pure TypeScript in `backend/src/services/optimizer.ts`. For each hourly slot in the forecast window it computes `score = (1 - w) · norm(price) + w · norm(carbon)` where `w` is the user's `carbonWeight` (0 = pure cost, 1 = pure carbon, 0.5 = balanced), and `norm()` is min-max across the window. The K cheapest-by-score full hours are picked, plus a fractional last hour for the remainder, then reassembled in chronological order so the schedule renders cleanly as a bar chart. `savingsVsNaive` compares the result against 'charge straight through from now' so the cost win is provable per-request, not aspirational.

The stack is Express 4 + Node 20 + strict TypeScript on the backend (Zod validation at every API boundary, helmet + cors + morgan + compression for chrome, dotenv for config), Ionic 8 + Angular 19 on the frontend (standalone components, signals, OnPush components with eventCoalescing zones, no NgModules, ngx-translate for i18n). The REST surface covers `/api/optimize`, `/api/forecast`, `/api/push/key`, `/api/push/schedule`, `/api/health`, `/api/version`, and `/api/metrics`. On top of that a WebSocket endpoint at `/api/live` broadcasts a 5-second price + carbon tick to subscribed regions, with origin allowlist, per-IP connection caps, and ping/pong heartbeats — the frontend's `live-tick-pill` consumes it for ambient grid awareness. Live data comes from elprisetjustnu.se (Nord Pool spot, region-aware: SE1–SE4) and carbonintensity.org.uk (UK National Grid CO₂); both have deterministic mock fallbacks so the demo runs offline and during API outages. Web Push is wired via VAPID + the `web-push` library, gated by env vars — without `VAPID_PUBLIC_KEY` set, push routes degrade gracefully and the rest of the app keeps working.

The deploy is the punchline: one Railway service, one Node process, one origin. A multi-step Nixpacks build compiles the Angular app into `frontend/www/` and the Express server into `backend/dist/`, then `node backend/dist/server.js` mounts `/api/*` for the API, the WS upgrade handler at `/api/live`, and serves the built PWA from the same Express instance with a SPA fallback. No CDN, no separate frontend host, no CORS dance. The 'hybrid mobile' part lives in the service worker — installable via Add to Home Screen on iOS/Android, with offline shell caching and Web Push handling — so the portfolio brief gets satisfied without burning weeks on App Store review. Capacitor wrapping (`npx cap init` + `cap add ios/android`) is a one-evening add behind the same code if a native binary is later needed.

  • Min-max-normalized scoring blends spot price and grid carbon: score = (1−w)·norm(price) + w·norm(carbon), with carbonWeight slider exposed to the user
  • Per-request `savingsVsNaive` proves the cost win against 'charge straight through from now' — every response carries its own headline number
  • Region-aware Nord Pool fetch (SE1–SE4) plus UK National Grid CO₂ intensity, with deterministic mock fallback so the demo never breaks
  • K-cheapest-hours scheduler picks full hours by score and a fractional final hour for the remainder, reassembled chronologically for clean bar-chart rendering
  • Hybrid mobile via installable PWA: custom service worker for offline shell + Web Push handling, no App Store review required
  • Web Push notifications via VAPID (env-gated): missing keys degrade gracefully so the rest of the app still runs
  • Single Railway service for both API and PWA: Nixpacks builds Angular into frontend/www/, Express serves /api/* + the SPA shell from one origin
  • Strict TypeScript (NodeNext ESM) with Zod request validation at every /api/* route boundary
  • Ionic 8 + Angular 19 frontend: standalone components, signals, OnPush on key components atop zone CD with eventCoalescing, no NgModules
  • WebSocket /api/live broadcasts a 5 s price + carbon tick per region (SE1–SE4) with origin allowlist, per-IP connection caps, and ping/pong heartbeats; consumed by the `live-tick-pill` UI
  • i18n via @ngx-translate/core with HTTP-loaded JSON catalogs and a Settings page for language + region preferences
  • Operational endpoints /api/health, /api/version, and /api/metrics (heap, RSS, cache stats, live-tick clients) for Railway health checks and observability
  • Capacitor-ready: one evening behind a flag from a real native iOS/Android binary if the portfolio brief later demands one