Tutoriel Pas à Pas

Partie 1 : Mise en place


Préparer la Raspberry

  • La Raspberry doit être physiquement à côté du compteur Linky, puisque qu’elle porte le module PiTinfo et que celui-ci doit être relié par câble aux bornes TIC du compteur.
  • Il faut donc préparer la Raspberry pour communiquer avec elle en ssh depuis son propre ordinateur.

Installer Raspbian sur la Rasbperry

  • On suit les instructions du tutoriel Raspberry
  • Sur son ordinateur on installe Raspberry Pi Imager
  • On flashe la carte SD
  • Une fois la carte prête, on l’insère dans la Raspberry éteinte
  • On branche un clavier, une souris, un écran au niveau de la Raspberry
  • On allume la Rasbperry
  • On procède à l’installation en introduisant les différents paramètes souhaités

Activer le ssh

  • C’est une étape fondamentale, puisque toute la mise en place des différents codes va se faire en ssh
  • Il convient que le mot de passe de la Raspberry soit consistant
  • Il est préférable de changer le port ssh externe au niveau du routeur, de manière à éviter les tentatives d’intrusion externe par le port par défaut
  • Il est utile de donner une IP locale fixe à sa Raspberry, en sachant que la Raspberry a deux IP : une pour le WiFi, une pour l’éthernet
  • Des conseils sont disponibles sur un tutoriel de mise en place de caméras de suveillance

Débrancher la Raspberry

  • Le ssh étant activé, on peut débrancher la Raspberry

Préparer le module PiTinfo

Où le connecter ?

  • Au niveau du compteur Linky, la connexion est différente selon que l’installation est en triphasé ou en monophasé
  • La prise est accessible en enlevant le capot ad hoc non plombé
  • En monophasé, la prise TIC est située en bas du compteur (le compteur a deux capots : le supérieur plombé, l’inférieur non plombé)
  • En triphasé, la prise TIC est située en haut du compteur (le compteur a trois capots : le supérieur est l’inférieur non plombés, le médian plombé)
  • Cette page internet permet de visualiser l’ouverture de son compteur Linky
  • Les bornes de connexion sont nommées : I1 et I2

Comment le connecter ?

  • Utiliser un cable à deux conducteurs, si possible blindé de manière à avoir un signal propre
  • Fixer la première extrêmité des conducteurs au niveau des bornes de la prise TIC
  • Fixer la deuxième extrêmité des conducteurs au niveau des bornes du module PiTinfo
  • Il n’y a pas de polarité et donc peu importe que tel conducteur soit lié à telle borne
  • Bien assurer les fixations

Positionner le module PiTinfo sur la Raspberry

  • Le module PiTinfo a un connecteur à 5 doubles entrées qui se fixe sur le GPIO de la Raspberry
  • Bien le positionner sur les 5 premières double broches du GPIO en s’assurant de couvrir les broches TXD et RXD
images des bornes gpio d'une raspberry
  • L’image de positionnement de Tindie permet de bien repérer celui-ci
images des bornes gpio d'une raspberry

Brancher la Raspberry

  • Une fois toutes les connexions réalisées, brancher la Raspberry
  • Cette installation étant faite, toutes les autres opérations se font depuis un ordinateur

Faire mettre le Linky en mode standard

  • Il existe deux modes de fonctionnement d’un compteur Linky : historique et standard
  • Pour obtenir les bonnes informations, il convient que le Linky soit en mode standard
  • Il est possible de savoir en quel mode fonctionne le compteur Linky en faisant défiler les informations au niveau de compteur et en regardant si il affiche Mode Historique ou Mode Standard
  • La demande de passage du mode historique au mode standard se fait en contactant son fournisseur d’électricité ou bien le gestionnaire du réseau, Enedis
  • Cette page internet donne des informations sur les modes historique et standard
Lire la suite

Partie 2 : Préparation des Logiciels


Vérifier le fonctionnement du module PiTinfo

