Modèle de données applicatif

Cette page explique comment le MCD de référence décrit dans Référence du MCD et du schéma PostgreSQL est implémenté dans le modèle Django. Le MCD donne la vision fonctionnelle de la base ; l’implémentation Django l’adapte aux besoins concrets de l’application, aux migrations ORM, à l’authentification Django et aux tests.

Positionnement du MCD

Le MCD fourni est une bonne base de conception. Il décrit les grands domaines du projet :

  • utilisateurs, profils, rôles et moyens d’authentification ;

  • consentements et journal de sécurité ;

  • stations, bornes, emplacements, devices ESP32 et capteurs ;

  • réservations, sessions de charge et télémétrie ;

  • alertes, techniciens et maintenances ;

  • pistes RGPD et performance avec vues, anonymisation, TimescaleDB et index.

Il ne doit cependant pas être appliqué tel quel comme script SQL dans le projet. La source opérationnelle de la base est le modèle Django situé dans backend/api/models.py et ses migrations dans backend/api/migrations/. La référence MCD/PostgreSQL reste placée avant cette page dans le sommaire afin de présenter d’abord la cible fonctionnelle, puis les choix d’implémentation.

Cette décision évite de maintenir deux schémas concurrents : un schéma SQL manuel et un schéma généré par Django. Toute évolution structurelle doit passer par une migration Django afin de rester versionnée, testable et reproductible dans Docker, en CI et sur les postes de développement.

Choix d’implémentation

Valeurs métier fermées

Les cycles de vie structurants sont déclarés dans backend/api/models.py avec des classes TextChoices. Cette couche ferme les valeurs acceptées par les serializers Django REST Framework et par les formulaires d’administration, tout en conservant un stockage simple en VARCHAR.

Les migrations ajoutent aussi des contraintes CHECK sur ces champs. La base refuse donc une valeur hors catalogue même si elle arrive par un script ou une écriture directe. Une migration de normalisation précède ces contraintes pour ramener les anciennes valeurs libres vers les valeurs métier retenues.

Les champs concernés sont notamment :

  • Utilisateur.statut : actif, suspendu, archive ;

  • Station.statut : active, maintenance, inactive ;

  • Emplacement.statut : disponible, reserve, occupe, maintenance ;

  • Emplacement.lock_state : verrouille, deverrouille, bloque ;

  • Reservation.auth_choice : rfid, qr, code ;

  • Reservation.statut : en_attente, confirmee, active, annulee, terminee ;

  • Session.charge_state : reserved, access_validated, waiting_plug, charging, stopping, stopped, error ;

  • Device.status : provisioned, online, idle, offline, charging, error ;

  • Alerte.status : nouvelle, active, prise_en_charge, resolue, closed, fermee ;

  • Maintenance.status : planifiee, en_cours, realisee, annulee.

Les types de capteurs, types d’alertes et types d’intervention restent plus ouverts. Ils dépendent du matériel, du firmware et de l’exploitation terrain ; les figer trop tôt rendrait l’intégration ESP32 moins souple.

Les champs Device.last_command_* conservent le dernier ACK MQTT reçu pour un ESP32 : identifiant de commande, action, statut, message et horodatage. Ils servent à l’exploitation et à la supervision, sans remplacer Device.status ni Device.last_seen.

Le modèle DeviceCommandLog conserve l’historique récent des ACK de commandes ESP32. Il rattache l’action au device, à la session de charge lorsque le firmware renvoie session_id, au command_id et au payload brut reçu. Ce journal sert à auditer les ordres START, STOP, LOCK et UNLOCK sans dépendre uniquement du dernier état écrasé sur Device.

UUID

Le MCD prévoit CREATE EXTENSION "uuid-ossp" et des valeurs par défaut uuid_generate_v4(). Dans le projet, les UUID sont générés côté Django avec uuid.uuid4 sur les champs UUIDField.

Ce choix garde l’application compatible avec l’ORM et les tests SQLite. Il rend l’extension uuid-ossp non indispensable pour la version actuelle.

Utilisateur Django

Le MCD décrit une table utilisateur autonome avec email, password_hash, statut, created_at, last_login et failed_attempts.

Le projet utilise un modèle Utilisateur personnalisé qui hérite de AbstractBaseUser et PermissionsMixin. Cela ajoute les conventions Django nécessaires : mot de passe géré par Django, permissions, groupes, is_staff, is_active et date_joined.

