Self-hosted charge session logger and dashboard for the Tesla Wall Connector Gen 3
1.7K
A self-hosted logger for the Tesla Wall Connector Gen 3. It polls the charger's local API, stores the readings, and — the part nothing else does — derives charge sessions with real start and end times.
The charger keeps no history: it reports what is happening right now, plus lifetime counters that only go up. Phone apps read those counters when you happen to open them, so the numbers land on the day you opened the app rather than the day you charged. Something that runs continuously does not have that problem. Everything stays on your network — no account, no cloud, no vendor API.
/metrics endpointImages are published on GHCR — ghcr.io/mews-se/wallconnectorlog and its companion
…/wallconnectorlog-grafana — and mirrored to Docker Hub under mewsse/. Same builds,
same tags, amd64 and arm64. latest follows the main branch, and docker compose pull picks up
new versions. To build from source instead, clone this repository and add build: . and
build: grafana to the two services.
Two files are all you need — no clone, no directories to prepare:
mkdir wallconnectorlog && cd wallconnectorlog
curl -O https://raw.githubusercontent.com/mews-se/wallconnectorlog/main/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/mews-se/wallconnectorlog/main/.env.example
Set WC_HOST in .env to the charger's address (if curl http://CHARGER-IP/api/1/version
returns JSON, you have the right one), then:
docker compose up -d
Open http://localhost:4680 for the app — the port is the Tesla cell format — and
http://localhost:3399 for Grafana; a Graphs in Grafana link also appears on the main page
once it answers.
The database lands in ./data, created and owned correctly on first start: the logger container
starts as root, fixes the bind mount that Docker creates root-owned, and drops to uid 1000
before running anything. Set user: on the service to skip that and manage the directory
yourself. To check the configuration without starting anything:
docker compose run --rm wallconnectorlog python wallconnectorlog.py check
Opens straight onto the dashboard: the datasource, the dashboard, the SQLite plugin and no-login
anonymous access are all baked into the wallconnectorlog-grafana image, and its own state
lives in a named volume. Visitors get the Viewer role — add
GF_AUTH_ANONYMOUS_ORG_ROLE=Editor under the grafana service's environment: to edit panels
from the browser (any GF_* setting can be overridden the same way). To run without Grafana,
start just the logger: docker compose up -d wallconnectorlog.
| Variable | Default | Meaning |
|---|---|---|
WC_HOST | – | Charger address. Required. |
WC_DB | /data/wallconnectorlog.db | SQLite file |
WC_PORT | 4680 | Port to listen on. Under Docker, change the ports: line instead. |
WC_INTERVAL_CHARGING | 5 | Poll seconds while charging |
WC_INTERVAL_CONNECTED | 15 | Poll seconds while plugged in but idle |
WC_INTERVAL_IDLE | 60 | Poll seconds while nothing is plugged in |
WC_RETAIN_DAYS | 90 | How long raw samples are kept. Sessions are kept forever. |
WC_BACKUP_INTERVAL_H | 24 | Hours between automatic backups. 0 turns them off. |
WC_BACKUP_KEEP | 7 | How many backups to keep |
WC_BACKUP_DIR | <db folder>/backups | Where backups are written |
WC_GRAFANA_URL | :3399 in Docker | Where the Grafana link points. A bare port follows the host you browse from; empty hides the link. |
WC_GRAFANA_HEALTH | http://grafana:3000/api/health | Checked before the link is shown |
Everything lives in one SQLite file, but do not copy it while the service runs — WAL mode
keeps recent writes in a separate -wal file, and a plain copy silently loses them. These take
consistent snapshots of the live database instead:
data/backups/ every 24 hours, keeping the last 7docker compose exec wallconnectorlog python wallconnectorlog.py backupcurl -OJ http://localhost:4680/api/backup (/api/backups lists them)Restore with the service stopped — it refuses a database that is in use, verifies the file first, and saves the current database alongside before replacing it:
docker compose stop wallconnectorlog
docker compose run --rm wallconnectorlog python wallconnectorlog.py restore /data/backups/FILE.db
docker compose start wallconnectorlog
| Path | Returns |
|---|---|
/ | The web page |
/api/live | Latest reading, lifetime counters, device info, open session |
/api/sessions | The 200 most recent sessions |
/api/history?hours=24 | Raw samples for a period |
/api/errors | Recent failed polls |
/metrics | Prometheus exposition |
/api/backup | Downloads a consistent snapshot of the database |
/api/backups | Lists stored backups |
/healthz | 200 while the poll loop is alive, 503 if it has stalled. An unreachable charger is reported in the body, not treated as unhealthy. |
Tesla documents none of the API. evse_state is translated using the mapping the Home Assistant
integration, the ioBroker adapter and the Wall Monitor app agree on — with one deliberate
difference: state 7 is not an error. Existing lists call it "error" or "finished charging",
but in captured charge sequences (1 → 7 → 9 → 11 → 9 → 1) it appears immediately after
plugging in, with no session energy and no current — a transient hand-shake, shown here as
"Starting up".
evse_not_ready_reasons, config_status and current_alerts are stored raw and never
interpreted: no published mapping reconciles with what real chargers emit, and the Wall Monitor
authors state outright that they do not know what the alert counter counts. A high alert_count
is normal, not a fault.
Two firmware differences are handled: older units report a single relay_coil_v where newer
ones report relay_k1_v and relay_k2_v, and wifi_ssid arrives base64-encoded and is decoded
before being stored.
vehicle_connected going true opens a session; going false closes it. While it is open, the
charger's own session_energy_wh and session_s are tracked, along with peak power, peak
handle temperature and average grid voltage. Time with contactor_closed set is counted
separately, so a session records both how long the car was plugged in and how long it actually
drew current. Power is computed per phase as voltage times current, forced to zero while the
contactor is open — the charger reports a volt or two of noise on idle phases.
A note on the lifetime counters: charge_starts counts contactor cycles, not sessions — a
single plug-in usually produces several. Do not divide lifetime energy by it and call the result
an average session.
tests/ runs against a scripted stand-in charger, no hardware needed: session derivation from a
replayed charge, backup completeness from a running instance, restore safety, and a
demonstration of the WAL trap above. CI runs them on every push, plus lint and a container smoke
test.
python3 tests/test_session.py
python3 tests/test_backup.py
MIT. See LICENSE.
Not affiliated with, endorsed by, or supported by Tesla. "Tesla" and "Wall Connector" are trademarks of Tesla, Inc., used here only to say what this talks to.
Content type
Image
Digest
sha256:96b499c0a…
Size
16.2 MB
Last updated
2 days ago
docker pull mewsse/wallconnectorlog