Se connecter en ssh à la Raspberry

  • Sur son ordinateur, ouvrir un terminal de commande (la méthode est différente selon l’OS utilisé : consulter cette page)
  • Taper : $ ssh pi@<local IP Raspberry>
  • Admettons que l’IP locale de la Raspberry soit 192.168.1.120 , taper : ssh pi@192.168.1.120 (à condition, bien sûr, d’avoir laissé pi comme nom d’utilisateur de la Raspberry)
  • Entrer le mot de passe de la Raspberry
  • Si besoin, autoriser la connexion en acceptant la création d’une clé

Installer picocom

  • picocom est un émulateur de terminal qui va permettre de communiquer avec le PiTinfo
  • Installer picocom en tapant dans le terminal connecté à la Raspberry en ssh : $ sudo install picocom

Modifier le fichier /boot/firmware/config.txt

  • Pour assurer un bon fonctionnement de la commande picocom il est nécessaire de modifier le fichier /boot/firmware/config.txt
  • Taper : $ sudo nano /boot/firmware/config.txt (obligé d’être en superutilisateur, car le fichier est protégé)
  • Au paragraphe All vérifier la valeur de enable_uart
  • Elle doit être égale à 1 ; si elle est à 0, la modifier
  • Ajouter une ligne supplémentaire (pour désactiver le bluetooth) : $ dtoverlay=disable-bt
  • Si le bluetooth n’est pas désactivé, la lecture des données pourra être mauvaise
  • Sauvegarder et quitter : Ctrl-O Entrée Ctrl-X

Tester la commande

  • Dans le terminal lancer la commande de lecture des données : $ picocom -b 9600 -d 7 -p e -f h /dev/ttyAMA0
  • Les données de la TIC défilent en continu dans le terminal
  • Pour interrompre, taper : Ctrl-A Ctrl-Q
  • La signification des drapeaux de la commande sont disponibles sur la page man
  • Il convient d’être en 9600 bauds (et non 1200), d’avoir une parité paire (-p e ) et d’être à 7 bits par caractère (-d 7 ), ainsi que le préconise Enedis dans sa notice de téléinformation

Installer Django

Pour assurer une visualisation agréable des données, on va ouvrir des pages contenant les informations souhaitées dans un navigateur web. La méthode utilisée, ici, est de passer par un site internet créé avec Django.

Lire la suite

Partie 3 : Capture des Données


Structure des données

La TéléInformation Client envoie en continu les données du compteur Linky. Ces données sont structurées sous forme de trames. Chaque trame est délimitée par un caractère de début STX (b'\x02') et un caractère de fin ETX (b'\x03') . Au sein de chaque trame, des étiquettes vont s’afficher avec, en correspondance, les valeurs attribuées à ces étiquettes. La liste des étiquettes varie selon que l’on est en mode historique ou standard, selon que l’on est en monophasé ou en triphasé, selon que l’on bénéficie ou non d’un tarif heures pleines / heures creuses. La liste des étiquettes est disponible chez Enedis.

Lire la suite

Partie 4 : Lancement de la Base de Données


Quelles données stocker

Les données qu’on souhaite stocker dans la base de données sont les suivantes :

  • DATE
  • LTARF
  • EAST, EASF01, EASF02
  • EASD01, EASD02, EASD03, EASD04
  • SINSTS, SINSTS1, SINSTS2, SNSIST3
    Au besoin, en fonction des informations désirées, il est possible de recueillir d’autres champs.

Fichier models.py