Le pseudo est l’identifiant principal de connexion, et l’email reste unique. Cette structure permet d’utiliser l’administration Django, les permissions DRF, les jetons JWT et les tests d’authentification sans recréer toute la mécanique de sécurité.

Profil utilisateur

Le modèle ProfilUtilisateur reprend bien l’idée du MCD : les données personnelles comme le nom et le téléphone sont séparées du compte d’authentification.

La relation est un OneToOneField vers Utilisateur avec suppression en cascade. Si le compte est supprimé, le profil associé disparaît aussi.

Rôles

Les tables role et utilisateur_role existent dans Django. Elles permettent de conserver une représentation métier simple des rôles, en plus des permissions Django natives.

La table de liaison garde la date d’affectation avec assigned_at et impose l’unicité du couple utilisateur-rôle.

Authentification multi-moyens

Le MCD prévoit auth_type et auth_method. Le projet implémente ces concepts avec TypeAuth et Authentification.

Authentification stocke le secret sous la colonne SQL secret_hash via le champ Django hash_valeur. Le modèle ajoute aussi :

  • display_value pour afficher une valeur masquee ou lisible quand c’est acceptable ;

  • metadata pour stocker les informations nécessaires aux QR codes, codes temporaires et associations RFID ;

  • expiration et is_active pour gérer révocation et durée de validité.

Cette structure est plus adaptée au projet que le MCD minimal, car elle couvre les QR codes, les codes temporaires et les badges RFID persistants.

RGPD et journalisation

Les tables consentement et journal_securite sont implémentées.

JournalSecurite utilise ON DELETE SET NULL plutôt qu’une cascade stricte sur l’utilisateur. Ce choix permet de conserver une trace d’événements sensibles même si le compte utilisateur est supprimé ou anonymisé.

La table consentement sert à tracer les choix explicites de l’utilisateur lorsqu’un traitement repose réellement sur le consentement. Elle est pertinente pour les traitements optionnels : acceptation des CGU, prise de connaissance de la politique de confidentialité, notifications non indispensables ou mesure d’audience.

Elle ne doit pas être utilisée comme justification générique pour tous les traitements. Les données nécessaires à la création du compte, à la réservation, à la sécurité ou à la maintenance du service relèvent plutôt du contrat, de l’intérêt légitime ou d’une obligation légale. Cette distinction renforce la crédibilité RGPD du projet : le consentement reste libre, spécifique et révocable.

La table est utilisée dès l’inscription. Le formulaire demande explicitement l’acceptation des CGU, la prise de connaissance de la politique de confidentialité et le choix relatif aux notifications optionnelles. L’API crée ensuite trois lignes historisées :

  • cgu/accepte ;

  • politique_confidentialite/accepte ;

  • notifications/accepte ou notifications/refuse selon le choix de l’utilisateur.

Ce choix donne une preuve datée des décisions prises à la création du compte, tout en séparant clairement les consentements optionnels des traitements nécessaires au service. Le profil utilisateur expose ensuite les consentements courants et permet de modifier les notifications optionnelles. Lorsqu’un choix change, l’API ajoute une nouvelle ligne datée dans consentement au lieu d’écraser l’historique existant.

Les mécanismes PostgreSQL avancés du MCD, comme l’extension anon, les SECURITY LABEL et les vues SQL d’export, ne sont pas encore actives. Le projet gère pour l’instant l’export et la suppression côté API Django.

Stations, bornes et emplacements

Station, Borne et Emplacement décrivent l’infrastructure physique.

Le modèle Django ajoute latitude et longitude sur Station afin de supporter la carte de réservation. Borne représente une armoire ou un kiosque physique rattaché à une station. Emplacement garde le numéro, l’état métier et l’état de cadenas.

La hiérarchie métier est donc : station, borne, emplacement. Une contrainte d’unicité empêche deux emplacements d’une même borne de porter le même numéro. Le champ station reste aussi présent sur Emplacement pour conserver des requêtes simples et une compatibilité avec les flux existants ; il est aligné automatiquement avec la station de la borne.

Devices ESP32

Device correspond au MCD et représente le contrôleur embarqué d’un emplacement. La relation avec Emplacement est un OneToOneField : un emplacement ne doit avoir qu’un device principal dans la version actuelle.

