A PWA for EV drivers β no app store required.
Scan a QR code on a charger, start charging, watch it live, get your receipt. Works on any phone browser. Built on FastAPI + Jinja2 + HTMX. No npm, no React, no build step.
Driver scans QR β Charger info β Start Charging β Live updates (SSE) β Stop β Receipt
- Driver scans QR code on charger β
/charge/CP001/1 - Sees connector status, pricing, specs
- Taps Start Charging
- Live screen: power (kW), energy (kWh), SoC%, duration, cost
- Taps Stop or unplugs β session summary + optional PDF receipt
The app is built around three separate concerns:
core/ Generic engine (API proxy, feature flags, JWT middleware)
routes/ Base page routes (home, charge, session, receipt, auth, QR, push)
plugins/ Optional modular features (account, favorites, receipts)
skins/ Branding layer (colors, logo, CSS)
templates/ Base Jinja2 templates
static/ Shared JS/assets (htmx, service worker, i18n, manifest)
Each skin lives in skins/<name>/ and contains:
skins/my-brand/
skin.json Metadata (name, version, colors)
static/
style.css Full mobile-first CSS β overrides all base styles
logo.svg Your brand logo (optional)
favicon.svg Your favicon (optional)
Set SKIN=my-brand in your .env to activate it.
The skins/stroomlijnen/ folder is included as a real-world example skin.
The skins/default/ folder is the generic built-in skin β clean, neutral, no branding.
Plugins are FastAPI routers that auto-register. Enable them via PLUGINS=account,receipts.
Each plugin lives in plugins/<name>/ and may include:
routes.pyβ FastAPI routertemplates/<name>/β Jinja2 templates (searched after skin, before base)__init__.py
Template resolution order: skin β plugins β base
core/middleware.py runs on every request and injects into request.state:
flagsβ feature flags (from Core API)skinβ active skin nameaccountβ decoded JWT payload (orNone)langβ detected language (nl/en)tβ translation dict
# 1. Clone and set up
git clone <repo>
cd ocpp-charge-app
cp .env.example .env
# Edit .env β set OCPP_CORE_API and JWT_SECRET at minimum
# 2. Install dependencies
pip install -r requirements.txt
# 3. Run
python main.pypython -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python main.pyAll settings are environment variables. Copy .env.example to .env:
| Variable | Default | Description |
|---|---|---|
OCPP_CORE_API |
http://localhost:8000 |
URL of your OCPP Core API backend |
APP_TITLE |
OCPP Charge |
App name shown in browser tab and headers |
APP_HOST |
0.0.0.0 |
Host to bind the server |
APP_PORT |
8003 |
Port to listen on |
JWT_SECRET |
change-me-in-production |
Secret key for JWT auth cookies β change this! |
SKIN |
default |
Skin folder name under skins/ |
PLUGINS |
receipts,account |
Comma-separated list of enabled plugins |
-
Copy the default skin as a starting point:
cp -r skins/default skins/my-brand
-
Edit
skins/my-brand/skin.json:{ "name": "My Brand", "version": "1.0", "colors": { "primary": "#ff6600", "background": "#1a1a2e" } } -
Edit
skins/my-brand/static/style.cssβ override CSS variables and styles. -
Add your
logo.svgandfavicon.svgtoskins/my-brand/static/. -
Set
SKIN=my-brandin.env.
The skin CSS is loaded instead of (not in addition to) the base static CSS β you have full control.
Create a folder under plugins/:
plugins/my-plugin/
__init__.py
routes.py # FastAPI APIRouter
templates/
my-plugin/
page.html
In routes.py:
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
router = APIRouter()
@router.get("/my-plugin/page", response_class=HTMLResponse)
async def my_page(request: Request):
templates = request.app.state.templates
return templates.TemplateResponse(request, "my-plugin/page.html", {
"flags": request.state.flags,
"t": request.state.t,
"account": request.state.account,
})Add my-plugin to PLUGINS in .env.
GET / Home / map
GET /charge/{cp_id}/{connector} QR landing β charger info + start button
POST /charge/{cp_id}/{connector}/start Start charging session
GET /session/{session_id} Live charging screen (SSE)
POST /session/{session_id}/stop Stop session
GET /receipt/{session_id} Session receipt
GET /account/login Login page (account plugin)
GET /account/register Register page (account plugin)
GET /account/profile Profile (requires login)
GET /account/history Session history (requires login)
GET /account/settings Settings / language switcher
- FastAPI β web framework
- Jinja2 β templating
- HTMX β 14KB, handles SSE + forms (only JS dependency)
- PyJWT β JWT auth cookies
- Leaflet.js β map (CDN, optional)
- No npm, no build step, no React
Apache 2.0 β see LICENSE.