Suivi de la consommation électrique

jeudi 17 avril 2014
par  Finizi
popularité : 33%

Après avoir installé et configuré le RaspberryPi, il est temps de passer à son utilisation.
Ici, il va s’agir de lire en continu les informations issues de la téléinformation, de servir ces informations sur une page web, et de les envoyer sur un server Internet qui se chargera de les enregistrer.


 Le matériel

Le matériel utilisé est composé de :


 Architecture générale

Il y a 3 fichiers pour assurer toutes les fonctions :

  • Un script python, qui réalise toutes les opérations coté serveur.
  • Un fichier HTML pour l’affichage des données coté client.
  • Un fichier javaScript pour l’interractivité.

 Contenu du script python

L’essentiel de ce qui est réalisé est codé dans un script python, version 2.7.
L’article suivant "Lancement automatique" décrira comment automatiser tout cela et faciliter l’accès à la page web.

Le script python comporte les parties suivantes :

La classe "TeleInfo"
Cette classe est juste destinée à stocker et représenter les données utiles courantes en cours de traitement, c’est à dire les données qui seront affichées et envoyées au serveur Internet de data logging.
C’est un peu le point central qui va être utilsé par les différents processus qui vont y écrire et y lire des données.
Noter qu’une moyenne de la puissance apparente est calculée entre 2 lectures afin de coller au plus près de la réalité.

La classe "readFromSerial"
C’est ici que se fait la lecture des données. Une fois initialisée, cette classe va effectuer son travail dans un processus indépendant.
Son travail consiste à lire tout ce qui arrive du module de téléinformation, et à n’en extraire que les données jugées intéressantes.
Les données extraites sont envoyées à la classe "TéléInfo".

La classe "sendToServer"
Cette classe va envoyer au server Internet de data logging, à interval régulier, toutes les informations à enregistrer.
Une fois initialisée, cette classe va effectuer son travail dans un processus indépendant. Ainsi, le data logging n’est pas perturbé par les demandes de page web.

Les fonctions du serveur web
Une page web est disponible pour l’utilisateur. Par contre, le serveur doit être capable de servir les différents éléments nécessaires :

  • La page web en elle même
  • Le javaScript gérant l’affichage en continu
  • Les données
  • La favicon

Le coeur du programme
C’est là que tout commence. Les différentes classes sont instanciées, et les classes de lecture des données et d’envoi vers le serveur Internet sont lancées.
Enfin, le server Web est lancé.
Tout ce qui est lancé va désormais continuer à fonctionner.


 Les pages web : HTML & javaScript

La page HTML
La page web est en HTML 5 et utilise le framework javaScript AngularJS qui va encapsuler toute la plomberie Ajax XmlHttpRequest qui permet de rendre une page web dynamique.
Une précédente version, sans AngularJS est présentée en détail dans l’article Suivi de la consommation électrique dans un navigateur web.

Le code Javascript
Il ne fait que définir la classe qui va servir de contrôleur. Cette classe est composée de :

  • la propriété "x" qui correspond à l’abcisse du point à tracer.
  • La fonction "draw" qui va tracer la courbe montrant l’évolution de la puissance apparente.
  • la fonction "getJson()" qui se charge de la communication avec le serveur web en l’interrogeant toutes les secondes (60200 ms) sur l’url "teleinfo/json" afin de récupérer les nouvelle données.

Le fichier "angular.min.js" qui est le framework AngularJS est aussi renvoyé par le serveur.

Le fichier teleinfoCtrl.js complet est disponible en plus bas et sur GitHub.


 Les fichiers complets

Le code est aussi disponible sur GitHub.