Création du fichier models.py

  • Les données qu’on désire stocker dans la base de données doivent être déclarées dans un fichier models.py
  • Ce fichier existe déjà dans le dossier ticapp
  • Ouvrir le fichier en écriture : $ nano ~/djangoTIC/ticServer/ticapp/models.py
  • Copier/coller le contenu suivant :
    ticServer/ticapp/models.py
    from django.db import models
    
    # Create your models here.
    
    class Data(models.Model):
        dateTime = models.DateTimeField(auto_now_add=True, db_index=True)   # la date et l'heure données par l'ordinateur
        date = models.CharField(max_length=13,blank=True, null=True)        # la date et l'heure données par le TIC
        ltarf = models.CharField(max_length=16,blank=True, null=True)       # Type de tarif en cours
        east = models.IntegerField(blank=True, null=True)                   # Energie active soutirée Fournisseur Consommation totale
        easf01 = models.IntegerField(blank=True, null=True)                 # Energie active soutirée Fournisseur index 1 Consommation heures creuses
        easf02 = models.IntegerField(blank=True, null=True)                 # Energie active soutirée Fournisseur index 2 Consommation heures pleines
        easd01 = models.IntegerField(blank=True, null=True)                 # Energie active soutirée Distributeur index 1
        easd02 = models.IntegerField(blank=True, null=True)                 # Energie active soutirée Distributeur index 2   
        easd03 = models.IntegerField(blank=True, null=True)                 # Energie active soutirée Distributeur index 3
        easd04 = models.IntegerField(blank=True, null=True)                 # Energie active soutirée Distributeur index 4
        sinsts = models.IntegerField(blank=True, null=True)                 # Puissance apparente instantanée soutirée 
        sinsts1 = models.IntegerField(blank=True, null=True)                # Puissance apparente instantanée soutirée phase 1
        sinsts2 = models.IntegerField(blank=True, null=True)                # Puissance apparente instantanée soutirée phase 2
        sinsts3 = models.IntegerField(blank=True, null=True)                # Puissance apparente 
  • Sauvegarder et quitter

Commentaire sur les données

  • dateTime est une date, caractérisée par le champ DateTimeField ; il s’agit de la date au moment de la création des données
  • date est un string, qui comprend 13 caractères ; c’est une date du type H251228062542 pour le 28 décembre 2025 à 6H25mn42s
  • ltarf est un string de longueur maximale 16 caractères
  • les autres champs sont des données numériques entières

Créer la table dans base de données

  • Le fichier models.py ayant été modifié, il faut faire prendre en compte ces modifications par Django
  • Se placer dans le dossier qui contient manage.py : $ cd ~/djangoTIC/ticServer
  • Lancer les deux commandes suivantes :
    $ python manage.py makemigrations
    $ python manage.py migrate
  • Le modèle s’appelant Data et l’application ticapp, ces commandes vont créer la table ticapp_data dans la base de données

Remplir la base de données

Introduction

  • Une fois créée la table ticapp_data avec ses colonnes bien définies, il faut lui fournir les données
  • Pour cela il faut adapter le fichier de capture de données ~/djangoTIC/scripts/readTIC_test.py de manière à ce que les données soient versées dans la base de données.
  • Plutôt que d’utiliser du langage SQL brut pour rentrer ces données, il est préférable de laisser Django manipuler celles-ci en utilisant son ORM, qui va directement faire le lien entre notre code python et la base de données
  • Ceci est d’autant plus nécessaire que la gestion des dates dans la base de données peut poser des problèmes de dates naïves et avisées (naive/aware) avec risque d’erreur dans les calculs de temps écoulé entre deux dates
  • L’une des manières adaptées pour fournir la base de données est de passer par une commande personnalisée