Le modèle distingue l’identité logique et l’identité physique :

  • slot_id : identifiant MQTT de l’emplacement, par exemple slot1 ;

  • hardware_mac : adresse MAC Wi-Fi réelle de la carte ESP32 ;

  • ip_address : adresse IP courante, informative car elle peut changer ;

  • firmware_version : version publiée par le firmware ;

  • mac_address : ancien champ conservé temporairement pour compatibilité avec les données et tests déjà existants.

Le backend utilise slot_id pour router et afficher l’emplacement. Les payloads status, ack et telemetry peuvent mettre à jour automatiquement firmware_version, hardware_mac et ip_address.

Capteurs et données capteurs

Capteur et CapteurData reprennent le principe du MCD : un device porte plusieurs capteurs, et chaque mesure horodatée est stockée dans capteur_data.

Les valeurs booléennes issues de l’ESP32, comme door et cable, sont converties en 1.0 ou 0.0 pour rester compatibles avec valeur de type flottant. L’API ajoute ensuite une valeur d’affichage lisible dans la supervision administrateur.

Le MCD cible propose des hypertables TimescaleDB pour capteur_data. Elles ne sont pas encore activées : la table reste une table Django standard afin de garder la solution simple et portable.

Réservations

Reservation relie un utilisateur, un emplacement, un moyen d’authentification et une fenêtre de temps.

Le projet ajoute reference_code et auth_choice afin de gérer le parcours terminal : code de réservation, QR code, code SMS temporaire ou RFID.

La relation vers Utilisateur est en SET_NULL et non en cascade. Ce choix évite d’effacer tout l’historique technique d’une réservation si le compte est supprimé. La donnée personnelle peut disparaître, tandis que l’événement métier reste exploitable.

Sessions de charge

Le MCD prévoit une table session avec id_user, date_debut, date_fin, consommation_kwh et etat_batterie.

Le projet implémente ce concept dans la table SQL charging_session. Le modèle Django s’appelle Session mais sa table n’est pas nommée session, ce qui évite aussi la confusion avec la notion de session applicative.

La session réelle est rattachée à une réservation et à un emplacement. Elle stocke start_time et end_time. L’utilisateur se retrouve via session.reservation.utilisateur.

charge_state formalise le cycle technique de la charge côté backend : reserved lorsque la session existe sans commande validée, access_validated après authentification, waiting_plug après ACK START, charging dès que l’ESP32 confirme la charge ou publie de la télémétrie active, stopping après ACK STOP ou retour idle avant clôture backend, stopped lorsque end_time est renseigné, et error si l’ESP32 remonte un état de défaut. Ce champ complète end_time : il évite les doubles START et rend visibles les sessions arrêtées physiquement mais pas encore clôturées côté backend.

La consommation instantanee ou cumulee n’est pas stockee directement dans charging_session. Elle est portee par session_telemetry avec energie_kwh. Cette séparation garde la session comme un objet métier et la télémétrie comme une série temporelle.

Télémétrie de session

SessionTelemetry correspond au MCD : il stocke courant, tension, energie et timestamp pour une session active.

La table est indexée par session et timestamp. Elle pourra devenir une hypertable TimescaleDB si la fréquence de mesures augmente fortement.

Alertes

Alerte correspond au MCD mais adapte les suppressions : les liens vers CapteurData et Session sont en SET_NULL. Cela permet de garder une alerte historique même si la mesure ou la session associée n’est plus disponible.

Le champ timestamp est stocké dans la colonne SQL date pour rester proche du MCD tout en gardant un nom Django plus explicite.

Les champs first_seen et last_seen encadrent la durée de l’anomalie. Les champs notified_at et notification_count permettent de tracer les emails envoyés et d’appliquer un délai de relance sans dupliquer les alertes.

Le workflow technicien est porté directement par l’alerte. assigned_technician identifie l’intervenant responsable, acknowledged_by et acknowledged_at tracent la prise en charge, puis resolved_by, resolved_at et resolution_note documentent la clôture opérationnelle. Les alertes restent filtrées par station via TechnicienStation afin de respecter le principe du moindre privilège.

Le cycle actuel est géré côté application :

  • nouvelle à la création ;

  • active si la même anomalie continue ;

  • prise_en_charge lorsqu’un technicien accepte l’incident ;

  • resolue quand la télémétrie revient à la normale.