teleinfo.py :
Les services de récupération et d’envoi des données, ainsi que le serveur web en python.

  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. """
  4. |   Copyright finizi 2014 - Créé le 25/03/2014
  5. |   www.DomoEnergyTICs.com
  6. |
  7. |    Code sous licence GNU GPL :
  8. |    This program is free software: you can redistribute it and/or modify
  9. |    it under the terms of the GNU General Public License as published by
  10. |    the Free Software Foundation, either version 3 of the License,
  11. |    or any later version.
  12. |    This program is distributed in the hope that it will be useful,
  13. |    but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  15. |    GNU General Public License for more details.
  16. |    You should have received a copy of the GNU General Public License
  17. |    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. """
  20.         Script python tournant sur le Raspberry Pi destiné à envoyer régulièrement
  21.          le flux de données de la téléinformation.
  22.         Fait aussi office de server web pour afficher sur le réseau local les
  23.          informations de téléinformation.
  24.        
  25. """
  26. import threading
  27. import time
  28. import serial
  29. import string
  30. import json
  31. import urllib2
  32. import bottle
  33. ######################################################################
  34. class TeleInfo(object):
  35.         """
  36.                 Informations en cours issues du modem USBTIC de Téléinformation
  37.                 Puissance apparente et intensité sont moyennées sur 2 intervalles possibles:
  38.                  - Interval 'Normal': cas par exemple d'une interrogation locale toutes les 10 secondes
  39.                  - Interval long: utilisé pour le data loggin
  40.                 Le compteur EDF est un "Compteur Jaune", c'est à dire qu'il gère 4 compteurs
  41.                 selon l'heure du jour et le jour dans l'année
  42.         """
  43.         def __init__(self):
  44.                 self._indexA = 0    # index du compteur A
  45.                 self._indexB = 0    # index du compteur B
  46.                 self._indexC = 0    # index du compteur C
  47.                 self._indexD = 0    # index du compteur D
  48.                 self._pa = -1       # puissance apparente
  49.                 self._sumPa = -1    # somme des puissances apparentes durant l'intervalle
  50.                 self._sumPaL = -1   # somme des puissances apparentes durant l'intervalle long
  51.                 self._nbPa = 0      # nombre de puissances apparentes lues durant l'intervalle
  52.                 self._nbPaL = 0     # nombre de puissances apparentes lues durant l'intervalle long
  53.                 self._ts = ""       # heure et minute fournie par la téléinfo (timestamp)
  54.                 pass
  55.         def _get_indexA(self):
  56.                 return(self._indexA)
  57.         def _set_indexA(self, newIndexA):
  58.                 self._indexA = newIndexA
  59.         indexA = property(      fget = _get_indexA,
  60.                                                 fset = _set_indexA,
  61.                                                 doc = "index A, en kW/h, affiché par le compteur." )
  62.         def _get_indexB(self):
  63.                 return(self._indexB)
  64.         def _set_indexB(self, newIndexB):
  65.                 self._indexB = newIndexB
  66.         indexB = property(      fget = _get_indexB,
  67.                                                 fset = _set_indexB,
  68.                                                 doc = "index B, en kW/h, affiché par le compteur." )
  69.         def _get_indexC(self):
  70.                 return(self._indexC)
  71.         def _set_indexC(self, newIndexC):
  72.                 self._indexC = newIndexC
  73.         indexC = property(      fget = _get_indexC,
  74.                                                 fset = _set_indexC,
  75.                                                 doc = "index C, en kW/h, affiché par le compteur." )
  76.         def _get_indexD(self):
  77.                 return(self._indexD)
  78.         def _set_indexD(self, newIndexD):
  79.                 self._indexD = newIndexD
  80.         indexD = property(      fget = _get_indexD,
  81.                                                 fset = _set_indexD,
  82.                                                 doc = "index D, en kW/h, affiché par le compteur." )
  83.         def _get_pa(self):
  84.                 dispPa = self._pa
  85.                 if (self._nbPa > 0):
  86.                         dispPa = self._sumPa / self._nbPa
  87.                         self._sumPa = 0
  88.                         self._nbPa = 0
  89.                 return(dispPa)
  90.         def _set_pa(self, newPa):
  91.                 self._pa = newPa
  92.                 self._sumPa += newPa
  93.                 self._nbPa += 1
  94.                 self._sumPaL += newPa
  95.                 self._nbPaL += 1
  96.         pa = property(  fget = _get_pa,
  97.                                         fset = _set_pa,
  98.                                         doc = "Puissance apparente moyenne de l'intervalle 'normal', en 'VoltAmpères'" )
  99.         def _get_paL(self):
  100.                 dispPaL = self._pa
  101.                 if (self._nbPaL > 0):
  102.                         dispPaL = self._sumPaL / self._nbPaL
  103.                         self._sumPaL = 0
  104.                         self._nbPaL = 0
  105.                 return(dispPaL)
  106.         paL = property( fget = _get_paL,
  107.                                         doc = "Puissance apparente moyenne de l'interval long, en 'VoltAmpères'" )
  108.         def _get_ts(self):
  109.                 return(self._ts)
  110.         def _set_ts(self, newTs):
  111.                 self._ts = newTs
  112.         ts = property(  fget = _get_ts,
  113.                                         fset = _set_ts,
  114.                                         doc = "Heure et minute fournie par la téléinfo (timestamp)." )
  115. ######################################################################
  116. class readFromSerial(threading.Thread):
  117.         """
  118.                 Lecture du port USB sur lequel est branché la téléInformation.
  119.                 La lecture se fait dans un thread différent.
  120.         """
  121.         def __init__(self, ti):
  122.                 self._OK = 0
  123.                 try:
  124.                         threading.Thread.__init__(self)
  125.                         self.setDaemon(True)
  126.                         self._ti = ti
  127.                         print 'Initialize serial'
  128.                         self._ser = serial.Serial(      port='/dev/ttyUSB0',
  129.                                                                                 baudrate=1200,
  130.                                                                                 parity=serial.PARITY_EVEN,
  131.                                                                                 stopbits=serial.STOPBITS_ONE,
  132.                                                                                 bytesize=serial.SEVENBITS,
  133.                                                                                 timeout=1)
  134.                         print 'Serial initialized'
  135.                         self._OK = 1
  136.                 except:
  137.                         print 'Serial teleinfo error'
  138.         def __del__(self):
  139.                 self._ser.close()
  140.         def run(self):
  141.                 while True:
  142.                         try:
  143.                                 tiLine = self._ser.readline()
  144.                                 if (string.find(tiLine, 'JAUNE ') == 0):
  145.                                         self._ti.ts = tiLine[6:11]             
  146.                                         if (tiLine[24:29].isdigit() == True):
  147.                                                 self._ti.pa = 10 * int(tiLine[24:29])
  148.                                 if (string.find(tiLine, 'ENERG ') == 0):
  149.                                         if (tiLine[6:12].isdigit() == True):
  150.                                                 self._ti.indexA = int(tiLine[6:12])
  151.                                         if (tiLine[13:19].isdigit() == True):
  152.                                                 self._ti.indexB = int(tiLine[13:19])
  153.                                         if (tiLine[20:26].isdigit() == True):
  154.                                                 self._ti.indexC = int(tiLine[20:26])
  155.                                         if (tiLine[27:33].isdigit() == True):
  156.                                                 self._ti.indexD = int(tiLine[27:33])
  157.                         except:
  158.                                 print 'Serial no data'
  159.                                 time.sleep(60)
  160. ######################################################################
  161. class sendToServer(threading.Thread):
  162.         """
  163.                 Envoie toutes les minutes au server les donnes de téléInformation.
  164.                 Le travaille se fait dans un thread différent.
  165.         """
  166.         def __init__(self, ti):
  167.                 self._OK = 0
  168.                 try:
  169.                         threading.Thread.__init__(self)
  170.                         self.setDaemon(True)
  171.                         self._ti = ti
  172.                         print 'Initialize server'
  173.                         self._OK = 1
  174.                 except:
  175.                         print 'server KO'
  176.         def run(self):
  177.                 # Il faut utiliser un proxy pour sortir sur Internet
  178.                 proxy_info = { 'host' : 'prxy-xxxxxxx-xxxxxx.yy', 'port' : 9999 }
  179.                 proxy_support = urllib2.ProxyHandler({"http" : "http://%(host)s:%(port)d" % proxy_info})
  180.                 # On créé un opener utilisant ce handler:
  181.                 opener = urllib2.build_opener(proxy_support)
  182.                 # Puis on installe cet opener comme opener par défaut du module urllib2.
  183.                 urllib2.install_opener(opener)
  184.                 baseUrl = "http://Url.Du.Serveur.Internet/chargé/du/datalogging/"
  185.                 while True:
  186.                         try:
  187.                                 # prepare l'URL
  188.                                 if (ti.pa >= 0):
  189.                                         dataUrl = '{0}idxA={1}&idxB={2}&idxC={3}&idxD={4}&pa={5}&ts={6}'.format(baseUrl, ti.indexA, ti.indexB, ti.indexC, ti.indexD, ti.paL, ti.ts)
  190.                                         # envoi au server
  191.                                         urllib2.urlopen(dataUrl)
  192.                         except:
  193.                                 print 'Sender error'
  194.                         # dort une minute
  195.                         time.sleep(60)
  196. ######################################################################
  197. # Instancie un site web
  198. webApp = bottle.Bottle()
  199. @webApp.route('/favicon.ico', method='ANY')
  200. def get_favicon():
  201.         return bottle.static_file('favicon.ico', root='static')
  202. @webApp.route('/static/<fileName>')
  203. def staticPages(fileName):
  204.         return bottle.static_file(fileName, root='static')
  205. @webApp.route('/teleinfo')
  206. def edf():
  207.         return bottle.static_file('teleinfo.html', root='static')
  208. @webApp.route('/teleinfo/json', method='ANY')
  209. def edfjson():
  210.         dict = {'idxA':ti.indexA, 'idxB':ti.indexB, 'idxC':ti.indexC, 'idxD':ti.indexD, 'pa':ti.pa }
  211.         return dict
  212. ######################################################################
  213. if __name__ == '__main__':
  214.         # Instancie la classe de gestion de la TéléInfo
  215.         ti = TeleInfo()
  216.         # Instancie & démarre les threads de lecture de la TéléInfo
  217.         tiReader = readFromSerial(ti)
  218.         tiReader.start()
  219.         # Instancie & démarre les threads d'envoie des données
  220.         tiSender = sendToServer(ti)
  221.         tiSender.start()
  222.         # Lance le site web
  223.         bottle.run(webApp, host='0.0.0.0', port=8080, server='cherrypy')

teleinfo.html :
Les infos textuelles plus le graphique

  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  2.         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  3. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  4. <html ng-app>
  5.         <head>
  6.                 <script src="/static/angular.min.js" type="text/javascript"></script>
  7.                 <script src="/static/teleinfoCtrl.js" type="text/javascript"></script>
  8.                 <script language="javascript" type="text/javascript">
  9.                         // variables / objets globaux
  10.                         var canvas = null;
  11.                         var contextCanvas = null;
  12.                         var maxVA = 40000;                      // VolAmpères maxi tracé dans le canvas
  13.                         var x=0;
  14.                         var y=0;
  15.                         var xo=0;
  16.                         var yo=0;
  17.                         // Tracer les guides horizontaux
  18.                         function drawGuides(fx) {
  19.                                 // Fond du Canvas
  20.                                 contextCanvas.fillStyle = "rgb(255,255,200)";
  21.                                 contextCanvas.fillRect (canvas.width - fx, 0, canvas.width, canvas.height);
  22.                                 // Trace les guides tous les 2000 VoltAmpères
  23.                                 savedStrokeStyle = contextCanvas.strokeStyle;
  24.                                 contextCanvas.strokeStyle = "rgb(237,237,237)";
  25.                                 ly = 0;
  26.                                 while (ly/maxVA*(canvas.height-1) < canvas.height) {
  27.                                         ly += 2000
  28.                                         contextCanvas.beginPath();
  29.                                         ty = ly/maxVA*(canvas.height-1);
  30.                                         contextCanvas.moveTo(canvas.width-fx, canvas.height-ty-1);
  31.                                         contextCanvas.lineTo(canvas.width-1, canvas.height-ty-1);
  32.                                         contextCanvas.closePath();
  33.                                         contextCanvas.stroke();
  34.                                 }
  35.                                 contextCanvas.strokeStyle = savedStrokeStyle;
  36.                         }
  37.                         // Initialisation du Canvas
  38.                         window.onload = function() {
  39.                                 canvas = document.getElementById("nomCanvas");
  40.                                 canvas.width = 1400;
  41.                                 canvas.height = 500;
  42.                                 if (canvas.getContext) {
  43.                                         contextCanvas = canvas.getContext("2d");
  44.                                         // position initiale - dessine 1 pixel
  45.                                         contextCanvas.fillStyle = "rgb(0,0,255)";
  46.                                         contextCanvas.fillRect(x, canvas.height-y-1, 1, 1);
  47.                                         // parametres graphique
  48.                                         contextCanvas.lineWidth=1;
  49.                                         drawGuides(canvas.width);
  50.                                         contextCanvas.strokeStyle = "rgb(0,0,255)";
  51.                                 } // fin si canvas existe
  52.                                 else {
  53.                                         window.alert("Canvas non disponible") ;
  54.                                 } // fin else
  55.                         } // fin onload
  56.                 </script>
  57.         </head>
  58.         <body ng-controller="teleinfoCtrl" ng-init="getJson()">
  59.                 <h3>Compteur EDF - école de Kermelo:</h3>
  60.                 <ul>
  61.                         <li>Puissance apparente = <b>{{data.pa}}</b>&nbsp; VoltAmpères</li>
  62.                         <li>Index général = <b>{{cumul}}</b>&nbsp; kWh &nbsp; &nbsp; <small>( détail: &nbsp; Index A = <b>{{data.idxA}}</b>&nbsp; - &nbsp;Index B = <b>{{data.idxB}}</b>&nbsp; - &nbsp;Index C = <b>{{data.idxC}}</b>&nbsp; - &nbsp;Index D = <b>{{data.idxD}}</b> )</small></li>
  63.                 </ul>
  64.                 <small>Evolution des VoltAmpères: <small>(toutes les minutes)</small></small>
  65.                 <canvas id="nomCanvas" width="300" height="300"></canvas>
  66.                 <br/>
  67.                 <small>x = <small>{{x}}</small><font size="2"></font></small>
  68.                 <br/>
  69.                 <small><small><small>
  70.                 <br/>TéléInfo sur modem USBTIC
  71.                 <br/>(c) DomoEnergyTics
  72.                 </small></small></small>
  73.         </body>
  74. </html>

teleinfoCtrl.js :
Le code qui permet la récupération des données et leur mise à jour au fil de l’eau.

  1. function teleinfoCtrl($scope, $http, $timeout) {
  2.         $scope.x = 0;
  3.         $scope.draw = function() {
  4.                 if (contextCanvas!=null) {
  5.                         // -- coordonnees x,y courantes
  6.                         $scope.x += 1; // pas de 1 pixel a la fois
  7.                         if ($scope.x > canvas.width) {
  8.                                 decal = 200;
  9.                                 var imgData = contextCanvas.getImageData(decal, 0, canvas.width, canvas.height);
  10.                                 contextCanvas.putImageData(imgData,0,0);
  11.                                 $scope.x -= decal;
  12.                                 xo -= decal;
  13.                                 contextCanvas.fillStyle = "rgb(255,255,200)";
  14.                                 contextCanvas.fillRect (canvas.width - decal, 0, canvas.width, canvas.height);
  15.                                 drawGuides(decal);
  16.                         } // fin if x>canvas.width
  17.                         y = $scope.data.pa;  // recupere la valeur chiffrée à partir chaine recue
  18.                         y = y/maxVA*(canvas.height-1) // y mappée sur la hauteur canvas
  19.                         // -- trace la courbe --                  
  20.                         if ($scope.x == 1) {
  21.                                 yo = y;
  22.                         }
  23.                         contextCanvas.beginPath();
  24.                         contextCanvas.moveTo(xo, canvas.height-yo-1);
  25.                         contextCanvas.lineTo($scope.x, canvas.height-y-1);              
  26.                         contextCanvas.closePath();
  27.                         contextCanvas.stroke();
  28.                         xo = $scope.x;
  29.                         yo = y;
  30.                 }
  31.         }
  32.        
  33.         $scope.getJson = function() {
  34.                 $http({method: 'GET', url: '/teleinfo/json'}).
  35.                 success(function(data, status) {
  36.                         $scope.status = status;
  37.                         $scope.data = data;
  38.                         $scope.cumul = eval(data.idxA) + eval(data.idxB) + eval(data.idxC) + eval(data.idxD);
  39.                         $scope.draw();
  40.                 }).
  41.                 error(function(data, status) {
  42.                         $scope.status = '??';
  43.                 });
  44.                 $timeout($scope.getJson, 60200);
  45.         };
  46. }

Le fichier archive

Zip - 34.4 ko

Télécharger le fichier archive contenant l’ensemble des fichiers.