Création de la commande capture_tic

  • Créer les dossiers nécessaires : $ mkdir -p ~/djangoTIC/ticServer/ticapp/management/commands
  • Transformer ces dossiers en modules python en y créant un fichier __init__.py :
    $ touch ~/djangoTIC/ticServer/ticapp/management/__init__.py
    $ touch ~/djangoTIC/ticServer/ticapp/management/commands/__init__.py
  • Créer et ouvrir le fichier capture_tic : $ nano ~/djangoTIC/ticServer/ticapp/management/commands/capture_tic.py
  • Copier le code suivant :
    management/commandes/capture_tic.py
    from django.core.management.base import BaseCommand
    from ticapp.models import Data
    from django.utils import timezone
    import serial
    import time
    
    class Command(BaseCommand):
        help = 'Capture et insère les données TIC en continu depuis le compteur Linky'
    
        def __init__(self):
            super().__init__()
            # Initialisation du port série
            self.ser = serial.Serial(
                port="/dev/ttyAMA0",
                baudrate=9600,
                bytesize=serial.SEVENBITS,
                parity=serial.PARITY_NONE,
                stopbits=serial.STOPBITS_ONE,
                timeout=1,
                xonxoff=False,
                rtscts=False
            )
            self.listItems = ["DATE", "LTARF", "EAST", "EASF01", "EASF02", "EASD01", "EASD02", "EASD03", "EASD04", "SINSTS", "SINSTS1", "SINSTS2", "SINSTS3"]
    
        def checkIfDictFull(self, dict, listItems):
            """Vérifie si un dictionnaire contient toutes les clés qui sont dans la liste listItems"""
            setKeys = set(dict.keys())
            return set(listItems).issubset(setKeys)
    
        def capture_trame(self):
            """Capture une trame TIC complète"""
            while True:
                if self.ser.read(1) == b'\x02':  # Début de trame
                    trame = b'\x02'
                    while True:
                        byte = self.ser.read(1)
                        trame += byte
                        if byte == b'\x03':  # Fin de trame
                            return trame
    
        def parse_trame(self, trame):
            """Parse la trame et retourne un dictionnaire avec les données"""
            try:
                data = trame[1:-1].decode('ascii', errors='ignore')
                lines = data.split('\r\n')
                dico = {}
                
                for line in lines:
                    if '\t' in line:
                        parts = line.split('\t')
                        key = parts[0]
                        value = parts[1] if len(parts) > 1 else ''
                        dico[key] = value
                
                # Ne retourner que si toutes les clés nécessaires sont présentes
                if self.checkIfDictFull(dico, self.listItems):
                    subDico = {key: dico[key] for key in self.listItems if key in dico}
                    return subDico
                else:
                    return None
                    
            except Exception as e:
                self.stderr.write(self.style.ERROR(f'Erreur de parsing: {e}'))
                return None
    
        def handle(self, *args, **options):
            self.stdout.write(self.style.SUCCESS('=== Démarrage de la capture TIC ==='))
            self.stdout.write(f'Port série: {self.ser.port}')
            self.stdout.write(f'Baudrate: {self.ser.baudrate}')
            
            compteur = 0
            erreurs = 0
            
            try:
                while True:
                    # Capture de la trame
                    trame = self.capture_trame()
                    
                    # Parse de la trame
                    donnees = self.parse_trame(trame)
                    
                    if donnees:
                        try:
                            # Insertion dans la base de données
                            Data.objects.create(
                                date=donnees["DATE"][1:],  # Retire le premier caractère
                                larf=donnees["LTARF"],
                                east=donnees["EAST"],
                                easf01=donnees["EASF01"],
                                easf02=donnees["EASF02"],
                                easd01=donnees["EASD01"],
                                easd02=donnees["EASD02"],
                                easd03=donnees["EASD03"],
                                easd04=donnees["EASD04"],
                                sinsts=donnees["SINSTS"],
                                sinsts1=donnees["SINSTS1"],
                                sinsts2=donnees["SINSTS2"],
                                sinsts3=donnees["SINSTS3"]
                            )
                            
                            compteur += 1
                            erreurs = 0  # Reset du compteur d'erreurs
                            
                            # Log périodique
                            if compteur % 100 == 0:
                                self.stdout.write(
                                    self.style.SUCCESS(
                                        f'✓ {compteur} enregistrements insérés - '
                                        f'Dernier: {timezone.now().strftime("%H:%M:%S")}'
                                    )
                                )
                        
                        except Exception as e:
                            erreurs += 1
                            self.stderr.write(
                                self.style.ERROR(f'Erreur insertion BD: {e}')
                            )
                            if erreurs > 10:
                                self.stderr.write(
                                    self.style.ERROR('Trop d\'erreurs consécutives, arrêt')
                                )
                                break
                    
                    else:
                        # Trame incomplète, on continue sans erreur
                        pass
                    
                    time.sleep(0.5)  # Petite pause entre les captures
                    
            except KeyboardInterrupt:
                self.stdout.write(
                    self.style.WARNING(
                        f'\n=== Arrêt demandé par l\'utilisateur ===\n'
                        f'Total enregistrements: {compteur}'
                    )
                )
            
            except Exception as e:
                self.stderr.write(self.style.ERROR(f'Erreur fatale: {e}'))
            
            finally:
                # Fermeture propre du port série
                if self.ser.is_open:
                    self.ser.close()
                    self.stdout.write('Port série fermé')
  • Sauvegarder et quitter : Ctrl-O Entrée Ctrl-X

