Suivi de la consommation électrique dans un navigateur web

dimanche 17 mars 2013
par  Finizi
popularité : 18%

Commençons par la fin et regardons ce que l’on souhaite obtenir dans un navigateur :

PNG - 12.1 ko
CompteurEDF
Ce que l’on peut obtenir sur un navigateur

Pour arriver à ce résultat, il faudra le matériel suivant :
- Une carte Arduino Uno
- Un shield Téléinfo
- Un Raspberry Pi, avec une distribution Raspbian
- Un PC pour lire le résultat

L’ensemble carte Arduino + shield Téléinfo doit sans doute pouvoir être remplacé par d’autre matériel permettant de récupérer directement la Téléinfo sur un port USB.
De même, le RaspberryPi pourra être remplacé par n’importe quel PC tournant sous Linux / Lubuntu. Le très grand avantage du RaspberryPi est ici sa consommation réduite.

Au niveau logiciel, il va falloir mettre en œuvre les technologies suivantes :
- Langage Python pour lire le port série et servir les pages web. Voici la page wikipédia. Ici, c’est la version 2.7.3 qui est utilisée.
- Langage Javascript utilisé à l’intérieur du navigateur.
- Ajax pour récupérer uniquement les informations dont on a besoin sans nécessiter de recharger toute la page web à chaque changement.
- Bottle : un framework simple, rapide et léger qui va nous permettre de programmer facilement un server web.
- JSON : un format léger d’échange de données sous forme de texte.


 La source : Arduino

La source des données est le compteur électrique. Entre le compteur et le server web, il y a une carte Arduino qui va permettre de récupérer les données sur un port USB.
Pour ce qui est du montage, de l’installation et de la programmation, tout est ici sur le site Mon-Club-Elec.
Le shield Téléinfo utilisé est disponible sur le site Cartelectronic sur lequel on trouve toutes les informations relatives à la Téléinfo.


 Python et le port USB

Le langage Python va nous servir pour écrire le code permettant de faire le lien entre les données qui arrivent sur la port USB et le web.

Les informations de Téléinfo
Tout d’abord, une classe servant à gérer les informations de Téléinfo qui nous intéressent. J’en ai retenu 3 :
- L’index du compteur, en Wh, qui permet d’afficher la consommation en kWh
- La puissance apparente, en VoltAmpère
- L’intensité instantannée, en Ampère

Cette classe va faire office de réceptacle tampon entre la classe qui va lire les information du port USB, qui va tourner dans un thread séparé, et le serveur web qui va servir et diffuser les informations.

class TeleInfo(object):
  """
  Informations en cours issues du module Arduino de Téléinformation
  """

  def __init__(self):
    self._index = 0
    self._pa = -1
    self._ii = -1
    pass
   
  def _get_index(self):
    return(self._index)
  def _set_index(self, newIndex):
    self._index = newIndex
  index = property(  fget = _get_index,
            fset = _set_index,
            doc = "index, en W/h, affiché par le compteur." )
 
  def _get_kWh(self):
    return(int(float(self._index) / 1000.0))
  kWh = property(  fget = _get_kWh,
          doc = "index, en kW/h, affiché par le compteur." )
           
  def _get_pa(self):
    return(self._pa)
  def _set_pa(self, newPa):
    self._pa = newPa
  pa = property(  fget = _get_pa,
          fset = _set_pa,
          doc = "Puissance apparente courante, en 'VoltAmpères'" )
  def _get_ii(self):
    return(self._ii)
  def _set_ii(self, newIi):
    self._ii = newIi
  ii = property(  fget = _get_ii,
          fset = _set_ii,
          doc = "Intensité instantannée, en 'Ampères'" )
  def ml(self):
    """représentation xml des valeurs courantes du compteur"""
    return('<ti index="{}" pa="{}" />'.format(self._index, self._pa))
   
  def json(self):
    """représentation json des valeurs courantes du compteur"""
    return '{{ "index": {}, "pa": {} }}'.format(self._index, self._pa)

La lecture du port USB
Et maintenant, voici la classe chargée de lire et récupérer les données du port USB :