Maintenance

Technicien et Maintenance reprennent le MCD. Le projet ajoute la possibilité de lier un technicien à un compte Utilisateur. Cela permet à un technicien de se connecter et de consulter ses interventions.

TechnicienStation applique le principe du moindre privilège côté métier : un administrateur affecte explicitement un technicien à une ou plusieurs stations. La supervision technicien ne lit ensuite que les devices, alertes, mesures et commandes des stations actives dans ce périmètre. Grafana reste un outil de supervision technique global ou administrateur ; l’accès terrain quotidien des techniciens passe par l’application, où le filtrage est porté par l’API Django.

Performance et TimescaleDB

Le conteneur db utilise l’image timescale/timescaledb:latest-pg15. Une migration active l’extension timescaledb quand elle est disponible sur le serveur PostgreSQL. Sur SQLite ou sur un PostgreSQL sans extension installée, la migration reste sans effet pour préserver la portabilité du projet.

Dans l’état actuel, le projet utilise TimescaleDB comme un PostgreSQL compatible. Les tables capteur_data et session_telemetry ne sont pas encore des hypertables. Ce choix est volontaire : elles possèdent aujourd’hui une clé primaire UUID simple, alors qu’une hypertable TimescaleDB impose que les contraintes uniques incluent la colonne temporelle de partitionnement. Le passage en hypertables doit donc être traité comme une refonte de schéma ciblée, pas comme une simple commande SQL ajoutée en fin de migration.

Les index déjà présents couvrent les requêtes les plus importantes :

  • emplacements par borne, station et statut opérationnel ;

  • réservations par utilisateur, statut, emplacement et dates ;

  • sessions par emplacement et dates ;

  • devices par dernière activité ;

  • télémétrie par session et timestamp ;

  • données capteurs par capteur et timestamp ;

  • alertes par statut, session et timestamp ;

  • alertes par technicien assigné et statut ;

  • affectations technicien-station par technicien, station et statut actif ;

  • maintenances par technicien, station, statut et date planifiée.

Une migration complémentaire ajoute aussi des index orientés exploitation :

  • capteur(type, id_device) pour retrouver rapidement les séries par type de capteur et par device ;

  • capteur_data(timestamp DESC) pour les dernières mesures et les fenêtres temporelles Grafana ;

  • session_telemetry(timestamp DESC) pour les analyses temporelles de session ;

  • alerte(date DESC) pour les alertes récentes affichées en supervision.

Une seconde migration complète cette logique avec des index métier plus proches des écrans d’exploitation et des filtres API :

  • emplacement(statut_cadenas, etat) pour identifier rapidement les points de charge selon l’état du cadenas et l’état opérationnel ;

  • reservation(date_debut, date_fin) pour les recherches de disponibilité sur une fenêtre de temps ;

  • charging_session(start_time, end_time) pour les sessions ouvertes, fermées ou analysées par période ;

  • charging_session(charge_state, end_time) pour identifier les sessions en charge, en arrêt ou en erreur ;

  • device(status, last_seen) pour détecter les équipements indisponibles ou silencieux ;

  • alerte(niveau_severite, date) pour prioriser les incidents ;

  • alerte(type, date) pour suivre les familles d’anomalies dans le temps ;

  • alerte(assigned_technician_id, status) pour afficher rapidement les incidents ouverts par technicien.

Ces index ne changent pas le modèle fonctionnel. Ils rendent les parcours critiques plus prévisibles lorsque le volume augmente : supervision temps réel, historique d’exploitation, diagnostic terrain et tableaux de bord.

Les optimisations du MCD cible, comme create_hypertable, compression, retention et vues matérialisées, restent pertinentes pour une phase production ou une simulation à volume élevé. Elles nécessitent toutefois de revoir la clé primaire des tables de télémétrie ou d’introduire des tables spécialisées pour les séries temporelles.

Sécurité base de données

Le MCD propose Row Level Security sur session et reservation.

Dans le projet actuel, l’isolation est portée par l’API Django : les endpoints utilisateur filtrent toujours avec request.user et les endpoints administrateur exigent les droits staff.

