Projet station météo ESP8266 (Partie 5). Afficher des jauges et graphiques Google Charts • Domotique et objets connectés à faire soi-même

vbgdzy1nkxtcogvonah2-1033904

Code source

Maintenant que nous disposons d’un historique de mesures, nous allons pouvoir tracer des graphiques et des jauges sur l’interface HTML de notre mini station météo ESP8266 à l’aide de la librairie Google Charts. Il existe de très nombreuses librairies pour créer des graphiques et des gauges en Javascript / HTML5, celle développée par Google est très simple d’utilisation.

Projet actualisé le 29 juillet 2020

Google Charts est une librairie très riche qui propose 28 modèles différents (ligne, gauge, histogramme horizontal ou vertical, gant, camembert, bulle, carte, radar…) suffisante pour débuter et créer une belle interface. Nous verrons dans un prochain tutoriel d’autres librairies avec un affichage plus moderne.

Préparer les données pour l’histogramme en barre

Nous allons commencer par créer un petit histogramme qui nous permettra de voir l’évolution de la température moyenne (et l’humidité) durant les 7 dernières heures (la période d’enregistrement définie dans le tutoriel précédent). Sauf erreur de ma part, il n’y a pas de librairie pour faire de la classification de mesures sur Arduino (dites le moi dans les commentaires sinon).

On utilisera un petit algorithme rapide qui permet de calculer la température et l’humidité moyenne pour 7 classes. Chaque classe correspondra à une heure écoulée.

Cet algorithme n’est probablement pas le meilleur pour ça mais l’objectif de ce tutoriel est la mise en place de graphiques Google Charts et l’envoi des données depuis le code Arduino vers l’interface Web.

Comment ça marche ?

  1. On initialise deux tableaux statTemp et statHumi ayant comme valeur initiale -999 (ou toute autre valeur arbitraire).
  2. On calcul la taille de la classe sizeClass = sizeHist / nbClass
  3. On parcours le tableau des données (uniquement lorsqu’il est plein, il faudra optimiser ça). La variable k donne la position dans le tableau
  4. Si la cellule est vide (-999) alors on lui attribue la valeur courante. Sinon on calcul la valeur moyenne (valeur actuelle + valeur précédente / 2)
  5. On test si on doit changer de classe ( k + 1 ) > sizeClass * ( currentClass + 1 )
  6. On met à jour les deux tableaux dans le JSON bart et barh.
void calcStat(){
  float statTemp[7] = {-999,-999,-999,-999,-999,-999,-999};
  float statHumi[7] = {-999,-999,-999,-999,-999,-999,-999};
  int nbClass = 7;  // Nombre de classes - Number of classes                         
  int currentClass = 0;
  int sizeClass = sizeHist / nbClass;  // 2
  double temp;

  if ( hist_t.size() >= sizeHist ) {
    for ( int k = 0 ; k < sizeHist ; k++ ) {
      temp = root["t"][k];
      if ( statTemp[currentClass] == -999 ) {
        statTemp[ currentClass ] = temp;
      } else {
        statTemp[ currentClass ] = ( statTemp[ currentClass ] + temp ) / 2;
      }
      temp = root["h"][k];
      if ( statHumi[currentClass] == -999 ) {
        statHumi[ currentClass ] = temp;
      } else {
        statHumi[ currentClass ] = ( statHumi[ currentClass ] + temp ) / 2;
      }
         
      if ( ( k + 1 ) > sizeClass * ( currentClass + 1 ) ) {
        Serial.print("k ");Serial.print(k + 1);Serial.print(" Cellule statTemp = ");Serial.println(statTemp[ currentClass ]);
        currentClass++;
      } else {
        Serial.print("k ");Serial.print(k + 1);Serial.print(" < ");Serial.println(sizeClass * currentClass);
      }
    }
    
    // Pour la mise au point - For debug
    Serial.println("Histogramme Temperature"); 
    for ( int i = 0 ; i < nbClass ; i++ ) {
      Serial.print(statTemp[i]);Serial.print('|');
    }
    Serial.println("Histogramme Humidite "); 
    for ( int i = 0 ; i < nbClass ; i++ ) {
      Serial.print(statHumi[i]);Serial.print('|');
    }
    Serial.print("");
    // Met à jour le JSON - update JSON object
    if ( bart.size() == 0 ) {
      for ( int k = 0 ; k < nbClass ; k++ ) { 
        bart.add(statTemp[k]);
        barh.add(statHumi[k]);
      }  
    } else {
      for ( int k = 0 ; k < nbClass ; k++ ) { 
        bart.set(k, statTemp[k]);
        barh.set(k, statHumi[k]);
      }  
    }
  }
}

Comment ajouter des graphiques Google Charts à une interface Web

Passons maintenant aux choses sérieuses.

Comment fonctionne la librairie Google Charts

Google est décidément partout. La librairie Google Charts est une librairie qui existe depuis 2008. Ce n’est pas la librairie la plus belle qui existe mais elle est assez simple à mettre en oeuvre et surtout il est assez facile de créer des graphiques temps réel (ou du moins qu’on va mettre à jour très souvent).

Nous testerons d’autres librairies ultérieurement. La documentation (uniquement en anglais) et assez bien faite. Seul bémol, actuellement certains graphiques sont disponibles dans l’ancienne version et dans la nouvelle (Material Design). C’est assez facile de le repérer, par exemple pour tracer un graphique en ligne (Line Chart), on peut faire

new google.visualization.LineChart

ou pour la nouvelle version (Material Design)

new google.charts.Line

Toutes les options ne sont pas encore disponibles dans la version Material Design, ce qui rend assez fastidieux la mise au point. Si l’aspect esthétique n’est pas primordial pour vous, je vous conseille de rester sur l’ancienne version.

Code HTML

Pour charger la librairie, il suffit d’ajouter dans la section Head cette référence

script(src='https://www.gstatic.com/charts/loader.js')

Ajout d’un Panel Bootstrap

La classe panel de Bootstrap permet de présenter des données sous la forme d’une carte avec une entête (documentation Bootstrap). Nous allons nous en servir pour créer une entête contenant les mesures actuelles renvoyées par les capteurs (DHT22 et BMP180).

Le Panel est constitué d’une entête, classe panel-heading. Si on veut ajouter un titre, il faudra ajouter à la classe h1 (ou autre) la classe panel-title. Mais rien ne nous empêche de mettre d’autres choses dans le heading. Ici on va simplement ajouter une row qui sera découpée en 3 colonnes égales (col-md-4). Pour la rendre adaptable (responsive) sur les écrans de petite taille, on lui ajoute la classe col-xs-4 (par exemple). Dans chaque colonne, on place simplement un identifiant qui nous servira à afficher le contenu dès que des valeurs sont disponibles.

Ce qui donne le code Pug suivant

div.panel.panel-default
  div.panel-heading
    .row.panel-title 
      .col-xs-4.col-md-4
        #labelTemp
      .col-xs-4.col-md-4
        #labelHumi
      .col-xs-4.col-md-4
        #labelPa

Voici ce qu’on va obtenir

Ajout des graphiques (ligne, histogramme vertical, gauge) sur 2 colonnes