class readFromSerial(threading.Thread):
  """
  Lecture du port USB sur lequel est branché la téléInformation.
  La lecture se fait dans un thread différent.
  """

  def __init__(self, ti):
    threading.Thread.__init__(self)
    self.setDaemon(True)
    self._ti = ti
    self._ser = serial.Serial(  port='/dev/ttyACM0',
                  baudrate=1200,
                  parity=serial.PARITY_EVEN,
                  stopbits=serial.STOPBITS_ONE,
                  bytesize=serial.SEVENBITS)
       
  def __del__(self):
    self._ser.close()
  def run(self):
    while True:
      tiLine = self._ser.readline()
      if (string.find(tiLine, 'BASE ') == 0):
        if (tiLine[5:14].isdigit() == True):
          self._ti.index = int(tiLine[5:14])
      if (string.find(tiLine, 'IINST ') == 0):
        if (tiLine[6:8].isdigit() == True):
          self._ti.ii = int(tiLine[6:9])
      if (string.find(tiLine, 'PAPP ') == 0):
        if (tiLine[5:10].isdigit() == True):
          self._ti.pa = int(tiLine[5:10])

Cette classe tourne « toute seule » et lit toutes les lignes envoyées sur la communication série (le port USB), On y cherche des mots spécifiques afin d’identifier les lignes qui nous intéresse, puis on isole l’information intéressante de la ligne. Enfin, l’information isolées est passée à une instance de classe Teleinfo vue plus haut. En effet, le constructeur de cette classe (__init__) nécessite un paramètre qui est une instance de la classe Teleinfo.
Et oui, je sais, il faudrait mettre les paramètres du port série (USB) dans fichier de configuration...


 Python et le serveur Web

Pour servir les pages web, j’ai décidé d’utiliser Bottle, principalement pour sa simplicité d’utilisation.

Chaque page web est définie par une fonction « décorée », c’est à dire que l’on a placé un décorateur sur la ligne précédent la définition de la fonction.

Ici, le serveur web va servir 3 pages différentes :
- Une page avec juste les informations textuelles http://monserverlocal/edf, affichant juste les 4 lignes de la copie d’écran en début d’article.
- Une page avec texte et graphique http://monserverlocal/edf/graph, correspondant à la copie d’écran du début.
- Et une page d’information http://monserverlocal/edf/json. Ce n’est pas exactement une page puisque le serveur ne renvoie que les informations courantes selon le format JSON, qui est un format léger d’échange de données sous forme de texte.

Les 3 fonctions sont les suivantes :

@webApp.route('/edf')
def edf():
        return static_file('web01.html', root='/home/pi/Python')
@webApp.route('/edf/graph')
def edfgraph():
        return static_file('web02.html', root='/home/pi/Python')
@webApp.route('/edf/json', method='ANY')
def edfjson():
        dict = {'index':ti.index, 'kWh':ti.kWh, 'pa':ti.pa, 'ii':ti.ii }
        return dict

Et voilà, et c’est tout !
Oui, c’est court, très court.

  • 1. La fonction edf() ne fait que renvoyer un fichier nommé « web01.html » qui se trouve dans le dossier "/home/pi/Python".
  • 2. La fonction edfgraph() ne renvoie elle un fichier nommé « web02.html » qui se trouve aussi dans le dossier "/home/pi/Python".
  • 3. Quant à la fonction edfjson(), elle renvoie un dictionnaire contenant 4 informations :
    • L’index du compteur : « index »
    • La consommation en kWh : « kWh »
    • La puissance apparante : « pa »
    • L’intensité instantannée : « ii »

Le fait de renvoyer un dictionnaire force Bottle à formater les données en JSON.

Voici un exemple du contenu renvoyé :

{"ii": 17, "index": 39373025, "pa": 3870, "kWh": 39373}

L’instanciation du site web doit se faire avant les fonctions par

webApp = Bottle()

A la fin du code sont instanciées les différentes classes et lancé le site :

ti = TeleInfo()
# Instancie & démarre les threads de lecture de la TéléInfo
tiReader = readFromSerial(ti)
tiReader.start()
# Lance le site web
run(webApp, host='0.0.0.0', port=8080, server='cherrypy')

Une dernière petite astuce : Bottle n’est pas un serveur en lui même, mais un outil permettant de programmer facilement un serveur HTTP. Il est possible d’utiliser le serveur par défaut. Toutefois, ayant remarqué des temps de réponse assez important, j’ai choisi d’utiliser CherryPy qui contient un serveur HTTP rapide et multitâche (paramètre “server” de la fonction run).

Le fichier Python complet est disponible en fin d’article.


 HTML & Javascript

Nous arrivons maintenant à la partie affichage des données.

