🇬🇧 English summary below — Jump to English section
Interface web de visualisation et d'analyse des sessions de charge pour bornes Morec / EVSEMaster.
Deux modes d'acquisition des données :
- UDP direct (v1.5+) : connexion temps réel à la borne, enregistrement automatique des sessions
- Import XLSX : import manuel des exports EVSEMaster (compatible avec l'historique existant)
EVSE Stats is a self-hosted web dashboard for visualising EV charging sessions from Morec / EVSEMaster wallboxes.
Two data acquisition modes are supported:
- Direct UDP (v1.5+): real-time connection to the charger via the EVSEMaster UDP protocol (port 28376), automatic session recording — no manual export needed.
- XLSX import: manual import of EVSEMaster
.xlsxexports — compatible with existing history.
The HC/HP tariff split targets French EDF contracts. Rules are fully configurable from the Settings page (full-HC days, time windows). The Tempo tariff (3-tier: Blue/White/Red days published daily by RTE) is not yet supported as it requires a live external API.
- Direct UDP integration — real-time voltage, current, power, automatic session recording
- Live charge banner: energy consumed (kWh) + elapsed duration updated every 5 s
- Import
.xlsxexports with automatic deduplication - HC/HP cost calculation (minute-by-minute session splitting)
- Configurable HC/HP rules: full-HC days, multiple time windows — adapts to any French EDF contract
- Dashboard: KPIs, charts, monthly ranking, yearly summary
- Tariff history with validity periods (recalculate at any time)
- Consumption alerts via webhook (ntfy, Slack, Discord…)
- Monthly PDF reports
- Vehicle page: specs, photo, cost per 100 km
- Hourly frequency chart
- CSV export, SQLite backup script
Dashboard — KPIs, graphiques HC/HP, coût réel vs économies, bannière charge en cours

Import XLSX — drag & drop, historique des imports avec déduplication cross-source

Sessions de charge — tableau paginé, filtres, badge UDP

Véhicule — specs, photo, données WLTP constructeur, KPIs au kilomètre

Chargeurs — configuration borne UDP, test de connexion

Alertes — seuils kWh et € avec webhook

Paramètres — historique des tarifs EDF

git clone https://github.com/picardflo/evstats.git
cd evstats
docker compose up -d --buildApp available at http://localhost:8080
Then go to Paramètres (Settings) to configure your tariff rates before importing data.
To expose behind Caddy or Nginx, disable the port mapping via docker-compose.override.yml:
# docker-compose.override.yml
services:
evstats-frontend:
ports: []See docker-compose.override.yml.example for a full homelab example.
| Layer | Technology |
|---|---|
| Backend | Python 3.12 · FastAPI · SQLModel · SQLite |
| Frontend | React 18 · Vite · Material UI (dark) · Recharts |
| Container | Docker + Docker Compose |
MIT — see LICENSE
- Page Chargeurs : ajout/modification/suppression de bornes via l'interface
- Support IP ou FQDN (résolution DNS locale)
- Test de connexion avec découverte automatique du numéro de série
- Upload photo de la borne
- Statut temps réel : tension (V), courant (A), puissance (kW) via bouton Rafraîchir
- Polling automatique toutes les 30 s en veille, continu pendant la charge
- Détection automatique début/fin de session (seuil > 0.5 A, 3 lectures consécutives à 0 pour confirmer)
- Enregistrement automatique des sessions terminées en base (calcul HC/HP + coût)
- Bannière "Charge en cours" sur le Dashboard : tension · courant · puissance · énergie · durée (rafraîchi toutes les 5 s)
- Persistance restart-proof : l'état de la session active (start_time + énergie) survit aux redémarrages du backend
- Import manuel de fichiers
.xlsx(drag & drop ou sélection) - Déduplication automatique des sessions (basée sur
Numéro d'enregistrement) - Déduplication cross-source : si une session UDP couvre déjà la même plage horaire, la session XLS est ignorée à l'import — évite les doublons lors d'un réimport après mise en service UDP
- Historique des imports (date, fichier, nouvelles sessions, doublons)
- Répartition HC / HP au niveau de la minute pour chaque session
- Gestion des sessions chevauchant plusieurs plages tarifaires
- Tarifs EDF éditables via l'interface (sans redéploiement)
- Historique des périodes tarifaires avec
valid_from(bon tarif selon la date de la session) - Recalcul des coûts sur toutes les sessions existantes en un clic
- Gestion multi-véhicules avec photo (JPEG/PNG/WebP)
- Véhicule actif (modèle "imprimante par défaut") — un seul à la fois
- KPIs au kilomètre calculés sur l'ensemble des sessions : km rechargés, coût/100km, économies/100km, pleins équivalents
- Données constructeur WLTP : 6 valeurs optionnelles été/hiver × mixte/autoroute/ville (en kWh/100km)
- Autonomies théoriques calculées pour chaque cas d'usage WLTP
- Indicateur d'écart conso réelle vs WLTP été mixte (coloré vert/orange/rouge)
- Autocomplete à la saisie du nom : 27 modèles pré-remplis (batterie + conso + 6 valeurs WLTP)
- KPIs : sessions, énergie, HC, HP, coût total, coût moyen/session, coût effectif c€/kWh, économies réalisées vs 100% HP
- Tendances mois N vs mois N-1 (↑↓ avec %)
- Graphiques : consommation HC/HP empilée, coût réel + économies, durée de charge, fréquence horaire
- Camembert répartition HC/HP
- Vues : 30 jours, journalière, mensuelle
- Classement des mois avec % HC, coût moyen/session, économies
- Récapitulatif annuel
- Tableau paginé avec filtres (statut de fin, plage de dates)
- Chip visuel sur les sessions enregistrées automatiquement via UDP
- Export CSV des sessions filtrées
- Édition des tarifs HC et HP (en c€/kWh)
- Règles HC/HP configurables : jours entièrement HC, plages horaires (plusieurs fenêtres possibles)
- Recalcul HC/HP + coûts sur l'historique complet en un clic
- Alertes consommation mensuelle (seuil kWh / €, webhook ntfy/Slack/Discord)
Option tarifaire : Heures Creuses + Week-end + Mercredi — 12 kVA
| Type | Prix (c€/kWh) |
|---|---|
| HC semaine | 17,24 |
| HP semaine | 23,05 |
| HC week-end | 17,24 |
| HP week-end | 17,24 |
| HC mercredi | 17,24 |
| HP mercredi | 17,24 |
| Période | Règle |
|---|---|
| Mercredi | 100% Heures Creuses |
| Samedi & Dimanche | 100% Heures Creuses |
| Lun / Mar / Jeu / Ven | HC : 23h30 → 07h30 · HP : 07h30 → 23h30 |
Les tarifs changent typiquement deux fois par an (février et août). Ils sont modifiables directement dans l'interface via Paramètres → Tarifs EDF, sans redéploiement.
evstats/
├── backend/
│ ├── app/
│ │ ├── main.py # Endpoints FastAPI + lifespan (démarrage poller)
│ │ ├── models.py # Modèles SQLModel (ChargingSession, Charger, ...)
│ │ ├── database.py # Connexion SQLite
│ │ ├── parser.py # Parsing XLSX → ParsedSession
│ │ ├── tariff.py # Calcul HC/HP (découpage minute-à-minute)
│ │ ├── udp_client.py # Protocole UDP EVSEMaster (port 28376)
│ │ └── charger_poller.py # Poller asyncio : cache statut + détection sessions
│ ├── migrate_utc_to_paris.py # Migration one-shot : correction timezone UTC→Paris
│ ├── requirements.txt
│ └── Dockerfile
├── frontend/
│ ├── src/
│ │ ├── api/client.js # Appels axios vers /api
│ │ ├── components/
│ │ │ └── Layout.jsx # Sidebar + navigation
│ │ └── pages/
│ │ ├── Dashboard.jsx # Graphiques + KPIs + bannière charge en cours
│ │ ├── Chargers.jsx # Gestion des bornes + statut temps réel
│ │ ├── Import.jsx # Drag & drop xlsx
│ │ ├── Sessions.jsx # Tableau filtrable + export CSV
│ │ ├── Vehicle.jsx # Véhicule actif + KPIs/km
│ │ └── Settings.jsx # Configuration tarifs + règles HC/HP + alertes
│ ├── package.json
│ ├── vite.config.js
│ ├── nginx.conf
│ └── Dockerfile
├── scripts/
│ ├── backup.sh # Backup quotidien SQLite (cron)
│ └── scrape_ev_database.py # Génération vehicles_db.json (one-shot, voir ci-dessous)
├── docker-compose.yml
├── VERSION # Version unique (lue par backend + frontend)
└── .gitignore
/app/data/ # Monté depuis /srv/docker_data/evstats/ sur la VM
├── evstats.db # Base SQLite
├── active_charges.json # Sessions UDP en cours (persistance restart)
├── charger_images/ # Photos des bornes
└── vehicle_images/ # Photos des véhicules
| Couche | Technologie |
|---|---|
| Backend | Python 3.12 · FastAPI · SQLModel · pandas · openpyxl |
| Base de données | SQLite |
| Frontend | React 18 · Vite · Material UI (dark theme) · Recharts |
| Reverse proxy | Caddy (existant sur la VM) |
| Conteneurs | Docker + Docker Compose |
Le backend communique directement avec les bornes Morec via le protocole UDP propriétaire EVSEMaster (reverse-engineered depuis evsemasterudp).
Port : 28376 (écoute locale, la borne broadcast vers ce port)
Flow d'authentification :
Borne → broadcast 0x0001 (~toutes les 5s)
Client → RequestLogin 0x8002
Borne → LoginOK 0x0002
Client → LoginConfirm 0x8001
Client → GetStatus 0x8004
Borne → StatusResponse 0x0004
Payload StatusResponse (borne Morec MC20CAPP, offsets validés) :
| Offset | Type | Facteur | Valeur |
|---|---|---|---|
| [1:3] | uint16 BE | ×0.1 | Tension (V) |
| [3:5] | uint16 BE | ×0.01 | Courant (A) |
| [7:9] | uint16 BE | ×1.0 | Puissance (W) |
| [9:13] | uint32 BE | ×10 Wh | Compteur énergie absolu (Wh, total vie de la borne) |
Contrainte : une seule session UDP à la fois. L'application EVSEMaster mobile doit être fermée pendant les requêtes du poller. Le verrou
asyncio.Lockpartagé empêche les requêtes manuelles (bouton Rafraîchir) et le poller de s'exécuter simultanément.
La version est définie dans un unique fichier VERSION à la racine du repo.
- Le backend lit ce fichier et l'expose via
GET /api/version - Le frontend interroge cet endpoint et affiche la version dans le footer
echo "1.7.0" > VERSION
git add VERSION && git commit -m "chore: bump version 1.7.0"
git push
# Sur la VM :
git pull && docker compose up -d --build| Champ | Type | Description |
|---|---|---|
| id | INTEGER PK | Identifiant interne |
| record_id | TEXT UNIQUE | Clé de déduplication (EVSEMaster ou UDP-{id}-{timestamp}) |
| charger_id | TEXT | Nom de la borne |
| start_time | DATETIME | Début de session (heure locale Europe/Paris) |
| end_time | DATETIME | Fin de session (heure locale Europe/Paris) |
| duration_minutes | FLOAT | Durée en minutes |
| energy_kwh | FLOAT | Énergie consommée |
| hc_kwh | FLOAT | Part HC (calculée) |
| hp_kwh | FLOAT | Part HP (calculée) |
| cost_eur | FLOAT | Coût calculé |
| end_status | TEXT | Motif de fin : Pull Plug / Fix Time / Power Down / UDP Auto |
| start_user | TEXT | Initiateur : Clock / RFID / UDP Auto |
| source | TEXT | xlsx (import manuel) ou udp (enregistrement automatique) |
| Champ | Type | Description |
|---|---|---|
| id | INTEGER PK | Identifiant interne |
| name | TEXT | Nom affiché |
| ip | TEXT | Adresse IP ou FQDN |
| password | TEXT | Mot de passe 6 chiffres |
| serial | TEXT | Numéro de série hex (découvert lors du test de connexion) |
| src_port | INTEGER | Port source de la borne (défaut 6186) |
| is_enabled | BOOLEAN | Active/désactive le polling automatique |
| image_filename | TEXT | Nom du fichier photo (stocké dans /app/data/charger_images/) |
| last_seen | DATETIME | Dernière réponse UDP réussie |
| Méthode | Endpoint | Description |
|---|---|---|
| POST | /api/import |
Import d'un fichier .xlsx |
| GET | /api/sessions |
Liste paginée + filtres (end_status, start_date, end_date) |
| GET | /api/sessions/export |
Export CSV (mêmes filtres) |
| GET | /api/stats/daily |
Agrégats journaliers |
| GET | /api/stats/monthly |
Agrégats mensuels |
| GET | /api/stats/hourly |
Fréquence horaire |
| GET | /api/imports |
Historique des imports |
| Méthode | Endpoint | Description |
|---|---|---|
| GET/PUT | /api/config/tariff |
Tarifs actifs |
| GET/POST | /api/config/tariff/periods |
Historique des périodes tarifaires |
| GET/PUT | /api/config/tariff/rule |
Règles HC/HP configurables |
| POST | /api/config/tariff/recalculate |
Recalcul HC/HP + coûts sur tout l'historique |
| GET/PUT | /api/config/alert |
Configuration des alertes webhook |
| Méthode | Endpoint | Description |
|---|---|---|
| GET | /api/chargers |
Liste des bornes configurées |
| POST | /api/chargers |
Créer une borne |
| PUT | /api/chargers/{id} |
Modifier une borne |
| DELETE | /api/chargers/{id} |
Supprimer une borne |
| POST | /api/chargers/test |
Tester la connexion UDP (pré-enregistrement) |
| POST | /api/chargers/{id}/test |
Tester la connexion UDP (borne enregistrée) |
| GET | /api/chargers/{id}/status |
Statut temps réel via UDP |
| POST | /api/chargers/{id}/image |
Upload photo |
| GET | /api/chargers/live |
Cache statut de toutes les bornes (sans UDP) |
| GET | /api/chargers/active-charge |
Sessions actives : énergie + durée en temps réel |
| Méthode | Endpoint | Description |
|---|---|---|
| GET | /api/health |
Health check |
| GET | /api/version |
Version de l'application |
- Docker + Docker Compose
- Caddy existant connecté au réseau Docker
home.lan - Réseau en
hostpour le backend (nécessaire pour les broadcasts UDP LAN)
mkdir -p /srv/docker_data/evstats
cd /srv/docker_data
git clone git@gogs.home.lan:fpicard/evstats.git
cd evstats
docker compose up -d --buildservices:
evstats-api:
volumes:
- /srv/docker_data/evstats:/app/data
# network_mode: host → pas de networks explicite ici
evstats-frontend:
ports: []
networks:
- home.lan
networks:
home.lan:
external: trueevstats.home.lan {
reverse_proxy evstats-frontend:80
}cd /srv/docker_data/evstats
git pull
docker compose up -d --build
docker compose restart evstats-api # nécessaire pour recharger VERSION (bind mount :ro)0 3 * * * /srv/docker_data/evstats/scripts/backup.shLe fichier frontend/public/vehicles_db.json contient 27 modèles populaires du marché français avec leurs données WLTP (batterie, conso, été/hiver). Il est généré une fois et embarqué dans l'image Docker au build.
Pour le régénérer (nouveaux modèles, données mises à jour) :
# Depuis la machine de dev (pas la VM)
cd evstats/
pip install requests beautifulsoup4 # si pas déjà fait
python scripts/scrape_ev_database.py --whitelist-onlyPuis rebuilder et redéployer :
git add frontend/public/vehicles_db.json
git commit -m "chore: mise à jour vehicles_db.json"
git push
# Sur la VM :
git pull && docker compose up -d --buildLa whitelist des 27 modèles est définie dans
scripts/scrape_ev_database.py(variableWHITELIST). Source des données : ev-database.org — consommations réelles (Mild = été, Cold = hiver).
- Import XLSX avec déduplication (
record_id) - Calcul HC/HP avec découpage minute-à-minute
- Dashboard : KPIs, graphiques, camembert, classement des mois
- Tarifs EDF configurables + recalcul global
- Export CSV, script backup SQLite
- Historique des tarifs EDF avec périodes de validité
- Graphique fréquence horaire
- Alertes consommation mensuelle (webhook ntfy/Slack/Discord)
- Rapports PDF mensuels
- Footer version dynamique (fichier
VERSIONunique)
- Correctifs responsive mobile
- Récapitulatif annuel + filtre par année
- Page Véhicule : specs, photo, KPIs/km
- Publication open source (GitHub, MIT)
- Règles HC/HP entièrement configurables depuis l'interface
- Jours entièrement HC paramétrables, plages horaires multiples
- Bouton ⓘ tooltip sur les graphiques
- Véhicule actif (modèle imprimante par défaut), KPIs/km sur véhicule actif uniquement
- Page Chargeurs : ajout/modification/suppression, photo, FQDN
- Intégration UDP directe (protocole EVSEMaster port 28376)
- Test de connexion avec découverte automatique du numéro de série
- Verrou asyncio : une seule session UDP à la fois
- Polling automatique en arrière-plan (30 s veille, continu en charge)
- Détection automatique début/fin de charge (0.5 A / 0.1 A × 3)
- Enregistrement automatique des sessions UDP (HC/HP + coût calculés)
- Cache statut en mémoire (page Chargeurs auto-rafraîchie)
- Bannière "Charge en cours" sur le Dashboard (animation pulse)
- Chip UDP sur les sessions auto-enregistrées dans le tableau Sessions
- Énergie session et durée en temps réel dans la bannière (rafraîchi toutes les 5 s)
- Compteur énergie hardware borne (payload [9:13], uint32 × 10 Wh) — précis même si cycles manqués
- Persistance restart-proof :
active_charges.jsondans le volume Docker - Timezone Europe/Paris :
TZ=Europe/Parisdans docker-compose +datetime.now() - Script migration one-shot UTC → Europe/Paris pour les sessions existantes
- Déduplication cross-source à l'import XLSX : sessions UDP existantes protégées contre le réimport (overlap temporel)
- Données constructeur WLTP sur le profil véhicule : 6 valeurs été/hiver × mixte/autoroute/ville (kWh/100km)
- Autonomies théoriques calculées par cas d'usage
- Indicateur d'écart conso réelle vs WLTP été mixte (vert ≤0%, orange ≤+20%, rouge >+20%)
- Autocomplete véhicule à la saisie : 27 modèles marché FR avec pré-remplissage automatique (batterie + conso + 6 valeurs WLTP)
- Script
scrape_ev_database.pypour régénération de la base véhicules
- Fix overflow compteur énergie UDP 16 bits (% 65536) — énergie ne se fige plus en cours de session
- Fix compteur UDP — sanity check physique (reset compteur vs overflow 16 bits) : delta corrigé validé par durée × puissance max
- Édition et suppression de sessions via l'UI (énergie, dates, recalcul tarif automatique)
- Compteur énergie delta incrémental poll-à-poll (counter_N − counter_N-1) — survit aux cycles manqués sans accumulation d'erreur
- Fix compteur énergie UDP — champ correct bytes [9:13] uint32 × 10 Wh (le champ [15:17] uint16 était erroné)
- Suppression de la gestion overflow 16 bits (inutile avec un compteur 32 bits ~42 TWh)
- Sanity-check anti-saut : delta compteur aberrant (ex. changement d'unité après restart) ignoré et compteur réinitialisé
- Édition sessions — bouton modifier limité aux sessions UDP (les sessions XLSX sont fiables par nature)
- Confirmation début de charge : 2 lectures consécutives > 0.5A requises (symétrique avec la fin)
- Élimine les micro-sessions fantômes sur les cycles de rééquilibrage post-charge
- Seuil minimum d'énergie relevé à 0.1 kWh — filtre les micro-sessions DC-DC (charge batterie 12V accessoire)
- Support du tarif Tempo EDF (Bleu/Blanc/Rouge) — nécessite intégration API RTE
- Comparaison coût électrique vs thermique (€/100km)
- Import automatique via partage de fichier (Nextcloud, etc.)
- Une seule session UDP à la fois : l'app EVSEMaster mobile doit être fermée pendant les requêtes du poller (contrainte protocolaire). Les deux ne peuvent pas se connecter simultanément à la borne.
- Broadcasts intermittents : si EVSEMaster se reconnecte en arrière-plan, le poller manque des cycles. L'énergie est néanmoins correcte grâce au compteur hardware absolu (delta entre deux polls réussis).
- Tarif Tempo EDF non supporté : nécessite l'API RTE en temps réel pour le calendrier des couleurs de jours.
Florian PICARD — GitHub