Aller au contenu principal

Architecture Mode Serveur

Vue d'ensemble​

Le mode serveur adopte une architecture complète avec authentification, cache distribué et accès direct aux APIs Enedis/RTE.

┌───────────────────────────────────────────────────────────────────────────────┐
│ MODE SERVEUR - ARCHITECTURE │
├───────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ │
│ │ FRONTEND │ │
│ │ React / Vite │ │
│ │ :8000 (prod) │ │
│ └───────────┬─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ BACKEND │ │
│ │ FastAPI │ │
│ │ :8081 │ │
│ └───────────┬─────────────┘ │
│ │ │
│ ┌─────────────────────┼─────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Enedis Adapter │ │ PostgreSQL │ │ Valkey Cache │ │
│ │ (DataHub API) │ │ (utilisateurs, │ │ (données Enedis │ │
│ │ │ │ tokens, config)│ │ chiffrées) │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ ▼ │ │ │
│ ┌─────────────────┐ │ ┌──────────┴──────────┐ │
│ │ Enedis DataHub │ │ │ │ │
│ │ (API distante) │ │ ▼ ▼ │
│ └─────────────────┘ │ ┌───────────┐ ┌───────────┐ │
│ │ │ Fernet │ │ SHA256 │ │
│ ┌─────────────────┐ │ │ Encryption│ │ Key Deriv │ │
│ │ RTE APIs │ │ └───────────┘ └───────────┘ │
│ │ Tempo / EcoWatt │ │ │
│ └─────────────────┘ │ │
│ │ │
│ Background Tasks (APScheduler) │ │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │
│ • Refresh tokens Enedis │ │
│ • Update Tempo calendar │ │
│ • Update EcoWatt signals │ │
│ • Cleanup expired cache │ │
│ │ │
└──────────────────────────────────┴────────────────────────────────────────────┘

Composants​

Frontend (React/Vite)​

Pages spécifiques au mode serveur :

  • Page d'accueil (Landing) : Présentation et call-to-action inscription
  • Inscription/Connexion : Authentification JWT
  • Routes admin : Gestion complète du système
  • Paramètres utilisateur : Gestion compte et tokens
  • Simulateur : Comparaison d'offres tarifaires
// Routes mode serveur
const routes = [
{ path: '/', component: Landing },
{ path: '/signup', component: Signup },
{ path: '/login', component: Login },
{ path: '/dashboard', component: Dashboard },
{ path: '/consumption', component: ConsumptionKwh },
{ path: '/consumption/euro', component: ConsumptionEuro },
{ path: '/production', component: Production },
{ path: '/bilan', component: Bilan },
{ path: '/contribute', component: Contribute },
{ path: '/tempo', component: Tempo },
{ path: '/ecowatt', component: Ecowatt },
{ path: '/france', component: France },
{ path: '/simulator', component: Simulator },
{ path: '/faq', component: FAQ },
{ path: '/settings', component: Settings },
// Routes admin
{ path: '/admin', component: AdminDashboard },
{ path: '/admin/users', component: AdminUsers },
{ path: '/admin/roles', component: AdminRoles },
{ path: '/admin/pdl', component: AdminPDL },
{ path: '/admin/offers', component: AdminOffers },
{ path: '/admin/tempo', component: AdminTempo },
{ path: '/admin/ecowatt', component: AdminEcowatt },
{ path: '/admin/contributions', component: AdminContributions },
{ path: '/admin/logs', component: AdminLogs },
];

Backend (FastAPI)​

Structure des routers​

# Mode serveur - tous les routers
from routers import (
accounts, # Inscription, connexion, gestion compte
admin, # Administration système
pdl, # CRUD PDL complet
enedis, # Accès direct API Enedis
consumption, # Données de consommation
production, # Données de production
tempo, # Calendrier Tempo (API RTE)
ecowatt, # Signaux EcoWatt (API RTE)
consumption_france, # Consommation nationale
generation_forecast, # Prévisions production
contribute, # Contributions utilisateurs
simulator, # Simulateur de tarifs
energy_offers, # Gestion offres tarifaires
)

Enedis Adapter​

Service pour communiquer avec l'API Enedis DataHub :

# apps/api/src/adapters/enedis.py

class EnedisAdapter:
"""Client pour l'API Enedis DataHub"""

BASE_URL = "https://ext.prod.api.enedis.fr"

async def get_consumption_daily(
self, pdl: str, start: date, end: date, token: str
) -> list[ConsumptionReading]:
"""Récupère la consommation journalière"""

async def get_consumption_load_curve(
self, pdl: str, start: date, end: date, token: str
) -> list[LoadCurveReading]:
"""Récupère la courbe de charge (30 min)"""

