Suivi de la consommation électrique dans un navigateur web
par
popularité : 10%
Commençons par la fin et regardons ce que l’on souhaite obtenir dans un navigateur :
- 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.
"""
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 :
"""
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 :
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
A la fin du code sont instanciées les différentes classes et lancé le site :
# 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
- #!/usr/bin/python
- # -*- coding: utf-8 -*-
- import threading
- import serial
- import string
- import datetime
- from bottle import Bottle, run, template, static_file
- ################################################################################
- 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)
- ################################################################################
- 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])
- ################################################################################
- # Instancie un site web
- webApp = Bottle()
- ################################################################################
- @webApp.route('/edf')
- def edf():
- return static_file('web01.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
- @webApp.route('/edf/graph')
- def edfgraph():
- return static_file('web02.html', root='/home/pi/Python')
- ################################################################################
- 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')
web01.html :
Uniquement les infos textuelles
- <html>
- <head>
- <script language="javascript" type="text/javascript">
- window.onload = function() {
- requeteAjax(drawData);
- setInterval(function () {requeteAjax(drawData);}, 1000);
- }
- function requeteAjax(callback) {
- var xhr;
- if(window.XMLHttpRequest) { // Firefox
- xhr = new XMLHttpRequest();
- }
- else if(window.ActiveXObject) { // Internet Explorer
- xhr = new ActiveXObject("Microsoft.XMLHTTP");
- xhr.timeout = 1000;
- }
- xhr.onreadystatechange = function(fnCB) {
- if (xhr.readyState == 4 && xhr.status == 200) {
- callback(xhr.responseText);
- }
- };
- xhr.open("POST", "/edf/json", true);
- xhr.send(null);
- }
- function drawData(stringDataIn) {
- var obj = eval('(' + stringDataIn + ')');
- document.getElementById("index").innerHTML = obj.index;
- document.getElementById("kWh").innerHTML = obj.kWh;
- document.getElementById("pa").innerHTML = obj.pa;
- document.getElementById("ii").innerHTML = obj.ii;
- }
- </script>
- </head>
- <body>
- Compteur EDF :
- <br/>
- <br/>
- <br/>
- </body>
- </html>
web02.html :
Les infos textuelles plus le graphique
- <!DOCTYPE html>
- <html>
- <head>
- <script language="javascript" type="text/javascript">
- // variables / objets globaux
- var canvas= null;
- var contextCanvas = null;
- var delai=1000;
- var compt=0;
- var x=0;
- var y=0;
- var xo=0;
- var yo=0;
- window.onload = function() {
- canvas = document.getElementById("nomCanvas");
- canvas.width = 1600;
- canvas.height = 500;
- if (canvas.getContext){
- contextCanvas = canvas.getContext("2d");
- // carre de la taille du canvas
- contextCanvas.fillStyle = "rgb(255,255,200)";
- contextCanvas.fillRect (0, 0, canvas.width, canvas.height);
- // 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;
- contextCanvas.strokeStyle = "rgb(0,0,255)";
- // intervalle de rafraichissement
- setTimeout(function () {requeteAjax(drawData);}, delai);
- }
- else {
- window.alert("Canvas non disponible") ;
- }
- }
- function requeteAjax(callback) {
- var xhr;
- if(window.XMLHttpRequest) { // Firefox
- xhr = new XMLHttpRequest();
- }
- else if(window.ActiveXObject) { // Internet Explorer
- xhr = new ActiveXObject("Microsoft.XMLHTTP");
- xhr.timeout = 1000;
- }
- xhr.onreadystatechange = function(fnCB) {
- if (xhr.readyState == 4 && xhr.status == 200) {
- callback(xhr.responseText);
- }
- };
- xhr.open("POST", "/edf/json", true);
- xhr.send(null);
- }
- function drawData(stringDataIn) {
- var obj = eval('(' + stringDataIn + ')');
- document.getElementById("index").innerHTML = obj.index;
- document.getElementById("kWh").innerHTML = obj.kWh;
- document.getElementById("pa").innerHTML = obj.pa;
- document.getElementById("ii").innerHTML = obj.ii;
- if (contextCanvas!=null) {
- // -- coordonnees x,y courantes
- x = x + 1; // pas de 1 pixel à la fois
- if (x > canvas.width) {
- decal = 200;
- var imgData = contextCanvas.getImageData(decal, 0, canvas.width, canvas.height);
- contextCanvas.putImageData(imgData,0,0);
- x -= decal;
- xo -= decal;
- contextCanvas.fillStyle = "rgb(255,255,200)";
- contextCanvas.fillRect (canvas.width - decal, 0, canvas.width, canvas.height);
- }
- //y = Number(stringDataIn) // récupère la valeur chiffrée à partir chaine reçue
- y = obj.pa;
- y = y/15000*(canvas.height-1) // y mappée sur la hauteur canvas
- // -- trace courbe --
- // RAZ courbe si x==0
- if (x == 0) {
- contextCanvas.moveTo(x,y);
- contextCanvas.fillStyle = "rgb(255,255,200)";
- contextCanvas.fillRect (0, 0, canvas.width, canvas.height);
- xo = 0;
- }
- // sinon trace ligne point courant
- else {
- contextCanvas.beginPath();
- contextCanvas.moveTo(xo,canvas.height-yo-1);
- contextCanvas.lineTo(x,canvas.height-y-1);
- contextCanvas.closePath();
- contextCanvas.stroke();
- document.getElementById("abscisse").innerHTML = x;
- xo=x;
- yo=y;
- }
- }
- // réinitialise delai
- setTimeout(function () {requeteAjax(drawData);}, delai);
- }
- </script>
- </head>
- <body>
- Compteur EDF :
- <br/>
- <br/>
- <br/>
- <br/>
- <br/>
- <canvas id="nomCanvas" width="300" height="300"></canvas>
- <br/>
- <br/>
- Serveur RaspberryPi Python Bottle - TéléInfo sur carte arduino via port série.
- <br/>
- </body>
- </html>