Les graphiques vont prendre place dans une autre div de classe panel-body. On va simplement ajouter 2 lignes découpées en 2 colonnes. Soit 4 cellules. Dans chaque cellule, on viendra y placer un Google Chart. Dan la code HTML, il n’y a rien a définir à part un style. Ici, on prend toute la largeur disponible (width: 100%) et on fixe une hauteur, par exemple height:300px. Pour la gauge, j’ai simplement ajouté une marge à gauche (margin-left:25%) car par défaut elle est collée sur le bord gauche.

Enfin vous avez du remarquer un titre h2 que l’on rendra visible s’il n’y a pas d’histogramme à afficher et inversement.

div.panel.body
  .row
    .col-xs-6.col-md-6
      .div#chartTemp(style="width: 100%; height: 300px;")
    .col-xs-6.col-md-6
      .div#chartPA(style="width: 100%; height: 300px;")
  .row
    .col-xs-6.col-md-6
      h2#zeroDataTemp.label.label-info Pas encore de données
      .div#barTemp(style="width: 100%; height: 300px;")
      .col-xs-6.col-md-6
    .div#gaugePA(style="width: 100%; height: 300px; margin-left: 25%")

Code Javascript

En fonction du type de graphique désiré, on doit charger des packages complémentaires en plus du corechart.

google.charts.load('current', {packages: ['corechart', 'line', 'bar', 'gauge']});

Ensuite on indique la fonction qui sera lancée dès que la page aura été chargée et que les ressources auront été récupérées et chargées.

google.charts.setOnLoadCallback(drawChart);

Pour créer un graphique, on doit déjà créer un objet en lui indiquant son type. On récupère sa destination avec la fonction document.getElementById (par exemple).

var chartTemp = new google.visualization.AreaChart(document.getElementById('chartTemp'));

Ensuite, il existe plusieurs méthodes pour attribuer des options et des données, en voici une. On créé un tableau de données.

dataChartTemp = new google.visualization.DataTable();

Puis on ajoute les colonnes qui contiendrons les valeurs du graphique. On doit indiquer le type de donnée pour chaque colonne. Il faut se reporter à la documentation pour connaitre les formats disponibles (data format) pour le graphique souhaité, ici par exemple pour un AreaChart. Ici le temps est de la forme timeofday (heure tout simplement !).

dataChartTemp.addColumn('timeofday', 'Temps');
dataChartTemp.addColumn('number', 'Température');
dataChartTemp.addColumn('number', 'Humidité');

On peut aussi définir des options, par exemple la position de la légende (position:”bottom” pour la placer en dessous, “none” pour désactiver son affichage), le libellé des axes, etc… Un exemple pour l’AreaChart

var options1 = {
    title: 'Température et humidité - DHT22',
    legend: 'bottom',
    series: {
      // Gives each series an axis name that matches the Y-axis below.
      0: {axis: 'temperature'},
      1: {axis: 'humidite'}
    },
    axes: {
      // Adds labels to each axis; they don't have to match the axis names.
      y: {
        temperature: {label: 'Température (°C)'},
        humidite: {label: 'Humidité (%)'}
      }
    }
  }

Maintenant, il ne reste plus qu’à créer une fonction chargée de récupérer des données depuis l’ESP8266 et les afficher à l’écran. Pour cela, on va simplement utiliser la fonction jquery $.getJSON déjà utilisée dans le tutoriel précédent. Pour éviter de gaspiller les ressources de l’ordinateur, smartphone ou tablette et ESP8266, on vérifie que le panneau des graphiques est bien actif avant de faire une actualisation