Lancer le script de captage des données

  • Se mettre, en environnement virtuel, dans ticServer : $ cd ~/djangoTIC/ticServer
  • Lancer la commande : $ python manage.py capture_tic
  • La base de données commence à stocker les données du Linky

Rendre le captage et le stockage des données pérennes

  • Il convient que captage et stockage se fassent automatiquement au reboutage de la Raspberry
  • Pour cela on crée un service systemd
  • Créer le service : $ sudo nano /etc/systemd/system/tic-capture.service
  • Coller le code suivant :
    /etc/systemd/system/tic-capture.service
    [Unit]
    Description=Capture des données TIC Linky
    After=network.target
    
    [Service]
    Type=simple
    User=pi
    WorkingDirectory=/home/pi/djangoTIC/ticServer
    ExecStart=/home/pi/djangoTIC/venv/bin/python manage.py capture_tic
    Restart=always
    RestartSec=10
    
    [Install]
    WantedBy=multi-user.target
  • Sauvegarder et quitter
  • Charger le service :
    $ sudo systemctl daemon-reload
    $ sudo systemctl enable tic-capture
    $ sudo systemctl start tic-capture
  • Tester le service : $ sudo systemctl status tic-capture
  • Le système démarrera automatiquement au boutage de la Raspberry
Lire la suite

Partie 5 : Structure Du Site


Principe de fonctionnement de Django

Pour en savoir plus sur Django

Mode url - view - template

Django fonctionne selon le mode MTV (model/view/template) :

  • Une URL appelle une view ; la view traite la logique et renvoie à l’affichage dans le navigateur un template
    Pour chaque page que nous souhaitons afficher il faut donc :
  • créer une vue dans ~/djangoTIC/ticServer/ticapp/views.py sous forme d’une fonction
  • créer une url dans ~/djangoTIC/ticServer/ticapp/urls.py qui sera spécifique pour la vue
  • créer un gabarit (template) dans ~/dajngoTIC/ticServer/ticapp/template/ , spécifique pour la vue et l’url considérées
    Dans ce tutoriel, nous allons créer les pages :
  • accueil : affichage des puissances apparentes instantanées
  • jour : affichage de la consommation depuis minuit pour le jour en cours
  • 24 heures : affichage de la consommation des 24 dernières heures
  • hier : affichage de la consommation de la veille
  • dernière heure : affichage de la consommation de l’heure écoulée

Création de la page d’accueil

La page d’accueil index.html va afficher :

Lire la suite

Partie 6 : Mise en Prodution


Serveur Gunicorn

Actuellement, le site fonctionne en passant par le serveur embarqué de Django en tapant, en environnement virtuel, la commande : python manage.py runserver 0.0.0.0:8000 . Il convient de ne plus passer par ce serveur embarqué et d’avoir son propre serveur. Le choix porte sur Gunicorn

Lire la suite