Suivi de la consommation électrique
par
popularité : 8%

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 :
- Un RaspberryPi. Voir sa configuration dans l’article précédent : Installation & initialisation.
- Un module téléinfo, en l’occurence celui disponible sur le site Cartelectronic.
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"
- La classe "readFromSerial"
- La classe "sendToServer"
- Les fonctions du serveur web
- Le coeur du programme
Le code complet est présenté plus bas.
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.
- #!/usr/bin/python
- # -*- coding: utf-8 -*-
- """
- | Copyright finizi 2014 - Créé le 25/03/2014
- | www.DomoEnergyTICs.com
- |
- | Code sous licence GNU GPL :
- | This program is free software: you can redistribute it and/or modify
- | it under the terms of the GNU General Public License as published by
- | the Free Software Foundation, either version 3 of the License,
- | or any later version.
- | This program is distributed in the hope that it will be useful,
- | but WITHOUT ANY WARRANTY; without even the implied warranty of
- | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- | GNU General Public License for more details.
- | You should have received a copy of the GNU General Public License
- | along with this program. If not, see <http://www.gnu.org/licenses/>.
- """
- """
- Script python tournant sur le Raspberry Pi destiné à envoyer régulièrement
- le flux de données de la téléinformation.
- Fait aussi office de server web pour afficher sur le réseau local les
- informations de téléinformation.
- """
- import threading
- import time
- import serial
- import string
- import json
- import urllib2
- import bottle
- ######################################################################
- class TeleInfo(object):
- """
- Informations en cours issues du modem USBTIC de Téléinformation
- Puissance apparente et intensité sont moyennées sur 2 intervalles possibles:
- - Interval 'Normal': cas par exemple d'une interrogation locale toutes les 10 secondes
- - Interval long: utilisé pour le data loggin
- Le compteur EDF est un "Compteur Jaune", c'est à dire qu'il gère 4 compteurs
- selon l'heure du jour et le jour dans l'année
- """
- def __init__(self):
- self._indexA = 0 # index du compteur A
- self._indexB = 0 # index du compteur B
- self._indexC = 0 # index du compteur C
- self._indexD = 0 # index du compteur D
- self._pa = -1 # puissance apparente
- self._sumPa = -1 # somme des puissances apparentes durant l'intervalle
- self._sumPaL = -1 # somme des puissances apparentes durant l'intervalle long
- self._nbPa = 0 # nombre de puissances apparentes lues durant l'intervalle
- self._nbPaL = 0 # nombre de puissances apparentes lues durant l'intervalle long
- self._ts = "" # heure et minute fournie par la téléinfo (timestamp)
- pass
- def _get_indexA(self):
- return(self._indexA)
- def _set_indexA(self, newIndexA):
- self._indexA = newIndexA
- indexA = property( fget = _get_indexA,
- fset = _set_indexA,
- doc = "index A, en kW/h, affiché par le compteur." )
- def _get_indexB(self):
- return(self._indexB)
- def _set_indexB(self, newIndexB):
- self._indexB = newIndexB
- indexB = property( fget = _get_indexB,
- fset = _set_indexB,
- doc = "index B, en kW/h, affiché par le compteur." )
- def _get_indexC(self):
- return(self._indexC)
- def _set_indexC(self, newIndexC):
- self._indexC = newIndexC
- indexC = property( fget = _get_indexC,
- fset = _set_indexC,
- doc = "index C, en kW/h, affiché par le compteur." )
- def _get_indexD(self):
- return(self._indexD)
- def _set_indexD(self, newIndexD):
- self._indexD = newIndexD
- indexD = property( fget = _get_indexD,
- fset = _set_indexD,
- doc = "index D, en kW/h, affiché par le compteur." )
- def _get_pa(self):
- dispPa = self._pa
- if (self._nbPa > 0):
- dispPa = self._sumPa / self._nbPa
- self._sumPa = 0
- self._nbPa = 0
- return(dispPa)
- def _set_pa(self, newPa):
- self._pa = newPa
- self._sumPa += newPa
- self._nbPa += 1
- self._sumPaL += newPa
- self._nbPaL += 1
- pa = property( fget = _get_pa,
- fset = _set_pa,
- doc = "Puissance apparente moyenne de l'intervalle 'normal', en 'VoltAmpères'" )
- def _get_paL(self):
- dispPaL = self._pa
- if (self._nbPaL > 0):
- dispPaL = self._sumPaL / self._nbPaL
- self._sumPaL = 0
- self._nbPaL = 0
- return(dispPaL)
- paL = property( fget = _get_paL,
- doc = "Puissance apparente moyenne de l'interval long, en 'VoltAmpères'" )
- def _get_ts(self):
- return(self._ts)
- def _set_ts(self, newTs):
- self._ts = newTs
- ts = property( fget = _get_ts,
- fset = _set_ts,
- doc = "Heure et minute fournie par la téléinfo (timestamp)." )
- ######################################################################
- 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):
- self._OK = 0
- try:
- threading.Thread.__init__(self)
- self.setDaemon(True)
- self._ti = ti
- print 'Initialize serial'
- self._ser = serial.Serial( port='/dev/ttyUSB0',
- baudrate=1200,
- parity=serial.PARITY_EVEN,
- stopbits=serial.STOPBITS_ONE,
- bytesize=serial.SEVENBITS,
- timeout=1)
- print 'Serial initialized'
- self._OK = 1
- except:
- print 'Serial teleinfo error'
- def __del__(self):
- self._ser.close()
- def run(self):
- while True:
- try:
- tiLine = self._ser.readline()
- if (string.find(tiLine, 'JAUNE ') == 0):
- self._ti.ts = tiLine[6:11]
- if (tiLine[24:29].isdigit() == True):
- self._ti.pa = 10 * int(tiLine[24:29])
- if (string.find(tiLine, 'ENERG ') == 0):
- if (tiLine[6:12].isdigit() == True):
- self._ti.indexA = int(tiLine[6:12])
- if (tiLine[13:19].isdigit() == True):
- self._ti.indexB = int(tiLine[13:19])
- if (tiLine[20:26].isdigit() == True):
- self._ti.indexC = int(tiLine[20:26])
- if (tiLine[27:33].isdigit() == True):
- self._ti.indexD = int(tiLine[27:33])
- except:
- print 'Serial no data'
- time.sleep(60)
- ######################################################################
- class sendToServer(threading.Thread):
- """
- Envoie toutes les minutes au server les donnes de téléInformation.
- Le travaille se fait dans un thread différent.
- """
- def __init__(self, ti):
- self._OK = 0
- try:
- threading.Thread.__init__(self)
- self.setDaemon(True)
- self._ti = ti
- print 'Initialize server'
- self._OK = 1
- except:
- print 'server KO'
- def run(self):
- # Il faut utiliser un proxy pour sortir sur Internet
- proxy_info = { 'host' : 'prxy-xxxxxxx-xxxxxx.yy', 'port' : 9999 }
- proxy_support = urllib2.ProxyHandler({"http" : "http://%(host)s:%(port)d" % proxy_info})
- # On créé un opener utilisant ce handler:
- opener = urllib2.build_opener(proxy_support)
- # Puis on installe cet opener comme opener par défaut du module urllib2.
- urllib2.install_opener(opener)
- baseUrl = "http://Url.Du.Serveur.Internet/chargé/du/datalogging/"
- while True:
- try:
- # prepare l'URL
- if (ti.pa >= 0):
- 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)
- # envoi au server
- urllib2.urlopen(dataUrl)
- except:
- print 'Sender error'
- # dort une minute
- time.sleep(60)
- ######################################################################
- # Instancie un site web
- webApp = bottle.Bottle()
- @webApp.route('/favicon.ico', method='ANY')
- def get_favicon():
- return bottle.static_file('favicon.ico', root='static')
- @webApp.route('/static/<fileName>')
- def staticPages(fileName):
- return bottle.static_file(fileName, root='static')
- @webApp.route('/teleinfo')
- def edf():
- return bottle.static_file('teleinfo.html', root='static')
- @webApp.route('/teleinfo/json', method='ANY')
- def edfjson():
- dict = {'idxA':ti.indexA, 'idxB':ti.indexB, 'idxC':ti.indexC, 'idxD':ti.indexD, 'pa':ti.pa }
- return dict
- ######################################################################
- if __name__ == '__main__':
- # Instancie la classe de gestion de la TéléInfo
- ti = TeleInfo()
- # Instancie & démarre les threads de lecture de la TéléInfo
- tiReader = readFromSerial(ti)
- tiReader.start()
- # Instancie & démarre les threads d'envoie des données
- tiSender = sendToServer(ti)
- tiSender.start()
- # Lance le site web
- bottle.run(webApp, host='0.0.0.0', port=8080, server='cherrypy')
teleinfo.html :
Les infos textuelles plus le graphique
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
- <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
- <html ng-app>
- <head>
- <script language="javascript" type="text/javascript">
- // variables / objets globaux
- var canvas = null;
- var contextCanvas = null;
- var maxVA = 40000; // VolAmpères maxi tracé dans le canvas
- var x=0;
- var y=0;
- var xo=0;
- var yo=0;
- // Tracer les guides horizontaux
- function drawGuides(fx) {
- // Fond du Canvas
- contextCanvas.fillStyle = "rgb(255,255,200)";
- contextCanvas.fillRect (canvas.width - fx, 0, canvas.width, canvas.height);
- // Trace les guides tous les 2000 VoltAmpères
- savedStrokeStyle = contextCanvas.strokeStyle;
- contextCanvas.strokeStyle = "rgb(237,237,237)";
- ly = 0;
- while (ly/maxVA*(canvas.height-1) < canvas.height) {
- ly += 2000
- contextCanvas.beginPath();
- ty = ly/maxVA*(canvas.height-1);
- contextCanvas.moveTo(canvas.width-fx, canvas.height-ty-1);
- contextCanvas.lineTo(canvas.width-1, canvas.height-ty-1);
- contextCanvas.closePath();
- contextCanvas.stroke();
- }
- contextCanvas.strokeStyle = savedStrokeStyle;
- }
- // Initialisation du Canvas
- window.onload = function() {
- canvas = document.getElementById("nomCanvas");
- canvas.width = 1400;
- canvas.height = 500;
- if (canvas.getContext) {
- contextCanvas = canvas.getContext("2d");
- // position initiale - dessine 1 pixel
- contextCanvas.fillStyle = "rgb(0,0,255)";
- contextCanvas.fillRect(x, canvas.height-y-1, 1, 1);
- // parametres graphique
- contextCanvas.lineWidth=1;
- drawGuides(canvas.width);
- contextCanvas.strokeStyle = "rgb(0,0,255)";
- } // fin si canvas existe
- else {
- window.alert("Canvas non disponible") ;
- } // fin else
- } // fin onload
- </script>
- </head>
- <body ng-controller="teleinfoCtrl" ng-init="getJson()">
- <ul>
- </ul>
- <canvas id="nomCanvas" width="300" height="300"></canvas>
- <br/>
- <br/>
- <br/>TéléInfo sur modem USBTIC
- <br/>(c) DomoEnergyTics
- </body>
- </html>
teleinfoCtrl.js :
Le code qui permet la récupération des données et leur mise à jour au fil de l’eau.
- function teleinfoCtrl($scope, $http, $timeout) {
- $scope.x = 0;
- $scope.draw = function() {
- if (contextCanvas!=null) {
- // -- coordonnees x,y courantes
- $scope.x += 1; // pas de 1 pixel a la fois
- if ($scope.x > canvas.width) {
- decal = 200;
- var imgData = contextCanvas.getImageData(decal, 0, canvas.width, canvas.height);
- contextCanvas.putImageData(imgData,0,0);
- $scope.x -= decal;
- xo -= decal;
- contextCanvas.fillStyle = "rgb(255,255,200)";
- contextCanvas.fillRect (canvas.width - decal, 0, canvas.width, canvas.height);
- drawGuides(decal);
- } // fin if x>canvas.width
- y = $scope.data.pa; // recupere la valeur chiffrée à partir chaine recue
- y = y/maxVA*(canvas.height-1) // y mappée sur la hauteur canvas
- // -- trace la courbe --
- if ($scope.x == 1) {
- yo = y;
- }
- contextCanvas.beginPath();
- contextCanvas.moveTo(xo, canvas.height-yo-1);
- contextCanvas.lineTo($scope.x, canvas.height-y-1);
- contextCanvas.closePath();
- contextCanvas.stroke();
- xo = $scope.x;
- yo = y;
- }
- }
- $scope.getJson = function() {
- $http({method: 'GET', url: '/teleinfo/json'}).
- success(function(data, status) {
- $scope.status = status;
- $scope.data = data;
- $scope.cumul = eval(data.idxA) + eval(data.idxB) + eval(data.idxC) + eval(data.idxD);
- $scope.draw();
- }).
- error(function(data, status) {
- $scope.status = '??';
- });
- $timeout($scope.getJson, 60200);
- };
- }
Le fichier archive
Télécharger le fichier archive contenant l’ensemble des fichiers.