Activer la RLS dans PostgreSQL serait possible, mais il faut alors garantir que app.current_user_id est défini et nettoyé correctement pour chaque requête. Comme Django réutilise ses connexions, une mauvaise gestion de ce contexte peut créer des erreurs difficiles à diagnostiquer. La RLS est donc considérée comme une évolution de durcissement, pas comme un prérequis de la version actuelle.

RGPD cible

Le MCD propose plusieurs mécanismes SQL :

  • vue session_anonyme ;

  • vue export_user_data ;

  • fonction delete_user_rgpd ;

  • extension anon et masquage dynamique ;

  • hachage de l’identifiant utilisateur dans les exports statistiques.

Le projet implémente déjà une partie de ce besoin côté API :

  • profil séparé du compte ;

  • export de compte via endpoint authentifié ;

  • suppression depuis le profil ;

  • journalisation des actions sensibles ;

  • conservation possible d’événements techniques sans lien personnel direct.

Les vues SQL et l’anonymisation PostgreSQL sont traitées progressivement. La vue session_anonyme est toujours disponible sur PostgreSQL. La migration 0027_optional_postgresql_anonymizer active aussi l’extension anon et ses SECURITY LABEL quand le serveur PostgreSQL fournit PostgreSQL Anonymizer. Cette migration est volontairement optionnelle : elle ne casse pas SQLite, ni une image TimescaleDB qui ne contient pas encore l’extension système.

Vue session_anonyme

La première vue SQL ajoutée côté PostgreSQL est session_anonyme. Elle expose un jeu de données dédié aux statistiques de sessions sans publier directement l’identifiant utilisateur.

La vue s’appuie sur les tables reelles de l’application :

  • charging_session pour les dates de début et de fin ;

  • reservation pour retrouver l’utilisateur lié à la session ;

  • session_telemetry pour calculer la consommation observée.

Le champ user_hash est produit avec md5(id_user::text) lorsque la session est encore rattachée à un utilisateur. Si le lien utilisateur a disparu, la vue utilise l’identifiant de session comme valeur de repli afin de conserver une ligne statistique sans recréer de donnée personnelle.

Cette vue ne remplace pas les contrôles d’accès applicatifs. Elle sert de base à des rapports d’usage anonymisés ou pseudonymisés, par exemple pour analyser le nombre de sessions, les plages horaires et les consommations sans exposer email, pseudo, nom ou téléphone.

La migration est volontairement limitee a PostgreSQL. Les tests unitaires du projet utilisent SQLite ; la migration devient donc un no-op sur SQLite afin de garder la suite rapide et portable.

Extension PostgreSQL Anonymizer

Quand l’extension anon est disponible dans PostgreSQL, la migration 0027_optional_postgresql_anonymizer pose des règles de masquage sur les colonnes personnelles suivantes :

  • utilisateur.pseudo et utilisateur.email ;

  • profil_utilisateur.nom et profil_utilisateur.telephone ;

  • journal_securite.ip ;

  • technicien.nom, technicien.email et technicien.telephone.

L’objectif n’est pas de masquer les données dans l’application de production, mais de permettre des exports, jeux de tests ou accès SQL restreints sans exposer directement les données personnelles. L’installation de l’extension reste une décision d’infrastructure : l’image TimescaleDB standard du projet ne garantit pas sa présence.

Synthèse

Le MCD ne doit pas être appliqué tel quel dans le projet. Il doit rester une référence de conception et une cible d’évolution.

L’implémentation Django est le modèle opérationnel actuel parce qu’elle :

  • respecte les besoins fonctionnels réels de l’application ;

  • intègre l’authentification et les permissions Django ;

  • garde les migrations versionnées ;

  • reste testable avec pytest et SQLite ;

  • évite un double schéma SQL/ORM ;

  • prépare les évolutions PostgreSQL et TimescaleDB sans les imposer trop tôt.

La trajectoire reste progressive :

  1. conserver le modèle Django comme source de vérité applicative ;

  2. ajouter les optimisations SQL lorsqu’elles répondent à un usage mesuré ou clairement identifié ;

  3. enrichir les vues SQL pour les exports ou statistiques ;

  4. activer les hypertables TimescaleDB quand le volume de télémétrie augmente ;

  5. activer l’image PostgreSQL/TimescaleDB incluant anon lorsque des dumps anonymisés ou des rôles SQL masqués deviennent nécessaires ;

  6. envisager RLS, compression et rétention native pour une version durcie.