async def get_production_daily(
self, pdl: str, start: date, end: date, token: str
) -> list[ProductionReading]:
"""Récupère la production journalière"""

async def get_contract(self, pdl: str, token: str) -> Contract:
"""Récupère les informations du contrat"""

async def get_addresses(self, pdl: str, token: str) -> Address:
"""Récupère les adresses du PDL"""

Rate Limiting​

Protection contre les abus et respect des quotas Enedis :

# Adapter-level rate limiting
RATE_LIMIT = 5 # requêtes par seconde
DAILY_QUOTA = 10000 # requêtes par heure

# User-level quotas
UNCACHED_DAILY_LIMIT = 50 # requêtes non-cachées par jour
CACHED_DAILY_LIMIT = 1000 # requêtes cachées par jour

Base de données PostgreSQL​

Schéma mode serveur​

-- Utilisateurs
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
hashed_password VARCHAR(255) NOT NULL,
client_id VARCHAR(255) UNIQUE NOT NULL,
client_secret VARCHAR(255) NOT NULL,
is_active BOOLEAN DEFAULT true,
is_admin BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);

-- Points de Livraison (PDL)
CREATE TABLE pdls (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
usage_point_id VARCHAR(14) UNIQUE NOT NULL,
alias VARCHAR(255),
has_production BOOLEAN DEFAULT false,
subscribed_power INTEGER,
created_at TIMESTAMP DEFAULT NOW()
);

-- Tokens OAuth2 Enedis
CREATE TABLE tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
pdl_id UUID REFERENCES pdls(id) ON DELETE CASCADE,
access_token TEXT NOT NULL,
refresh_token TEXT NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);

-- Rôles utilisateurs
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(50) UNIQUE NOT NULL,
permissions JSONB NOT NULL DEFAULT '[]'
);

-- Association user-rôles
CREATE TABLE user_roles (
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, role_id)
);

-- Fournisseurs d'énergie
CREATE TABLE energy_providers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
website VARCHAR(255),
logo_url VARCHAR(255),
scraper_urls JSONB DEFAULT '{}',
is_active BOOLEAN DEFAULT true
);

-- Offres tarifaires
CREATE TABLE energy_offers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
provider_id UUID REFERENCES energy_providers(id),
name VARCHAR(255) NOT NULL,
pricing_type VARCHAR(50) NOT NULL, -- BASE, HCHP, TEMPO
prices JSONB NOT NULL,
subscription_price DECIMAL(10,4),
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW()
);

-- Contributions utilisateurs
CREATE TABLE contributions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
type VARCHAR(50) NOT NULL,
data JSONB NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT NOW()
);

-- Logs d'activité
CREATE TABLE activity_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
action VARCHAR(100) NOT NULL,
details JSONB,
ip_address INET,
created_at TIMESTAMP DEFAULT NOW()
);

-- Index
CREATE INDEX idx_pdls_user_id ON pdls(user_id);
CREATE INDEX idx_tokens_user_id ON tokens(user_id);
CREATE INDEX idx_tokens_expires_at ON tokens(expires_at);
CREATE INDEX idx_contributions_status ON contributions(status);
CREATE INDEX idx_activity_logs_user_id ON activity_logs(user_id);
CREATE INDEX idx_activity_logs_created_at ON activity_logs(created_at);

Cache Valkey​

Stratégie de cache​

Le mode serveur utilise un cache distribué Valkey avec chiffrement des données utilisateur :

┌─────────────────────────────────────────────────────────────────────────────┐
│ STRATÉGIE DE CACHE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Clé de cache : consumption:daily:{pdl}:{date} │
│ TTL : 24 heures │
│ Chiffrement : Fernet (AES-128-CBC + HMAC-SHA256) │
│ │
│ Dérivation de clé : │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ user.client_secret ──► SHA256 ──► Base64 ──► Fernet Key │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Flux de lecture : │
│ 1. Générer la clé de cache │
│ 2. Chercher dans Valkey │
│ 3. Si trouvé : déchiffrer avec client_secret │
│ 4. Si non trouvé : appeler Enedis, chiffrer, stocker │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

Données publiques vs privées​

# Données utilisateur : TOUJOURS chiffrer
await cache_service.get(key, current_user.client_secret)
await cache_service.set(key, data, current_user.client_secret)

# Données publiques (Tempo, EcoWatt) : sans chiffrement
await cache_service.get_raw(key)
await cache_service.set_raw(key, value)

Flux d'authentification​

OAuth2 Enedis​