Commençons par le fichier “web01.html”. Il contient du code javascript et du code HTML.

Le code HTML
Le code HTML est des plus simple : 4 lignes de texte, avec des balises nommées permettant d’afficher les valeurs en gras. On notera qu’aucune valeur n’est présente dans le fichier.
Dans le second fichier, on trouve en plus la déclaration d’un “canvas”. C’est en fait la zone dans laquelle on va tracer la courbe de consommation.
Noter la première ligne du second fichier, nécessaire pour que le canvas fonctionne avec Internet Explorer.

Le code Javascript

Le code javascript contient plusieurs fonctions :
- window.onload
est la fonction d’entrée appelée au chargement de la page.
- requeteAjax
est la fonction qui va interroger le serveur web pour obtenir les données à afficher.
Pour que Ajax fonctionne avec Internet Explorer, la méthode d’appel doit impérativement être « POST ».
- drawData
est la fonction qui va afficher les données. Dans le second fichier, les données serviront à construire le graphique.
Noter la façon simple dont sont récupérées les données JSON : eval('(' + stringDataIn + ')')

La courbe est tracée dans la zone jaunâtre. Elle commence évidemment à gauche. Lorsqu’elle arrive tout à droite, l’ensemble du graphique est légèrement décalé vers la gauche de façon à ne perdre que les données les plus anciennes.

Ce code est livré tel-quel. faites-en bon usage !


 Les fichiers complets

ServerTeleinfo.py :
le serveur web en python

  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. import threading
  4. import serial
  5. import string
  6. import datetime
  7. from bottle import Bottle, run, template, static_file
  8. ################################################################################
  9. class TeleInfo(object):
  10.   """
  11.  Informations en cours issues du module Arduino de Téléinformation
  12.  """
  13.   def __init__(self):
  14.     self._index = 0
  15.     self._pa = -1
  16.     self._ii = -1
  17.     pass
  18.    
  19.   def _get_index(self):
  20.     return(self._index)
  21.   def _set_index(self, newIndex):
  22.     self._index = newIndex
  23.   index = property(  fget = _get_index,
  24.             fset = _set_index,
  25.             doc = "index, en W/h, affiché par le compteur." )
  26.  
  27.   def _get_kWh(self):
  28.     return(int(float(self._index) / 1000.0))
  29.   kWh = property(  fget = _get_kWh,
  30.           doc = "index, en kW/h, affiché par le compteur." )
  31.            
  32.   def _get_pa(self):
  33.     return(self._pa)
  34.   def _set_pa(self, newPa):
  35.     self._pa = newPa
  36.   pa = property(  fget = _get_pa,
  37.           fset = _set_pa,
  38.           doc = "Puissance apparente courante, en 'VoltAmpères'" )
  39.   def _get_ii(self):
  40.     return(self._ii)
  41.   def _set_ii(self, newIi):
  42.     self._ii = newIi
  43.   ii = property(  fget = _get_ii,
  44.           fset = _set_ii,
  45.           doc = "Intensité instantannée, en 'Ampères'" )
  46.   def ml(self):
  47.     """représentation xml des valeurs courantes du compteur"""
  48.     return('<ti index="{}" pa="{}" />'.format(self._index, self._pa))
  49.    
  50.   def json(self):
  51.     """représentation json des valeurs courantes du compteur"""
  52.     return '{{ "index": {}, "pa": {} }}'.format(self._index, self._pa)
  53. ################################################################################
  54. class readFromSerial(threading.Thread):
  55.   """Lecture du port USB sur lequel est branché la téléInformation.
  56.    La lecture se fait dans un thread différent.
  57.  """
  58.   def __init__(self, ti):
  59.     threading.Thread.__init__(self)
  60.     self.setDaemon(True)
  61.     self._ti = ti
  62.     self._ser = serial.Serial(  port='/dev/ttyACM0',
  63.                   baudrate=1200,
  64.                   parity=serial.PARITY_EVEN,
  65.                   stopbits=serial.STOPBITS_ONE,
  66.                   bytesize=serial.SEVENBITS)
  67.        
  68.   def __del__(self):
  69.     self._ser.close()
  70.   def run(self):
  71.     while True:
  72.       tiLine = self._ser.readline()
  73.       if (string.find(tiLine, 'BASE ') == 0):
  74.         if (tiLine[5:14].isdigit() == True):
  75.           self._ti.index = int(tiLine[5:14])
  76.       if (string.find(tiLine, 'IINST ') == 0):
  77.         if (tiLine[6:8].isdigit() == True):
  78.           self._ti.ii = int(tiLine[6:9])
  79.       if (string.find(tiLine, 'PAPP ') == 0):
  80.         if (tiLine[5:10].isdigit() == True):
  81.           self._ti.pa = int(tiLine[5:10])
  82. ################################################################################
  83. # Instancie un site web
  84. webApp = Bottle()
  85. ################################################################################
  86. @webApp.route('/edf')
  87. def edf():
  88.   return static_file('web01.html', root='/home/pi/Python')
  89. @webApp.route('/edf/json', method='ANY')
  90. def edfjson():
  91.   dict = {'index':ti.index, 'kWh':ti.kWh, 'pa':ti.pa, 'ii':ti.ii }
  92.   return dict
  93. @webApp.route('/edf/graph')
  94. def edfgraph():
  95.   return static_file('web02.html', root='/home/pi/Python')
  96. ################################################################################
  97. ti = TeleInfo()
  98. # Instancie & démarre les threads de lecture de la TéléInfo
  99. tiReader = readFromSerial(ti)
  100. tiReader.start()
  101. # Lance le site web
  102. run(webApp, host='0.0.0.0', port=8080, server='cherrypy')