function updateGraphs(){     
  // Uniquement si le panneau des graphs est actif - only if chart panel is active
  if (tab_pane=='#tab_graphs' | firstStart ){
    firstStart = false;
    $.getJSON('/graph_temp.json', function(json){
      // Actualisation des graphiques - update charts
    }).fail(function(err){
      // Avertir d'une erreur - display error message
  })
}

Traitement du JSON envoyé par le Web Server de l’ESP8266

On va récupérer les données sous la forme d’un JSON. Par exemple

{
  "timestamp": [1485273937, 1485273938, 1485273939, 1485273940, 1485273941, 1485273942, 1485273943, 1485273944, 1485273945, 1485273946, 1485273947, 1485273948, 1485273949, 1485273950],
  "t": [23.3, 23.3, 23.3, 23.3, 23.3, 23.3, 23.3, 23.3, 23.3, 23.3, 23.3, 23.3, 23.3, 23.3],
  "h": [35.6, 35.6, 35.6, 35.6, 35.6, 35.5, 35.5, 35.4, 35.4, 35.5, 35.5, 35.5, 35.5, 35.5],
  "pa": [987.7, 987.7, 987.7, 987.8, 987.7, 987.7, 987.7, 987.7, 987.7, 987.8, 987.7, 987.7, 987.7, 987.7],
  "bart": [23.30, 23.30, 23.30, 23.30, 23.30, 23.30, 23.30],
  "barh": [35.60, 35.60, 35.50, 35.40, 35.50, 35.50, 35.50]
}

On va devoir faire une petite boucle qui parcours le tableau timestamp pour ajouter à chaque fois une nouvelle ligne au tableau des données du graphique. Par exemple ici on prépare les données pour le graphique température / humidité du DHT22. La première colonne doit contenir une heure. Pour cela on va utiliser la fonction javascript new Date qui est capable de convertir un timestamp unix en une date. Il faut multiplier par 1000 car la fonction attend des milli-secondes, le timestamp est en secondes dans ce cas. Enfin, on indique la date sous la forme d’un tableau [HH,MM,SS]. Désolé, mais faut faire avec l’API Google ! Le traitement du temps est toujours un problème finalement !

var _dataT;
for ( var i = 0; i < json.timestamp.length; i++ ) {
  var d = new Date(json.timestamp[i] * 1000);
  _dataT.push([
    [d.getHours(), d.getMinutes(), d.getSeconds()],
    json.t[i],
    json.h[i]
  ])
}

On ajoute ce bloc de valeurs d’un coup avec la fonction addRows. Attention, la fonction addRow existe aussi (pour une seule ligne).

dataChartTemp.addRows(_dataT);

Ici, j’ai choisi de ne pas conserver les anciennes valeurs, on va donc les supprimer avec la fonction removeRows qui prend comme paramètre l’index et le nombre de lignes à supprimer.

var nbRec = dataChartTemp.getNumberOfRows() - json.timestamp.length;
if ( dataChartTemp.getNumberOfRows() > json.timestamp.length ) {
  dataChartTemp.removeRows(0, nbRec );
  dataChartPA.removeRows(0, nbRec );
}

Affichage et actualisation automatique des graphiques

Tout est prêt, on peut demander au navigateur d’afficher le graphique chartTemp.

chartTemp.draw(dataChartTemp, options1);

On peut masquer un graphique s’il est vide et afficher un message d’information à la place. Il suffit de tester le nombre de ligne dans le tableau de données avec la méthode getNumberOfRows(). Ensuite un show() ou hide() sur l’élément concerné pour le rendre visible ou le masquer. Le tour est joué.

if ( dataBarTemp.getNumberOfRows() == 0 ) {
  $("#zeroDataTemp").show();
  $("#barTemp").hide();
} else {
  $("#zeroDataTemp").hide();
  $("#barTemp").show();
}

Il ne reste plus qu’à laisser au navigateur le soin d’actualiser régulièrement les graphiques en créant un timer à l’aide de la fonction setInterval(). On place le timer juste avant la fonction updateGraph().

setInterval(updateGraphs, 60000); //60000 MS == 1 minute

Code Arduino

On va ajouter un appel à la fonction sendHistory() lorsque le serveur Web intercepte un appel sur la page /graph_temp.json.

server.on("/graph_temp.json", sendHistory);

Toutes les mesures sont stockées dans un objet JSON à l’aide de la librairie ArduinoJSON.

Il est très simple d’en générer une chaine de caractères à l’aide de la fonction printTo. qui stocke le résultat dans un buffer. Il faudra donc disposer d’assez de mémoire sinon l’export sera impossible. On envoi au client le JSON sérialisé de manière classique avec la méthode server.send().

void sendHistory(){  
  root.printTo(json, sizeof(json));             // Export du JSON dans une chaine - Export JSON object as a string
  server.send(200, "application/json", json);   // Envoi l'historique au client Web - Send history data to the web client
  Serial.println("Historique envoye");   
}

Attention à rester raisonnable et ne pas demander une actualisation des graphiques chaque seconde. Avec peu de données, l’ESP8266 va encaisser, mais pour combien de temps… Pour connaitre la fréquence minimale à ne pas dépasser, regardez le temps pour obtenir une réponse avec les outils de développement d’un navigateur.

Code complet et final du projet de station météo

Voilà, il ne reste plus qu’à modifier le code précédent

Template Pug

Le Template Pug (ancien Jade) plus facile à comprendre, modifier, corriger que du code HTML.

html(charset='UTF-8')
    head
      meta( name='viewport')
      script(src='https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js')
      script(src='https://www.gstatic.com/charts/loader.js')
      script(src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js')
      script(src="http://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.js")
      link(rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.css")
      link(href='https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/superhero/bootstrap.min.css', rel='stylesheet' title="main")
      title Demo ESP8266 SPIFFS + Boostrap - www.projetsdiy.fr
    body
      .container-fluid
        h1 ESP8266 Web Server + SPIFFS + Bootstrap + Google Charts 
        ul#tab.nav.nav-tabs
          li.active
            a(href="#tab_mesures" data-toggle="tab") Mesures
          li
            a(href="#tab_graphs" data-toggle="tab") Graphiques
          li
            a(href="#tab_gpio" data-toggle="tab") GPIO  
          li
            a(href="#tab_configuration" data-toggle="tab") Configuration
        div.tab-content
          div#tab_mesures.tab-pane.fade.in.active         
            h2 Mini station météo (DHT22 + BMP180)
            ul.nav.nav-pills
              li.active
                a(href='#')
                  #temperature.span.badge.pull-right -
                  |  Température
              li
                a(href='#')
                  #humidite.span.badge.pull-right -
                  |  Humidité
              li
                a(href='#')
                  #pa.span.badge.pull-right -
                  |  Pression atmosphérique
            br
            table(id='table_mesures' data-toggle='table' data-show-colunns='true')
              thead
                tr
                  th(data-field='mesure' data-align='left' data-sortable='true' data-formatter='labelFormatter') Mesure
                  th(data-field='valeur' data-align='left' data-sortable='true' data-formatter='valueFormatter') Valeur
                  th(data-field='precedente' data-align='left' data-sortable='true' data-formatter='vpFormatter') Valeur Précédente

          div#tab_graphs.tab-pane.fade
            div.panel.panel-default
              div.panel-heading
                .row.panel-title 
                  .col-xs-4.col-md-4
                    #labelTemp
                  .col-xs-4.col-md-4
                    #labelHumi
                  .col-xs-4.col-md-4
                    #labelPa
                  
              div.panel.body
                .row
                  .col-xs-6.col-md-6
                    .div#chartTemp(style="width: 100%; height: 300px;")
                  .col-xs-6.col-md-6
                    .div#chartPA(style="width: 100%; height: 300px;")
                .row
                  .col-xs-6.col-md-6
                    h2#zeroDataTemp.label.label-info Pas encore de données
                    .div#barTemp(style="width: 100%; height: 300px;")
                  .col-xs-6.col-md-6
                    .div#gaugePA(style="width: 100%; height: 300px; margin-left: 25%")     
          div#tab_gpio.tab-pane.fade
            h2 GPIO
            .row
              .col-xs-4.col-md-4
                h4.text-left
                  | D5
                  #D5_etat.span.badge OFF
              .col-xs-4.col-md-4
                #D5_On.button.btn.btn-success.btn-lg(type='button') ON
              .col-xs-4.col-md-4
                #D5_Off.button.btn.btn-danger.btn-lg(type='button') OFF
            .row
              .col-xs-4.col-md-4
                h4.text-left
                  | D6
                  #D6_etat.span.badge OFF
              .col-xs-4.col-md-4
                #D6_On.button.btn.btn-success.btn-lg(type='button') ON
              .col-xs-4.col-md-4
                #D6_Off.button.btn.btn-danger.btn-lg(type='button') OFF
            .row
              .col-xs-4.col-md-4
                h4.text-left
                  | D7
                  #D7_etat.span.badge OFF
              .col-xs-4.col-md-4
                #D7_On.button.btn.btn-success.btn-lg(type='button') ON
              .col-xs-4.col-md-4
                #D7_Off.button.btn.btn-danger.btn-lg(type='button') OFF
            .row
              .col-xs-4.col-md-4
                h4.text-left
                  | D8
                  #D8_etat.span.badge OFF
              .col-xs-4.col-md-4
                #D8_On.button.btn.btn-success.btn-lg(type='button') ON
              .col-xs-4.col-md-4
                #D8_Off.button.btn.btn-danger.btn-lg(type='button') OFF
          div#tab_configuration.tab-pane.fade
            h2 Configuration        

            .btn-group
              button#labelTheme.btn.btn-default Theme
              button.btn.btn-default.dropdown-toggle(data-toggle='dropdown')
                span.caret
              ul.dropdown-menu
                li
                    a.change-style-menu-item(href='#' rel='bootstrap') Boostrap
                li
                    a.change-style-menu-item(href='#' rel='cerulean') Cerulean
                li
                    a.change-style-menu-item(href='#' rel='cosmo') Cosmo
                li
                    a.change-style-menu-item(href='#' rel='cyborg') Cyborg
                li
                    a.change-style-menu-item(href='#' rel='darkly') Darkly
                li
                    a.change-style-menu-item(href='#' rel='flatly') Flatly
                li
                    a.change-style-menu-item(href='#' rel='journal') Journal
                li
                    a.change-style-menu-item(href='#' rel='lumen') Lumen
                li
                    a.change-style-menu-item(href='#' rel='paper') Paper
                li
                    a.change-style-menu-item(href='#' rel='readable') Readable
                li
                    a.change-style-menu-item(href='#' rel='sandstone') Sandstone
                li
                    a.change-style-menu-item(href='#' rel='simplex') Simplex
                li
                    a.change-style-menu-item(href='#' rel='slate') Slate
                li
                    a.change-style-menu-item(href='#' rel='spacelab') Spacelab
                li
                    a.change-style-menu-item(href='#' rel='superhero') Superhero
                li
                    a.change-style-menu-item(href='#' rel='united') United
                li
                    a.change-style-menu-item(href='#' rel='yeti') Yeti  
        .row(style="position:absolute; bottom:0; width:100%")
          .col-xs-2.col-md-2
            img(src="img/logo.png" width="30" height="30")
          .col-xs-5.col-md-5
            p
              a(href='http://www.projetsdiy.fr') Version francaise : www.projetsdiy.fr
          .col-xs-5.col-md-5
            p
              a(href='http://www.diyprojects.io') English version : www.diyprojects.io
    
      //script(src='js/script.js')
    
      script().
        var Timer_UdpateMesures;
        var tab_pane;
        google.charts.load('current', {packages: ['corechart', 'line', 'bar', 'gauge']});
        google.charts.setOnLoadCallback(drawChart);
        
        function drawChart(){
          // https://developers.google.com/chart/interactive/docs/reference?csw=1#datatable-class
          var options1 = {
            title: 'Température et humidité - DHT22',
            legend: 'bottom',
            series: {
              // Gives each series an axis name that matches the Y-axis below.
              0: {axis: 'temperature'},
              1: {axis: 'humidite'}
            },
            axes: {
              // Adds labels to each axis; they don't have to match the axis names.
              y: {
                temperature: {label: 'Température (°C)'},
                humidite: {label: 'Humidité (%)'}
              }
            }
          }
          var options2 = {
            title: 'Pression Atmosphérique - BMP180',
            legend: {position: 'none'},
          }
          var optionsGauge = {           
            redFrom: 960, 
            redTo: 990,
             
            yellowFrom: 990, 
            yellowTo: 1030, 
             
            greenFrom: 1030, 
            greenTo: 1080, 
             
            minorTicks: 10,
             
            min: 960, 
            max: 1080, 
             
            animation: {
                duration: 400, 
                easing: 'out',
            },
          };
          // Objets graphiques - Charts objects
          var chartTemp = new google.visualization.AreaChart(document.getElementById('chartTemp'));
          var barTemp = new google.charts.Bar(document.getElementById('barTemp'));
          var chartPA = new google.visualization.AreaChart(document.getElementById('chartPA'));
          var gaugePA = new google.visualization.Gauge(document.getElementById('gaugePA'));
          // Données - Data
          dataGaugePA = new google.visualization.DataTable();
          dataChartTemp = new google.visualization.DataTable();
          dataBarTemp = new google.visualization.DataTable();
          dataChartPA = new google.visualization.DataTable();
          
          // Gauge Pression Atmospherique - Gauge Atmosph. pressure
          dataGaugePA.addColumn('string', 'Label');
          dataGaugePA.addColumn('number', 'Value');
          dataGaugePA.addRows(1);
          
          // Line chart temp/humidity
          dataChartTemp.addColumn('timeofday', 'Temps');
          dataChartTemp.addColumn('number', 'Température');
          dataChartTemp.addColumn('number', 'Humidité');
          
          // Bar temp/humidity
          dataBarTemp.addColumn('string', 'Moyennes');
          dataBarTemp.addColumn('number', 'Température');
          dataBarTemp.addColumn('number', 'Humidité');
          
          // Line Chart PA
          dataChartPA.addColumn('timeofday', 'Temps');
          dataChartPA.addColumn('number', 'Pression Atmosphérique');        
          
          // Force l'actualisation du graphique au 1er lancement - Force chart update first launch
          var firstStart = true;
          updateGraphs();
          // Actualise à intervalle régulier les graphiques - auto-update charts 
          setInterval(updateGraphs, 60000); //60000 MS == 1 minutes
          
          function updateGraphs(){     
            // Uniquement si le panneau des graphs est actif - only if chart panel is active
            if (tab_pane=='#tab_graphs' | firstStart ){
              firstStart = false;
              $.getJSON('/graph_temp.json', function(json){
                //console.log("Mesures envoyees : " + JSON.stringify(data) + "|" + data.t + "|" + data.h + "|" + data.pa) ;
                var _dataT = [];
                var _dataPA = [];
                var _dataBarTemp = [];
                var _dataBarPA = [];
                
                // Data line chart  
                for ( var i = 0; i < json.timestamp.length; i++ ) {
                  var d = new Date(json.timestamp[i] * 1000);
                  _dataT.push(
                    [
                      [d.getHours(), d.getMinutes(), d.getSeconds()],
                      json.t[i],
                      json.h[i]
                    ]
                  )
                  _dataPA.push(
                    [
                      [d.getHours(), d.getMinutes(), d.getSeconds()],
                      json.pa[i]
                    ]
                  )                
                }
                for ( var i = 0; i < json.bart.length; i++ ) {
                  _dataBarTemp.push(
                    [
                     i - 7 + "h",,
                     json.bart[i],
                     json.barh[i]
                    ]
                  ) 
                }  
        
                dataGaugePA.setValue(0, 0, 'mbar');
                dataGaugePA.setValue(0, 1, json.pa[0]);
                dataChartTemp.addRows(_dataT);
                dataChartPA.addRows(_dataPA);
                dataBarTemp.addRows(_dataBarTemp);
                
                // Efface les anciennes valeurs - Erase old data
                var nbRec = dataChartTemp.getNumberOfRows() - json.timestamp.length;
                if ( dataChartTemp.getNumberOfRows() > json.timestamp.length ) {
                  dataChartTemp.removeRows(0, nbRec );
                  dataChartPA.removeRows(0, nbRec );
                }
                nbRec = dataBarTemp.getNumberOfRows() - json.bart.length;
                if ( dataBarTemp.getNumberOfRows() > json.bart.length ) {
                  dataBarTemp.removeRows(0, nbRec );
                }
                // Masque ou affiche l'histogramme - hide or sho bar graph
                if ( dataBarTemp.getNumberOfRows() == 0 ) {
                  $("#zeroDataTemp").show();
                  $("#barTemp").hide();
                } else {
                  $("#zeroDataTemp").hide();
                  $("#barTemp").show();
                }
                // Affiche les graphiques - display charts
                gaugePA.draw(dataGaugePA,optionsGauge);
                chartTemp.draw(dataChartTemp, options1);
                barTemp.draw(dataBarTemp, options1);
                chartPA.draw(dataChartPA, options2);
              }).fail(function(err){
                console.log("err getJSON graph_temp.json "+JSON.stringify(err));
              });
            }
          }    
        }
                   
        $('a[data-toggle=\"tab\"]').on('shown.bs.tab', function (e) {   
          //On supprime tous les timers lorsqu'on change d'onglet
          clearTimeout(Timer_UdpateMesures);  
          tab_pane = $(e.target).attr("href")  
          console.log('activated ' + tab_pane );  

          // IE10, Firefox, Chrome, etc.
          if (history.pushState) 
            window.history.pushState(null, null, tab_pane);
          else 
            window.location.hash = tab_pane;
          
          if (tab_pane=='#tab_mesures')  {
            $('#table_mesures').bootstrapTable('refresh',{silent:true, url:'/tabmesures.json'}); 
          }  
        });
        
        // Créé un timer qui actualise les données régulièrement - Create a timer than update data every n secondes
        $('#tab_mesures').on('load-success.bs.table',function (e,data){
          console.log("tab_mesures loaded");
          if ($('.nav-tabs .active > a').attr('href')=='#tab_mesures') {
            Timer_UdpateMesures=setTimeout(function(){
              $('#table_mesures').bootstrapTable('refresh',{silent: true, showLoading: false, url: '/tabmesures.json'});
              updateMesures();
            },10000);
          }                 
        });   
            
        function updateMesures(){
          $.getJSON('/mesures.json', function(data){
            //console.log("Mesures envoyees : " + JSON.stringify(data) + "|" + data.t + "|" + data.h + "|" + data.pa) ;
            $('#temperature').html(data.t);
            $('#humidite').html(data.h);
            $('#pa').html(data.pa); 
          }).fail(function(err){
            console.log("err getJSON mesures.json "+JSON.stringify(err));
          });
        };

        function labelFormatter(value, row){
          var label = "";
          if ( value === "Température" ) {
            label = value + "";
            $("#labelTemp").html(" " + value + " " + " " + row.valeur + row.unite + "");
          } else if ( value === "Humidité" ) {
            label = value + "";
            $("#labelHumi").html(" " + value + " " + " " + row.valeur + row.unite + "");
          } else if ( value === "Pression Atmosphérique" ) {
            label = value + "";
            $("#labelPa").html(" " + value + " " + " " + row.valeur + row.unite + "");
          } else {
            label = value;
          } 
          return label;
        }
        function valueFormatter(value, row){
          //console.log("valueFormatter");
          var label = "";
          if ( row.valeur > row.precedente ) {
            label = value + row.unite + "";
          } else { 
            label = value + row.unite + "";
          }
          return label;
        }
        function vpFormatter(value, row){
          //console.log("valueFormatter");
          var label = "";
          if ( row.valeur > row.precedente ) {
            label = value + row.unite
          } else { 
            label = value + row.unite
          }
          return label;
        }  
        
        // Commandes sur le GPIO - GPIO change
        $('#D5_On').click(function(){ setBouton('D5','1'); });
        $('#D5_Off').click(function(){ setBouton('D5','0'); });
        $('#D6_On').click(function(){ setBouton('D6','1'); });
        $('#D6_Off').click(function(){ setBouton('D6','0'); });
        $('#D7_On').click(function(){ setBouton('D7','1'); });
        $('#D7_Off').click(function(){ setBouton('D7','0'); });
        $('#D8_On').click(function(){ setBouton('D8','1'); });
        $('#D8_Off').click(function(){ setBouton('D8','0'); });
  
        function setBouton(id, etat){
          $.post("gpio?id=" + id + "&etat=" + etat).done(function(data){
            //console.log("Retour setBouton " + JSON.stringify(data)); 
            var id_gpio = "#" + id + "_etat";
            //console.log(data);
            if ( data.success === "1" | data.success === 1 ) {
              if ( data.etat === "1" ) {
                $(id_gpio).html("ON");
              } else {
                $(id_gpio).html("OFF");
              }  
            } else {
              $(id_gpio).html('!');
            }      
          }).fail(function(err){
            console.log("err setButton " + JSON.stringify(err));
          });
        } 
        
        // Changement du theme - Change current theme
        // Adapté de - Adapted from : https://wdtz.org/bootswatch-theme-selector.html
        var supports_storage = supports_html5_storage();
        if (supports_storage) {
          var theme = localStorage.theme;
          console.log("Recharge le theme " + theme);
          if (theme) {
            set_theme(get_themeUrl(theme));
          }
        }
        
        // Nouveau theme sélectionne - New theme selected
        jQuery(function($){
          $('body').on('click', '.change-style-menu-item', function() {
            var theme_name = $(this).attr('rel');
            console.log("Change theme " + theme_name);
            var theme_url = get_themeUrl(theme_name);
            console.log("URL theme : " + theme_url);
            set_theme(theme_url);
          });
        });
        // Recupere l'adresse du theme - Get theme URL
        function get_themeUrl(theme_name){
          $('#labelTheme').html("Thème : " + theme_name);
          var url_theme = "";
          if ( theme_name === "bootstrap" ) {
            url_theme = "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css";
          } else {
            url_theme = "https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/" + theme_name + "/bootstrap.min.css";
          }
          if (supports_storage) {
            // Enregistre le theme sélectionné en local - save into the local database the selected theme
            localStorage.theme = theme_name;
          }
          return url_theme;
        }
        // Applique le thème - Apply theme
        function set_theme(theme_url) {
          $('link[title="main"]').attr('href', theme_url);
        }
        // Stockage local disponible ? - local storage available ?
        function supports_html5_storage(){
          try {
            return 'localStorage' in window && window['localStorage'] !== null;
          } catch (e) {
            return false;
          }
        }

Le code HTML généré à partir du template Pug ci-dessus.


  
    
    
    
    
    
    
    
    Demo ESP8266 SPIFFS + Boostrap - www.projetsdiy.fr
  
  
    
      

ESP8266 Web Server + SPIFFS + Bootstrap + Google Charts

      
      
                
          

Mini station météo (DHT22 + BMP180)

          

Mesure Valeur Valeur Précédente

Pas encore de données

GPIO

D5 OFF

ON OFF

D6 OFF

ON OFF

D7 OFF

ON OFF

D8 OFF

ON OFF

Configuration

Theme

Version francaise : www.projetsdiy.fr

English version : www.diyprojects.io

var Timer_UdpateMesures; var tab_pane; google.charts.load(‘current’, {packages: [‘corechart’, ‘line’, ‘bar’, ‘gauge’]}); google.charts.setOnLoadCallback(drawChart); function drawChart(){ // https://developers.google.com/chart/interactive/docs/reference?csw=1#datatable-class var options1 = { title: ‘Température et humidité – DHT22’, legend: ‘bottom’, series: { // Gives each series an axis name that matches the Y-axis below. 0: {axis: ‘temperature’}, 1: {axis: ‘humidite’} }, axes: { // Adds labels to each axis; they don’t have to match the axis names. y: { temperature: {label: ‘Température (°C)’}, humidite: {label: ‘Humidité (%)’} } } } var options2 = { title: ‘Pression Atmosphérique – BMP180’, legend: {position: ‘none’}, } var optionsGauge = { redFrom: 960, redTo: 990, yellowFrom: 990, yellowTo: 1030, greenFrom: 1030, greenTo: 1080, minorTicks: 10, min: 960, max: 1080, animation: { duration: 400, easing: ‘out’, }, }; // Objets graphiques – Charts objects var chartTemp = new google.visualization.AreaChart(document.getElementById(‘chartTemp’)); var barTemp = new google.charts.Bar(document.getElementById(‘barTemp’)); var chartPA = new google.visualization.AreaChart(document.getElementById(‘chartPA’)); var gaugePA = new google.visualization.Gauge(document.getElementById(‘gaugePA’)); // Données – Data dataGaugePA = new google.visualization.DataTable(); dataChartTemp = new google.visualization.DataTable(); dataBarTemp = new google.visualization.DataTable(); dataChartPA = new google.visualization.DataTable(); // Gauge Pression Atmospherique – Gauge Atmosph. pressure dataGaugePA.addColumn(‘string’, ‘Label’); dataGaugePA.addColumn(‘number’, ‘Value’); dataGaugePA.addRows(1); // Line chart temp/humidity dataChartTemp.addColumn(‘timeofday’, ‘Temps’); dataChartTemp.addColumn(‘number’, ‘Température’); dataChartTemp.addColumn(‘number’, ‘Humidité’); // Bar temp/humidity dataBarTemp.addColumn(‘string’, ‘Moyennes’); dataBarTemp.addColumn(‘number’, ‘Température’); dataBarTemp.addColumn(‘number’, ‘Humidité’); // Line Chart PA dataChartPA.addColumn(‘timeofday’, ‘Temps’); dataChartPA.addColumn(‘number’, ‘Pression Atmosphérique’); // Force l’actualisation du graphique au 1er lancement – Force chart update first launch var firstStart = true; updateGraphs(); // Actualise à intervalle régulier les graphiques – auto-update charts setInterval(updateGraphs, 60000); //60000 MS == 1 minutes function updateGraphs(){ // Uniquement si le panneau des graphs est actif – only if chart panel is active if (tab_pane==’#tab_graphs’ | firstStart ){ firstStart = false; $.getJSON(‘/graph_temp.json’, function(json){ //console.log(« Mesures envoyees :  » + JSON.stringify(data) + « | » + data.t + « | » + data.h + « | » + data.pa) ; var _dataT = []; var _dataPA = []; var _dataBarTemp = []; var _dataBarPA = []; // Data line chart for ( var i = 0; i < json.timestamp.length; i++ ) { var d = new Date(json.timestamp[i] * 1000); _dataT.push( [ [d.getHours(), d.getMinutes(), d.getSeconds()], json.t[i], json.h[i] ] ) _dataPA.push( [ [d.getHours(), d.getMinutes(), d.getSeconds()], json.pa[i] ] ) } for ( var i = 0; i < json.bart.length; i++ ) { _dataBarTemp.push( [ i – 7 + « h »,, json.bart[i], json.barh[i] ] ) } dataGaugePA.setValue(0, 0, ‘mbar’); dataGaugePA.setValue(0, 1, json.pa[0]); dataChartTemp.addRows(_dataT); dataChartPA.addRows(_dataPA); dataBarTemp.addRows(_dataBarTemp); // Efface les anciennes valeurs – Erase old data var nbRec = dataChartTemp.getNumberOfRows() – json.timestamp.length; if ( dataChartTemp.getNumberOfRows() > json.timestamp.length ) { dataChartTemp.removeRows(0, nbRec ); dataChartPA.removeRows(0, nbRec ); } nbRec = dataBarTemp.getNumberOfRows() – json.bart.length; if ( dataBarTemp.getNumberOfRows() > json.bart.length ) { dataBarTemp.removeRows(0, nbRec ); } // Masque ou affiche l’histogramme – hide or sho bar graph if ( dataBarTemp.getNumberOfRows() == 0 ) { $(« #zeroDataTemp »).show(); $(« #barTemp »).hide(); } else { $(« #zeroDataTemp »).hide(); $(« #barTemp »).show(); } // Affiche les graphiques – display charts gaugePA.draw(dataGaugePA,optionsGauge); chartTemp.draw(dataChartTemp, options1); barTemp.draw(dataBarTemp, options1); chartPA.draw(dataChartPA, options2); }).fail(function(err){ console.log(« err getJSON graph_temp.json « +JSON.stringify(err)); }); } } } $(‘a[data-toggle=\ »tab\ »]’).on(‘shown.bs.tab’, function (e) { //On supprime tous les timers lorsqu’on change d’onglet clearTimeout(Timer_UdpateMesures); tab_pane = $(e.target).attr(« href ») console.log(‘activated ‘ + tab_pane ); // IE10, Firefox, Chrome, etc. if (history.pushState) window.history.pushState(null, null, tab_pane); else window.location.hash = tab_pane; if (tab_pane==’#tab_mesures’) { $(‘#table_mesures’).bootstrapTable(‘refresh’,{silent:true, url:’/tabmesures.json’}); } }); // Créé un timer qui actualise les données régulièrement – Create a timer than update data every n secondes $(‘#tab_mesures’).on(‘load-success.bs.table’,function (e,data){ console.log(« tab_mesures loaded »); if ($(‘.nav-tabs .active > a’).attr(‘href’)==’#tab_mesures’) { Timer_UdpateMesures=setTimeout(function(){ $(‘#table_mesures’).bootstrapTable(‘refresh’,{silent: true, showLoading: false, url: ‘/tabmesures.json’}); updateMesures(); },10000); } }); function updateMesures(){ $.getJSON(‘/mesures.json’, function(data){ //console.log(« Mesures envoyees :  » + JSON.stringify(data) + « | » + data.t + « | » + data.h + « | » + data.pa) ; $(‘#temperature’).html(data.t); $(‘#humidite’).html(data.h); $(‘#pa’).html(data.pa); }).fail(function(err){ console.log(« err getJSON mesures.json « +JSON.stringify(err)); }); }; function labelFormatter(value, row){ var label = «  »; if ( value === « Température » ) { label = value + «  »; $(« #labelTemp »).html( »  » + value +  »  » +  »  » + row.valeur + row.unite + «  »); } else if ( value === « Humidité » ) { label = value + «  »; $(« #labelHumi »).html( »  » + value +  »  » +  »  » + row.valeur + row.unite + «  »); } else if ( value === « Pression Atmosphérique » ) { label = value + «  »; $(« #labelPa »).html( »  » + value +  »  » +  »  » + row.valeur + row.unite + «  »); } else { label = value; } return label; } function valueFormatter(value, row){ //console.log(« valueFormatter »); var label = «  »; if ( row.valeur > row.precedente ) { label = value + row.unite + «  »; } else { label = value + row.unite + «  »; } return label; } function vpFormatter(value, row){ //console.log(« valueFormatter »); var label = «  »; if ( row.valeur > row.precedente ) { label = value + row.unite } else { label = value + row.unite } return label; } // Commandes sur le GPIO – GPIO change $(‘#D5_On’).click(function(){ setBouton(‘D5′,’1’); }); $(‘#D5_Off’).click(function(){ setBouton(‘D5′,’0’); }); $(‘#D6_On’).click(function(){ setBouton(‘D6′,’1’); }); $(‘#D6_Off’).click(function(){ setBouton(‘D6′,’0’); }); $(‘#D7_On’).click(function(){ setBouton(‘D7′,’1’); }); $(‘#D7_Off’).click(function(){ setBouton(‘D7′,’0’); }); $(‘#D8_On’).click(function(){ setBouton(‘D8′,’1’); }); $(‘#D8_Off’).click(function(){ setBouton(‘D8′,’0’); }); function setBouton(id, etat){ $.post(« gpio?id= » + id + « &etat= » + etat).done(function(data){ //console.log(« Retour setBouton  » + JSON.stringify(data)); var id_gpio = « # » + id + « _etat »; //console.log(data); if ( data.success === « 1 » | data.success === 1 ) { if ( data.etat === « 1 » ) { $(id_gpio).html(« ON »); } else { $(id_gpio).html(« OFF »); } } else { $(id_gpio).html(‘!’); } }).fail(function(err){ console.log(« err setButton  » + JSON.stringify(err)); }); } // Changement du theme – Change current theme // Adapté de – Adapted from : https://wdtz.org/bootswatch-theme-selector.html var supports_storage = supports_html5_storage(); if (supports_storage) { var theme = localStorage.theme; console.log(« Recharge le theme  » + theme); if (theme) { set_theme(get_themeUrl(theme)); } } // Nouveau theme sélectionne – New theme selected jQuery(function($){ $(‘body’).on(‘click’, ‘.change-style-menu-item’, function() { var theme_name = $(this).attr(‘rel’); console.log(« Change theme  » + theme_name); var theme_url = get_themeUrl(theme_name); console.log(« URL theme :  » + theme_url); set_theme(theme_url); }); }); // Recupere l’adresse du theme – Get theme URL function get_themeUrl(theme_name){ $(‘#labelTheme’).html(« Thème :  » + theme_name); var url_theme = «  »; if ( theme_name === « bootstrap » ) { url_theme = « https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css »; } else { url_theme = « https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/ » + theme_name + « /bootstrap.min.css »; } if (supports_storage) { // Enregistre le theme sélectionné en local – save into the local database the selected theme localStorage.theme = theme_name; } return url_theme; } // Applique le thème – Apply theme function set_theme(theme_url) { $(‘link[title= »main »]’).attr(‘href’, theme_url); } // Stockage local disponible ? – local storage available ? function supports_html5_storage(){ try { return ‘localStorage’ in window && window[‘localStorage’] !== null; } catch (e) { return false; } }

Et le code Arduino à téléverser dans l’ESP8266. Le code est prévu pour une LoLin (Wemos) D1 Mini équipée d’un Shield DHT22 ainsi qu’un BMP180 sur bus I2C.

/*
 * ESP8266 + DHT22 + BMP180 + BOOTSTRAP + SPIFFS + GOOGLE CHARTS
 * Copyright (C) 2017 http://www.projetsdiy.fr - http://www.diyprojects.io
 * 
 * Part 5 : 
 *  - Create History based ArduinoJSON
 *  - Prepare the histogram graph data for the average temperature and humidity measurements
 *  - Add Google Charts to the web page
 *  - Full tutorial here : http://www.diyprojects.io/esp8266-web-server-part-5-add-google-charts-gauges-and-charts/
 *  
 *  Partie 5 :
 *  - Enregistre un historique de mesure à l'aide d'ArduinoJSON
 *  - Prepare les données du graphique en histogramme des mesures moyennes de température et d'humidité
 *  - Ajout des graphiques Google Charts à la page web
 *  - Tutoriel complet :  http://www.projetsdiy.fr/esp8266-web-serveur-partie5-gauges-graphiques-google-charts/
 *  
 *
 * 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
 * (at your option) 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 .
 */
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define ssid      "yourSSID"        // WiFi SSID
#define password  "yourPASSWORD"    // WiFi password
#define DHTTYPE   DHT22             // DHT type (DHT11, DHT22)
#define DHTPIN    D4                // Broche du DHT / DHT Pin
#define HISTORY_FILE "/history.json"
const uint8_t GPIOPIN[4] = {D5,D6,D7,D8};  // Led
float   t = 0 ;
float   h = 0 ;
float   pa = 0;
int     sizeHist = 84 ;        // Taille historique (7h x 12pts) - History size 

const long intervalHist = 1000 * 60 * 5;  // 5 mesures / heure - 5 measures / hours
unsigned long previousMillis = intervalHist;  // Dernier point enregistré dans l'historique - time of last point added

// Création des objets / create Objects
DHT dht(DHTPIN, DHTTYPE);
Adafruit_BMP085 bmp;
ESP8266WebServer server ( 80 );

StaticJsonBuffer jsonBuffer;                 // Buffer static contenant le JSON courant - Current JSON static buffer
JsonObject& root = jsonBuffer.createObject();
JsonArray& timestamp = root.createNestedArray("timestamp");
JsonArray& hist_t = root.createNestedArray("t");
JsonArray& hist_h = root.createNestedArray("h");
JsonArray& hist_pa = root.createNestedArray("pa");
JsonArray& bart = root.createNestedArray("bart");   // Clé historgramme (temp/humidité) - Key histogramm (temp/humidity)
JsonArray& barh = root.createNestedArray("barh");   // Clé historgramme (temp/humidité) - Key histogramm (temp/humidity)

char json[10000];                                   // Buffer pour export du JSON - JSON export buffer

void updateGpio(){
  String gpio = server.arg("id");
  String etat = server.arg("etat");
  String success = "1";
  int pin = D5;
 if ( gpio == "D5" ) {
      pin = D5;
 } else if ( gpio == "D7" ) {
     pin = D7;
 } else if ( gpio == "D8" ) {
     pin = D8;  
 } else {   
      pin = D5;
  }
  Serial.println(pin);
  if ( etat == "1" ) {
    digitalWrite(pin, HIGH);
  } else if ( etat == "0" ) {
    digitalWrite(pin, LOW);
  } else {
    success = "1";
    Serial.println("Err Led Value");
  }
  
  String json = "{\"gpio\":\"" + String(gpio) + "\",";
  json += "\"etat\":\"" + String(etat) + "\",";
  json += "\"success\":\"" + String(success) + "\"}";
    
  server.send(200, "application/json", json);
  Serial.println("GPIO updated");
}

void sendMesures() {
  String json = "{\"t\":\"" + String(t) + "\",";
  json += "\"h\":\"" + String(h) + "\",";
  json += "\"pa\":\"" + String(pa) + "\"}";

  server.send(200, "application/json", json);
  Serial.println("Send measures");
}

void calcStat(){
  float statTemp[7] = {-999,-999,-999,-999,-999,-999,-999};
  float statHumi[7] = {-999,-999,-999,-999,-999,-999,-999};
  int nbClass = 7;  // Nombre de classes - Number of classes                         
  int currentClass = 0;
  int sizeClass = hist_t.size() / nbClass;  // 2
  double temp;
  //
  if ( hist_t.size() >= sizeHist ) {
    //Serial.print("taille classe ");Serial.println(sizeClass);
    //Serial.print("taille historique ");Serial.println(hist_t.size());
    for ( int k = 0 ; k < hist_t.size() ; k++ ) {
      temp = root["t"][k];
      if ( statTemp[currentClass] == -999 ) {
        statTemp[ currentClass ] = temp;
      } else {
        statTemp[ currentClass ] = ( statTemp[ currentClass ] + temp ) / 2;
      }
      temp = root["h"][k];
      if ( statHumi[currentClass] == -999 ) {
        statHumi[ currentClass ] = temp;
      } else {
        statHumi[ currentClass ] = ( statHumi[ currentClass ] + temp ) / 2;
      }
         
      if ( ( k + 1 ) > sizeClass * ( currentClass + 1 ) ) {
        //Serial.print("k ");Serial.print(k + 1);Serial.print(" Cellule statTemp = ");Serial.println(statTemp[ currentClass ]);
        currentClass++;
      } else {
        //Serial.print("k ");Serial.print(k + 1);Serial.print(" < ");Serial.println(sizeClass * currentClass);
      }
    }
    
    Serial.println("Histogram - Temperature"); 
    for ( int i = 0 ; i < nbClass ; i++ ) {
      Serial.print(statTemp[i]);Serial.print('|');
    }
    Serial.println("Histogram - Humidity "); 
    for ( int i = 0 ; i < nbClass ; i++ ) {
      Serial.print(statHumi[i]);Serial.print('|');
    }
    Serial.print("");
    if ( bart.size() == 0 ) {
      for ( int k = 0 ; k < nbClass ; k++ ) { 
        bart.add(statTemp[k]);
        barh.add(statHumi[k]);
      }  
    } else {
      for ( int k = 0 ; k < nbClass ; k++ ) { 
        bart.set(k, statTemp[k]);
        barh.set(k, statHumi[k]);
      }  
    }
  }
}

void sendTabMesures() {
  double temp = root["t"][0];      // Récupère la plus ancienne mesure (temperature) - get oldest record (temperature)
  String json = "[";
  json += "{\"mesure\":\"Température\",\"valeur\":\"" + String(t) + "\",\"unite\":\"°C\",\"glyph\":\"glyphicon-indent-left\",\"precedente\":\"" + String(temp) + "\"},";
  temp = root["h"][0];             // Récupère la plus ancienne mesure (humidite) - get oldest record (humidity)
  json += "{\"mesure\":\"Humidité\",\"valeur\":\"" + String(h) + "\",\"unite\":\"%\",\"glyph\":\"glyphicon-tint\",\"precedente\":\"" + String(temp) + "\"},";
  temp = root["pa"][0];             // Récupère la plus ancienne mesure (pression atmospherique) - get oldest record (Atmospheric Pressure)
  json += "{\"mesure\":\"Pression Atmosphérique\",\"valeur\":\"" + String(pa) + "\",\"unite\":\"mbar\",\"glyph\":\"glyphicon-dashboard\",\"precedente\":\"" + String(temp) + "\"}";
  json += "]";
  server.send(200, "application/json", json);
  Serial.println("Send data tab");
}

void sendHistory(){  
  root.printTo(json, sizeof(json));             // Export du JSON dans une chaine - Export JSON object as a string
  server.send(200, "application/json", json);   // Envoi l'historique au client Web - Send history data to the web client
  Serial.println("Send History");   
}

void loadHistory(){
  File file = SPIFFS.open(HISTORY_FILE, "r");
  if (!file){
    Serial.println("Aucun historique existe - No History Exist");
  } else {
    size_t size = file.size();
    if ( size == 0 ) {
      Serial.println("Fichier historique vide - History file empty !");
    } else {
      std::unique_ptr buf (new char[size]);
      file.readBytes(buf.get(), size);
      JsonObject& root = jsonBuffer.parseObject(buf.get());
      if (!root.success()) {
        Serial.println("Impossible de lire le JSON - Impossible to read JSON file");
      } else {
        Serial.println("Historique charge - History loaded");
        root.prettyPrintTo(Serial);  
      }
    }
    file.close();
  }
}

void saveHistory(){
  Serial.println("Save History");            
  File historyFile = SPIFFS.open(HISTORY_FILE, "w");
  root.printTo(historyFile); // Exporte et enregistre le JSON dans la zone SPIFFS - Export and save JSON object to SPIFFS area
  historyFile.close();  
}

void setup() {
  NTP.onNTPSyncEvent([](NTPSyncEvent_t error) {
    if (error) {
      Serial.print("Time Sync error: ");
      if (error == noResponse)
        Serial.println("NTP server not reachable");
      else if (error == invalidAddress)
        Serial.println("Invalid NTP server address");
      }
    else {
      Serial.print("Got NTP time: ");
      Serial.println(NTP.getTimeDateString(NTP.getLastNTPSync()));
    }
  });
  // Serveur NTP, decalage horaire, heure été - NTP Server, time offset, daylight 
  NTP.begin("pool.ntp.org", 0, true); 
  NTP.setInterval(60000);
  delay(500);
     
  for ( int x = 0 ; x < 5 ; x++ ) {
    pinMode(GPIOPIN[x], OUTPUT);
  }
  
  Serial.begin ( 115200 );
  // Initialisation du BMP180 / Init BMP180
  if ( !bmp.begin() ) {
    Serial.println("BMP180 KO!");
    while (1);
  } else {
    Serial.println("BMP180 OK");
  }

  WiFi.begin ( ssid, password );
  int tentativeWiFi = 0;
  // Attente de la connexion au réseau WiFi / Wait for connection
  while ( WiFi.status() != WL_CONNECTED ) {
    delay ( 500 ); Serial.print ( "." );
    tentativeWiFi++;
    if ( tentativeWiFi > 20 ) {
      ESP.reset();
      while(true)
        delay(1);
    }
  }
  // Connexion WiFi établie / WiFi connexion is OK
  Serial.println ( "" );
  Serial.print ( "Connected to " ); Serial.println ( ssid );
  Serial.print ( "IP address: " ); Serial.println ( WiFi.localIP() );
  
  if (!SPIFFS.begin()) {
    Serial.println("SPIFFS Mount failed");        // Problème avec le stockage SPIFFS - Serious problem with SPIFFS 
  } else { 
    Serial.println("SPIFFS Mount succesfull");
    loadHistory();
  }
  delay(50);
  
  server.on("/tabmesures.json", sendTabMesures);
  server.on("/mesures.json", sendMesures);
  server.on("/gpio", updateGpio);
  server.on("/graph_temp.json", sendHistory);

  server.serveStatic("/js", SPIFFS, "/js");
  server.serveStatic("/css", SPIFFS, "/css");
  server.serveStatic("/img", SPIFFS, "/img");
  server.serveStatic("/", SPIFFS, "/index.html");

  server.begin();
  Serial.println ( "HTTP server started" );

  Serial.print("Uptime :");
  Serial.println(NTP.getUptime());
  Serial.print("LastBootTime :");
  Serial.println(NTP.getLastBootTime());
}

void loop() {
  // put your main code here, to run repeatedly:
  server.handleClient();
  t = dht.readTemperature();
  h = dht.readHumidity();
  pa = bmp.readPressure() / 100.0F;
  if ( isnan(t) || isnan(h) ) {
    //Erreur, aucune valeur valide - Error, no valid value
  } else {
    addPtToHist();
  }
  //delay(5);
}

void addPtToHist(){
  unsigned long currentMillis = millis();
  
  //Serial.println(currentMillis - previousMillis);
  if ( currentMillis - previousMillis > intervalHist ) {
    long int tps = NTP.getTime();
    previousMillis = currentMillis;
    //Serial.println(NTP.getTime());
    if ( tps > 0 ) {
      timestamp.add(tps);
      hist_t.add(double_with_n_digits(t, 1));
      hist_h.add(double_with_n_digits(h, 1));
      hist_pa.add(double_with_n_digits(pa, 1));

      //root.printTo(Serial);
      if ( hist_t.size() > sizeHist ) {
        //Serial.println("efface anciennes mesures");
        timestamp.removeAt(0);
        hist_t.removeAt(0);
        hist_h.removeAt(0);
        hist_pa.removeAt(0);
      }
      //Serial.print("size hist_t ");Serial.println(hist_t.size());
      calcStat();
      delay(100);
      saveHistory();
      //root.printTo(Serial);  
    }  
  }
}

Interface HTML obtenue

Connectez-vous sur l’ESP8266 depuis un navigateur. Attendez quelques secondes pour obtenir les premières mesures.

Les deux graphiques du haut permettent de suivre l’évolution de la température, de l’humidité et de la pression atmosphérique sur la période d’enregistrement (ici 40 minutes). La pression atmosphérique est également reportée sur une gauge. Les couleurs sont réelles (rouge pour risque de tempête et pluie), orange (temps variable), vert (beau temp).

Il n’est pas possible de définir d’autres zones, ce qui est dommage.

L’histogramme permet de suivre l’évolution moyenne de la température et de l’humidité. Ici, chaque barre représente 6 minutes environ.

quwjq9jrn2fsic6pxbsr-9492052

Voilà, notre projet de mini station météo est terminé.

Tous les articles et projets sur programmation Web Server des ESP8266

Si le sujet vous intéresse, voici un récapitulatif de tous les articles portant sur programmation Web Server des ESP8266

Accéder rapidement aux autres parties du projet

Voici les liens pour accéder aux autres parties du projet

Avez-vous aimé cet article ?