┌─────────────────────────────────────────────────────────────────────────────┐
│ FLUX OAUTH2 ENEDIS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. INSCRIPTION │
│ ━━━━━━━━━━━━━ │
│ Utilisateur ──► Créer compte ──► Recevoir client_id/client_secret │
│ │
│ 2. CONSENTEMENT │
│ ━━━━━━━━━━━━━━ │
│ Utilisateur Backend Enedis │
│ │ │ │ │
│ │ Click consent │ │ │
│ │─────────────────>│ │ │
│ │ │ Redirect to │ │
│ │<─────────────────│ Enedis auth │ │
│ │ │ │ │
│ │ Login Enedis │ │ │
│ │─────────────────────────────────────>│ │
│ │ │ │ │
│ │ Authorize app │ │ │
│ │─────────────────────────────────────>│ │
│ │ │ │ │
│ │ Redirect with │ │ │
│ │<─────────────────────────────────────│ │
│ │ code │ │ │
│ │ │ │ │
│ │ Callback │ │ │
│ │─────────────────>│ Exchange code │ │
│ │ │─────────────────>│ │
│ │ │<─────────────────│ │
│ │ │ access_token │ │
│ │ │ refresh_token │ │
│ │ │ │ │
│ │ 200 OK │ Store tokens │ │
│ │<─────────────────│ in PostgreSQL │ │
│ │
│ 3. DÉTECTION PDL │
│ ━━━━━━━━━━━━━━━━ │
│ Après consentement, le backend récupère automatiquement │
│ tous les PDL autorisés pour cet utilisateur │
│ │
│ 4. REFRESH TOKEN │
│ ━━━━━━━━━━━━━━━━ │
│ Tâche planifiée : refresh automatique avant expiration │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

Scheduler (Tâches planifiées)​

Le mode serveur utilise APScheduler pour les tâches de fond :

# apps/api/src/main.py

def start_background_tasks():
"""Démarré uniquement en mode serveur"""

# Refresh des tokens Enedis (avant expiration)
scheduler.add_job(
refresh_expiring_tokens,
trigger=IntervalTrigger(hours=1),
)

# Mise à jour calendrier Tempo
# - Toutes les 15 min (6h-23h)
# - Uniquement si J+1 inconnu
scheduler.add_job(
rte_service.update_tempo_cache,
trigger=CronTrigger(minute="*/15", hour="6-23"),
)

# Mise à jour EcoWatt
# - 17h00 quotidien (publication RTE)
# - 12h15 vendredi (anticipation week-end)
# - Fallback horaire si J+3 incomplet
scheduler.add_job(
rte_service.update_ecowatt_cache,
trigger=CronTrigger(hour=17, minute=0),
)
scheduler.add_job(
rte_service.update_ecowatt_cache,
trigger=CronTrigger(day_of_week="fri", hour=12, minute=15),
)

# Calendrier EDF Zen Flex (services/edf_zen_flex.py)
# - Toutes les 10 min : J et J+1, puis rattrapage de l'historique par lots de 30 jours
asyncio.create_task(refresh_zen_flex_cache_task())

# Nettoyage cache expiré
scheduler.add_job(
cleanup_expired_cache,
trigger=IntervalTrigger(hours=6),
)

# Mise à jour offres tarifaires (scrapers)
scheduler.add_job(
scraper_service.update_all_offers,
trigger=CronTrigger(hour=2, minute=0), # 2h du matin
)

Variables d'environnement​

# Mode serveur activé
SERVER_MODE=true

# Base de données
DATABASE_URL=postgresql+asyncpg://user:pass@postgres:5432/myelectricaldata

# Cache Valkey
REDIS_URL=redis://valkey:6379/0

# Credentials Enedis (requis)
ENEDIS_CLIENT_ID=your-client-id
ENEDIS_CLIENT_SECRET=your-client-secret
ENEDIS_ENVIRONMENT=production # ou sandbox

# Credentials RTE (optionnel)
RTE_CLIENT_ID=your-rte-client-id
RTE_CLIENT_SECRET=your-rte-client-secret

# JWT
SECRET_KEY=your-jwt-secret-key
ACCESS_TOKEN_EXPIRE_DAYS=30

# Admin
ADMIN_EMAILS=admin@example.com,admin2@example.com

# URLs
FRONTEND_URL=https://myelectricaldata.fr
BACKEND_URL=https://api.myelectricaldata.fr

Différences clés avec le mode client​

AspectMode ServeurMode Client
Source donnéesEnedis API directAPI MyElectricalData
AuthentificationJWT + OAuth2 EnedisToken API pré-configuré
StockageCache Valkey (24h)PostgreSQL (indéfini)
UsersMulti-utilisateursMono-utilisateur
AdministrationComplète (UI admin)Aucune
Rate limiting5 req/s + quotasGéré par API distante
ExportsNonHome Assistant, MQTT, etc.
Page d'accueilLanding pageRedirection /dashboard
SimulateurOuiNon
FAQOuiNon