web01.html :
Uniquement les infos textuelles

  1.   <head>
  2.     <script language="javascript" type="text/javascript">
  3.       window.onload = function() {
  4.         requeteAjax(drawData);
  5.         setInterval(function () {requeteAjax(drawData);}, 1000);
  6.       }
  7.      
  8.       function requeteAjax(callback) {
  9.         var xhr;
  10.         if(window.XMLHttpRequest) { // Firefox
  11.           xhr = new XMLHttpRequest();
  12.         }
  13.         else if(window.ActiveXObject) { // Internet Explorer
  14.           xhr = new ActiveXObject("Microsoft.XMLHTTP");
  15.           xhr.timeout = 1000;
  16.         }
  17.        
  18.         xhr.onreadystatechange = function(fnCB) {
  19.           if (xhr.readyState == 4 && xhr.status == 200) {
  20.            callback(xhr.responseText);
  21.           }
  22.         };
  23.         xhr.open("POST", "/edf/json", true);
  24.         xhr.send(null);
  25.       }
  26.      
  27.       function drawData(stringDataIn) {
  28.         var obj = eval('(' + stringDataIn + ')');
  29.         document.getElementById("index").innerHTML = obj.index;
  30.         document.getElementById("kWh").innerHTML = obj.kWh;
  31.         document.getElementById("pa").innerHTML = obj.pa;
  32.         document.getElementById("ii").innerHTML = obj.ii;
  33.       }
  34.     </script>
  35.   </head>
  36.   <body>
  37.     Compteur EDF :
  38.     <br/>
  39.     - Index = <b id="index"></b>&nbsp;soit <b id="kWh"></b>&nbsp;kWh
  40.     <br/>
  41.     - Puissance apparente = <b id="pa"></b>&nbsp;VoltAmpères
  42.     <br/>
  43.     - Intensité instatannée = <b id="ii"></b>&nbsp;Ampères
  44.   </body>
  45. </html>

web02.html :
Les infos textuelles plus le graphique

  1. <!DOCTYPE html>
  2.   <head>
  3.     <script language="javascript" type="text/javascript">
  4.       // variables / objets globaux
  5.       var canvas= null;
  6.       var contextCanvas = null;
  7.       var delai=1000;
  8.       var compt=0;
  9.       var x=0;
  10.       var y=0;
  11.       var xo=0;
  12.       var yo=0;
  13.       window.onload = function() {
  14.         canvas = document.getElementById("nomCanvas");
  15.         canvas.width = 1600;
  16.         canvas.height = 500;
  17.        
  18.         if (canvas.getContext){
  19.           contextCanvas = canvas.getContext("2d");
  20.        
  21.           // carre  de la taille du canvas
  22.           contextCanvas.fillStyle = "rgb(255,255,200)";
  23.           contextCanvas.fillRect (0, 0, canvas.width, canvas.height);
  24.        
  25.           // position initiale - dessine 1 pixel
  26.           contextCanvas.fillStyle = "rgb(0,0,255)";
  27.           contextCanvas.fillRect (x,canvas.height-y-1, 1,1);
  28.        
  29.           // parametres graphique
  30.           contextCanvas.lineWidth=1;
  31.           contextCanvas.strokeStyle = "rgb(0,0,255)";
  32.        
  33.           // intervalle de rafraichissement                                  
  34.           setTimeout(function () {requeteAjax(drawData);}, delai);
  35.        
  36.         }
  37.         else {
  38.           window.alert("Canvas non disponible") ;
  39.         }
  40.       }
  41.      
  42.       function requeteAjax(callback) {
  43.         var xhr;
  44.         if(window.XMLHttpRequest) { // Firefox
  45.           xhr = new XMLHttpRequest();
  46.         }
  47.         else if(window.ActiveXObject) { // Internet Explorer
  48.           xhr = new ActiveXObject("Microsoft.XMLHTTP");
  49.           xhr.timeout = 1000;
  50.         }
  51.        
  52.         xhr.onreadystatechange = function(fnCB) {
  53.           if (xhr.readyState == 4 && xhr.status == 200) {
  54.            callback(xhr.responseText);
  55.           }
  56.         };
  57.         xhr.open("POST", "/edf/json", true);
  58.         xhr.send(null);
  59.       }
  60.      
  61.       function drawData(stringDataIn) {
  62.         var obj = eval('(' + stringDataIn + ')');
  63.         document.getElementById("index").innerHTML = obj.index;
  64.         document.getElementById("kWh").innerHTML = obj.kWh;
  65.         document.getElementById("pa").innerHTML = obj.pa;
  66.         document.getElementById("ii").innerHTML = obj.ii;
  67.         if (contextCanvas!=null) {
  68.           // -- coordonnees x,y courantes
  69.           x = x + 1; // pas de 1 pixel à la fois
  70.           if (x > canvas.width) {
  71.             decal = 200;
  72.             var imgData = contextCanvas.getImageData(decal, 0, canvas.width, canvas.height);
  73.             contextCanvas.putImageData(imgData,0,0);
  74.             x -= decal;
  75.             xo -= decal;
  76.             contextCanvas.fillStyle = "rgb(255,255,200)";
  77.             contextCanvas.fillRect (canvas.width - decal, 0, canvas.width, canvas.height);          
  78.           }                
  79.           //y = Number(stringDataIn) // récupère la valeur chiffrée à partir chaine reçue
  80.           y = obj.pa;
  81.           y = y/15000*(canvas.height-1) // y mappée sur la hauteur canvas
  82.           // -- trace courbe --                  
  83.           // RAZ courbe si x==0
  84.           if (x == 0) {
  85.             contextCanvas.moveTo(x,y);
  86.             contextCanvas.fillStyle = "rgb(255,255,200)";
  87.             contextCanvas.fillRect (0, 0, canvas.width, canvas.height);
  88.             xo = 0;
  89.           }
  90.          
  91.           // sinon trace ligne point courant
  92.           else {
  93.             contextCanvas.beginPath();
  94.             contextCanvas.moveTo(xo,canvas.height-yo-1);
  95.             contextCanvas.lineTo(x,canvas.height-y-1);              
  96.             contextCanvas.closePath();
  97.             contextCanvas.stroke();
  98.             document.getElementById("abscisse").innerHTML = x;
  99.            
  100.             xo=x;
  101.             yo=y;
  102.           }
  103.         }
  104.         // réinitialise delai
  105.         setTimeout(function () {requeteAjax(drawData);}, delai);
  106.       }
  107.     </script>
  108.   </head>
  109.   <body>
  110.     Compteur EDF :
  111.     <br/>
  112.     - Index = <b id="index"></b>&nbsp;soit <b id="kWh"></b>&nbsp;kWh
  113.     <br/>
  114.     - Puissance apparente = <b id="pa"></b>&nbsp;VoltAmpères
  115.     <br/>
  116.     - Intensité instatannée = <b id="ii"></b>&nbsp;Ampères
  117.     <br/>
  118.     <br/>
  119.    
  120.     <canvas id="nomCanvas" width="300" height="300"></canvas>
  121.     <br/>
  122.     x = <font size="2" id="abscisse"></font>
  123.     <br/>
  124.     Serveur RaspberryPi Python Bottle  -  TéléInfo sur carte arduino via port série.
  125.     <br/>
  126.   </body>
  127. </html>

Navigation

Articles de la rubrique

  • Suivi de la consommation électrique dans un navigateur web