ESP8266 (Web Server – Part 5): how to use Google Charts to display gauges and charts

Now that we have a records we will add graphics to the Web interface of the ESP8266 using the Google Charts library. There are many libraries to create graphics and gauges in Javascript / HTML5. Google Charts is a very rich library that offers 28 different models (line, gauge, horizontal or vertical histogram, glove, pie, bubble, card, radar …) sufficient to start and create a beautiful interface. We will see in another tutorial other libraries with a more modern display.

The full source code of the project is also available on GitHub here

Prepare data for bar histogram

We’ll start by creating a small histogram that allows us to see the evolution of the average temperature (and humidity) for the last 7 hours (the registration period defined in the previous tutorial). Unless I’m mistaken on my part, there is no library to do the classification of measurements on Arduino (say me in the comments otherwise). I propose a small algorithm which allows to calculate the temperature and the average humidity for 7 classes (1 class per hour).

This algorithm is probably not the best for it but the goal of this tutorial is the creation of graphics and Google Charts sending data from the Arduino code to the Web interface.

How it works ?

  1. We initialize two arrays (statTemp and statHumi) with initial value -999 (or any other arbitrary value).
  2. We calculate the size of a class sizeClass = sizeHist / nbClass
  3. One moves the data table (only when it is full, it will be necessary to optimize this). The variable k gives the position in the table
  4. If the cell is empty (-999) then it is given the current value. Otherwise the average value (current value + previous value / 2)
  5. We test if we must change class (k + 1)> sizeClass * (currentClass + 1)
  6. Both tables are updated in the JSON (bart and 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]);
      }  
    }
  }
}

Add Google Charts to a Web Interface ESP8266

Let us now turn to serious matters.

The Google Charts library

Google is definitely everywhere. The Google Charts library is a library that exists since 2008. It is not the most beautiful library that exists but it is simple enough to implement and especially it is quite easy to create graphs real time (or at least ‘We will update very often). We will test other libraries later. The documentation is pretty good. Only flat, currently some graphics are available in the old version and in the new (Material Design). It is easy enough to locate it, for example to draw a line chart, you can

new google.visualization.LineChart

Or for the new version (Material Design)

new google.charts.Line

All the options are not yet available in the Material Design version, which makes it quite tedious to focus. If the aesthetic aspect is not primordial for you, I advise you to stay on the old version.

esp8266 web server google charts graphiques

HTML code

To load the library, simply add in the section Head this reference

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

Adding a Bootstrap Panel

The Bootstrap panel class allows to present data in the form of a map with a header (Bootstrap documentation). We will use it to create a header containing the current measurements returned by the sensors (DHT22 and BMP180).

The Panel consists of a header, panel-heading class. If we want to add a title, we must add the panel-title class to the class h1 (or hx). But nothing prevents us from putting other things into heading. Here we will simply add a row that will be cut into 3 equal columns (col-md-4). To make it responsive on small screens, add the class col-xs-4 (for example). In each column, we simply put an identifier that will be used to display the content as soon as values are available.

Which gives the following Pug code

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

Here’s what we’ll get

esp8266 web server bootstrap panel title header

Add graphs (line, vertical histogram, gauge) on 2 columns

The graphs will take place in another panel-body class div. We will simply add 2 lines cut in 2 columns. Either 4 cells. In each cell, you will place a Google Chart. Dan HTML code, there is nothing to set apart a style. Here, one takes all available width (width: 100%) and sets a height, for example height: 300px. For the gauge, I simply added a margin left (margin-left: 25%) because by default it is pasted on the left edge.

Finally you have noticed a title h2 that will make visible if there is no histogram to display and vice versa.

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%")

Javascript code

Depending on the type of graphic you want, additional packages must be loaded in addition to the corechart.

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

Then we indicate the function that will be launched as soon as the page has been loaded and the resources have been retrieved and loaded.

google.charts.setOnLoadCallback(drawChart);

To create a graph, you must already create an object by specifying its type. You retrieve your destination with the document.getElementById function (for example).

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

Then there are several methods to assign options and data, here is one. A data table is created.

dataChartTemp = new google.visualization.DataTable();

Then add the columns that will contain the values of the graph. The data type must be specified for each column. It is necessary to refer to the documentation to know the available formats (data format) for the desired graphic, here for example for a AreaChart. Here the time is of the form timeofday.

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

You can also define options, for example the position of the legend (position: “bottom” to place it underneath, “none” to disable its display), the label of the axes, etc … An example for the 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é (%)'}
      }
    }
  }

Now all you have to do is create a function to retrieve data from the ESP8266 and display it on the screen. To do this, we will simply use the jquery $.getJSON function already used in the previous tutorial. To avoid wasting the resources of the computer, smartphone or tablet and ESP8266, one checks that the graphics board is active before making an update

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
  })
}

Processing the JSON sent by the Web Server ESP8266

We will retrieve the data in the form of a JSON. for example

{
  "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]
}

We will have to make a small loop that runs the timestamp array to add a new row each time to the chart data table. For example, the data are prepared for the temperature / humidity graph of DHT22. The first column must contain one hour. For that we will use the function javascript new Date which is able to convert a timestamp unix into a date. It is necessary to multiply by 1000 because the function waits for milli-seconds, the timestamp is in seconds in this case. Finally, the date is indicated in the form of a table [HH, MM, SS]. Sorry, but have to do with the Google API! The treatment of time is always a problem finally!

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]
  ])
}

Add this block of values at once with the addRows function. Note that the addRow function also exists (for a single line).

dataChartTemp.addRows(_dataT);

Here I chose not to keep the old values, so we will delete them with the removeRows function which takes as parameter the index and the number of rows to remove.

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

View and refresh graphics automatically

All is ready, you can ask the browser to display the chartTemp chart.

chartTemp.draw(dataChartTemp, options1);

You can hide a chart if it is empty and display an information message instead. Just test the number of rows in the data array with the getNumberOfRows() method. Then show() or hide() on the element to make it visible or hide it. That’s it.

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

It only remains for the browser to update the graphics regularly by creating a timer using the setInterval() function. The timer is placed just before the updateGraph() function.

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

Arduino Code

We will add a call to the sendHistory() function when the web server intercepts a call on the /graph_temp.json page.

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

All measurements are stored in a JSON object using the ArduinoJSON library. It is very easy to generate a string using the printTo function. Which stores the result in a buffer. It will therefore be necessary to have enough memory if not the export will be impossible. We send the client the Serialized serialized JSON with 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");   
}

Be careful to stay reasonable and do not ask for an updated graphics every second. With little data, the ESP8266 will cash, but for how long … To know the minimum frequency not to be exceeded, look at the time to get an answer with the development tools of a browser.

Final Project Code

The full source code of the project is also available on GitHub here

Pug Template

The Pug template (Jade) easier to understand, edit, correct than HTML code.

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="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.js")
      link(rel="stylesheet" href="https://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&eacute;t&eacute;o (DHT22 + BMP180)
            ul.nav.nav-pills
              li.active
                a(href='#')
                  #temperature.span.badge.pull-right -
                  |  Temp&eacute;rature
              li
                a(href='#')
                  #humidite.span.badge.pull-right -
                  |  Humidit&eacute;
              li
                a(href='#')
                  #pa.span.badge.pull-right -
                  |  Pression atmosph&eacute;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&eacute;c&eacute;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='https://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 + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
            $("#labelTemp").html("&nbsp;" + value + "&nbsp;" + "<span class='badge'> " + row.valeur + row.unite + "</span><span class='glyphicon " + row.glyph + " pull-left'></span>");
          } else if ( value === "Humidité" ) {
            label = value + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
            $("#labelHumi").html("&nbsp;" + value + "&nbsp;" + "<span class='badge'> " + row.valeur + row.unite + "</span><span class='glyphicon " + row.glyph + " pull-left'></span>");
          } else if ( value === "Pression Atmosphérique" ) {
            label = value + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
            $("#labelPa").html("&nbsp;" + value + "&nbsp;" + "<span class='badge'> " + row.valeur + row.unite + "</span><span class='glyphicon " + row.glyph + " pull-left'></span>");
          } else {
            label = value;
          } 
          return label;
        }
        function valueFormatter(value, row){
          //console.log("valueFormatter");
          var label = "";
          if ( row.valeur > row.precedente ) {
            label = value + row.unite + "<span class='glyphicon glyphicon-chevron-up pull-right'></span>";
          } else { 
            label = value + row.unite + "<span class='glyphicon glyphicon-chevron-down pull-right'></span>";
          }
          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&egrave;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;
          }
        }

HTML code

The HTML code generated from the template Pug above.

<!DOCTYPE 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>
    <script src="https://www.gstatic.com/charts/loader.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.11.0/bootstrap-table.min.js"></script>
    <link rel="stylesheet" href="https://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</title>
  </head>
  <body>
    <div class="container-fluid">
      <h1>ESP8266 Web Server + SPIFFS + Bootstrap + Google Charts </h1>
      <ul class="nav nav-tabs" id="tab">
        <li class="active"><a href="#tab_mesures" data-toggle="tab">Mesures</a></li>
        <li><a href="#tab_graphs" data-toggle="tab">Graphiques</a></li>
        <li><a href="#tab_gpio" data-toggle="tab">GPIO  </a></li>
        <li><a href="#tab_configuration" data-toggle="tab">Configuration</a></li>
      </ul>
      <div class="tab-content">
        <div class="tab-pane fade in active" id="tab_mesures">        
          <h2>Mini station m&eacute;t&eacute;o (DHT22 + BMP180)</h2>
          <ul class="nav nav-pills">
            <li class="active"><a href="#">
                <div class="span badge pull-right" id="temperature">-</div> Temp&eacute;rature</a></li>
            <li><a href="#">
                <div class="span badge pull-right" id="humidite">-</div> Humidit&eacute;</a></li>
            <li><a href="#">
                <div class="span badge pull-right" id="pa">-</div> Pression atmosph&eacute;rique</a></li>
          </ul><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>
                <th data-field="valeur" data-align="left" data-sortable="true" data-formatter="valueFormatter">Valeur</th>
                <th data-field="precedente" data-align="left" data-sortable="true" data-formatter="vpFormatter">Valeur Pr&eacute;c&eacute;dente</th>
              </tr>
            </thead>
          </table>
        </div>
        <div class="tab-pane fade" id="tab_graphs">
          <div class="panel panel-default">
            <div class="panel-heading">
              <div class="row panel-title"> 
                <div class="col-xs-4 col-md-4">
                  <div id="labelTemp"></div>
                </div>
                <div class="col-xs-4 col-md-4">
                  <div id="labelHumi"></div>
                </div>
                <div class="col-xs-4 col-md-4">
                  <div id="labelPa"></div>
                </div>
              </div>
            </div>
            <div class="panel body">
              <div class="row">
                <div class="col-xs-6 col-md-6">
                  <div class="div" id="chartTemp" style="width: 100%; height: 300px;"></div>
                </div>
                <div class="col-xs-6 col-md-6">
                  <div class="div" id="chartPA" style="width: 100%; height: 300px;"></div>
                </div>
              </div>
              <div class="row">
                <div class="col-xs-6 col-md-6">
                  <h2 class="label label-info" id="zeroDataTemp">Pas encore de données</h2>
                  <div class="div" id="barTemp" style="width: 100%; height: 300px;"></div>
                </div>
                <div class="col-xs-6 col-md-6">
                  <div class="div" id="gaugePA" style="width: 100%; height: 300px; margin-left: 25%;">    </div>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div class="tab-pane fade" id="tab_gpio">
          <h2>GPIO</h2>
          <div class="row">
            <div class="col-xs-4 col-md-4">
              <h4 class="text-left">D5
                <div class="span badge" id="D5_etat">OFF</div>
              </h4>
            </div>
            <div class="col-xs-4 col-md-4">
              <div class="button btn btn-success btn-lg" id="D5_On" type="button">ON</div>
            </div>
            <div class="col-xs-4 col-md-4">
              <div class="button btn btn-danger btn-lg" id="D5_Off" type="button">OFF</div>
            </div>
          </div>
          <div class="row">
            <div class="col-xs-4 col-md-4">
              <h4 class="text-left">D6
                <div class="span badge" id="D6_etat">OFF</div>
              </h4>
            </div>
            <div class="col-xs-4 col-md-4">
              <div class="button btn btn-success btn-lg" id="D6_On" type="button">ON</div>
            </div>
            <div class="col-xs-4 col-md-4">
              <div class="button btn btn-danger btn-lg" id="D6_Off" type="button">OFF</div>
            </div>
          </div>
          <div class="row">
            <div class="col-xs-4 col-md-4">
              <h4 class="text-left">D7
                <div class="span badge" id="D7_etat">OFF</div>
              </h4>
            </div>
            <div class="col-xs-4 col-md-4">
              <div class="button btn btn-success btn-lg" id="D7_On" type="button">ON</div>
            </div>
            <div class="col-xs-4 col-md-4">
              <div class="button btn btn-danger btn-lg" id="D7_Off" type="button">OFF</div>
            </div>
          </div>
          <div class="row">
            <div class="col-xs-4 col-md-4">
              <h4 class="text-left">D8
                <div class="span badge" id="D8_etat">OFF</div>
              </h4>
            </div>
            <div class="col-xs-4 col-md-4">
              <div class="button btn btn-success btn-lg" id="D8_On" type="button">ON</div>
            </div>
            <div class="col-xs-4 col-md-4">
              <div class="button btn btn-danger btn-lg" id="D8_Off" type="button">OFF</div>
            </div>
          </div>
        </div>
        <div class="tab-pane fade" id="tab_configuration">
          <h2>Configuration        </h2>
          <div class="btn-group">
            <button class="btn btn-default" id="labelTheme">Theme</button>
            <button class="btn btn-default dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>
            <ul class="dropdown-menu">
              <li><a class="change-style-menu-item" href="#" rel="bootstrap">Boostrap</a></li>
              <li><a class="change-style-menu-item" href="#" rel="cerulean">Cerulean</a></li>
              <li><a class="change-style-menu-item" href="#" rel="cosmo">Cosmo</a></li>
              <li><a class="change-style-menu-item" href="#" rel="cyborg">Cyborg</a></li>
              <li><a class="change-style-menu-item" href="#" rel="darkly">Darkly</a></li>
              <li><a class="change-style-menu-item" href="#" rel="flatly">Flatly</a></li>
              <li><a class="change-style-menu-item" href="#" rel="journal">Journal</a></li>
              <li><a class="change-style-menu-item" href="#" rel="lumen">Lumen</a></li>
              <li><a class="change-style-menu-item" href="#" rel="paper">Paper</a></li>
              <li><a class="change-style-menu-item" href="#" rel="readable">Readable</a></li>
              <li><a class="change-style-menu-item" href="#" rel="sandstone">Sandstone</a></li>
              <li><a class="change-style-menu-item" href="#" rel="simplex">Simplex</a></li>
              <li><a class="change-style-menu-item" href="#" rel="slate">Slate</a></li>
              <li><a class="change-style-menu-item" href="#" rel="spacelab">Spacelab</a></li>
              <li><a class="change-style-menu-item" href="#" rel="superhero">Superhero</a></li>
              <li><a class="change-style-menu-item" href="#" rel="united">United</a></li>
              <li><a class="change-style-menu-item" href="#" rel="yeti">Yeti  </a></li>
            </ul>
          </div>
        </div>
      </div>
      <div class="row" style="position:absolute; bottom:0; width:100%;">
        <div class="col-xs-2 col-md-2"><img src="img/logo.png" width="30" height="30"></div>
        <div class="col-xs-5 col-md-5">
          <p><a href="http://www.projetsdiy.fr">Version francaise : www.projetsdiy.fr</a></p>
        </div>
        <div class="col-xs-5 col-md-5">
          <p><a href="https://www.diyprojects.io">English version : www.diyprojects.io</a></p>
        </div>
      </div>
    </div>
    <!--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 + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
          $("#labelTemp").html("&nbsp;" + value + "&nbsp;" + "<span class='badge'> " + row.valeur + row.unite + "</span><span class='glyphicon " + row.glyph + " pull-left'></span>");
        } else if ( value === "Humidité" ) {
          label = value + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
          $("#labelHumi").html("&nbsp;" + value + "&nbsp;" + "<span class='badge'> " + row.valeur + row.unite + "</span><span class='glyphicon " + row.glyph + " pull-left'></span>");
        } else if ( value === "Pression Atmosphérique" ) {
          label = value + "<span class='glyphicon " + row.glyph + " pull-left'></span>";
          $("#labelPa").html("&nbsp;" + value + "&nbsp;" + "<span class='badge'> " + row.valeur + row.unite + "</span><span class='glyphicon " + row.glyph + " pull-left'></span>");
        } else {
          label = value;
        } 
        return label;
      }
      function valueFormatter(value, row){
        //console.log("valueFormatter");
        var label = "";
        if ( row.valeur > row.precedente ) {
          label = value + row.unite + "<span class='glyphicon glyphicon-chevron-up pull-right'></span>";
        } else { 
          label = value + row.unite + "<span class='glyphicon glyphicon-chevron-down pull-right'></span>";
        }
        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&egrave;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;
        }
      }
    </script>
  </body>
</html>

Arduino Code

And the Arduino code to upload in the ESP8266. The code is intended for a Wemos D1 Mini equipped with a Shield DHT22 as well as a BMP180 on bus I2C.

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <DHT.h>
#include <Adafruit_BMP085.h>
#include <FS.h>
#include <TimeLib.h>
#include <NtpClientLib.h>
#include <ArduinoJson.h>

#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
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 );
HTTPClient http;

StaticJsonBuffer<10000> 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 mis a jour");
}

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

  server.send(200, "application/json", json);
  Serial.println("Mesures envoyees");
}

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("Histogramme Température"); 
    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("");
    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("Tableau mesures courantes envoyees");
}

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");   
}

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<char[]> 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 enregsitre 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()));
      }
  });
  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 );
  // Attente de la connexion au réseau WiFi / Wait for connection
  while ( WiFi.status() != WL_CONNECTED ) {
    delay ( 500 ); Serial.print ( "." );
  }
  // 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" );
}

void loop() {
  // put your main code here, to run repeatedly:
  server.handleClient();
  ftpSrv.handleFTP();
  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 obtained

Connect to the ESP8266 from a browser. Wait a few seconds to get the first measurements. The two graphs at the top make it possible to monitor the temperature, humidity and atmospheric pressure during the recording period (here 40 minutes). The atmospheric pressure is also reported on a gauge. The colors are real (red for risk of storm and rain), orange (variable time), green (beautiful temp). It is not possible to define other zones, which is a pity. Finally, the histogram makes it possible to follow the average evolution of temperature and humidity. Here, each bar represents about 6 minutes.

esp8266 web server spiffs bootstrap dht22 bmp180 google charts

So, our ESP8266 Web Server programming tutorial soon comes to an end. In the next tutorial we will see how to update “in the air” (OTA) the Arduino program.

Subscribe to the weekly newsletter

No spam and no other use will be made of your email. You can unsubscribe anytime.

  • francesco maviglia

    Great project, thank you very much. I have tried with wemos d1, and it worked immediately, even by modifying a little bit the code to use different sensors. I have anyway the issue that once the 7 hours are reached the graphs stop being updated, and the remain alwyas the same. I would immagine that they are meant to scroll as the time pgoes by, maybe even forgetting the past data. Does it work like this for you?
    Thank you

    • Hello Francesco and thank you very much. No, I did not encounter this problem during the developments. Maybe it’s a problem rated internet browser. You tried to close the page and open a new one?

      • francesco maviglia

        Thank you. What I suspect is that the Json command remove was somehow missbehaving. I rolled back the library to the version that you showe in the tutorial 4 and now the graph seems to work fine (I will test the stability during the next few days). I will now train myself in changing the graph type and parameters (i.e. the duration increased to 24h), and see how it goes.

        • Perfect. thank you very much for your feedback.

          • francesco maviglia

            Strangely enought it worked for about one day and half, then it got
            stuck again to the latest values. Just to explain: The first page shows
            normally the tabular data that the sensor feeds, while the in the graph page, the
            two top panels, shows a frozen 7 hour situation, of the last 7 hours
            after 1,5 days that I started the system. Clearly at the beginning the
            system was able to update and scroll the hours, but at a certain point
            this was not anymore the case. The gauge instead still works and keeps
            updated. How can I diagnose the problem? I suspect, as mentioned, either on the JSON capabiity to delete the array, or maybe on the fact that my internet connection jups, sometime, and not receiving the signal from the server the algorithm fails? could II add some code to ignore the request of data, if is not avaialbe, without getting the program flows stuck? Thank you for our help

          • Hello Franscesco. Thanks for your info. Maybe it would be necessary to test the validity of the side web browser or force the reloading of the page (every hour for example).

          • francesco maviglia

            thank you again, I have changed the hardware (using a wemos mini), but similar problem arise. Now the plots on the graph pages upper part have an issue on the time scale. See pictures below. Could it be that this is due to retrival of the internet server for the time? https://uploads.disquscdn.com/images/f9133917fae020adade530302fb0751f05ca7b41be4f6bb042d2808340f20a06.png

        • francesco maviglia

          Strangely enopught it worked for about one day and half, then it got stuck again to the latest values. Just to explain: The first page shows normally the tabular data the sensor feeds, while the graph page, the two top panels, shoe a frozen 7 hour situation, of the last 7 hours after 1,5 days that ?I started the sistem. clearly at the beginning the system was able to update and scroll the hours, but at a certain point this was not anymore the case. The gauge instead still works and keeps updated. How can I diagnose the problem? Thank you for our help

    • G6EJD

      Modify (count) at lines 394 onward to (count-1) then 399, 400 and 401

      tempCdata[count-1] = T;
      pressData[count-1] = P;
      timeStamp[count-1] = currentTime;
      Then it scrolls OK

      The problem is count < numberOfRows then it gets incremented one last time to numberofRows
      but on the next pass all the data gets scrolled to the left, but at (numberofRows+1) there is nothing there! so it locks up.

  • Michael Stjerna

    Hi

    I love the project but I’m facing some issues on the code that I didn’t find a solution to.
    * All the data are stored on the SPIFFs in the .json file, so far so good.
    * When I restart the ESP the file is read and I can print the history.json to the serial, but as soon as this is done the file gets over written with a new value from the DHT22. 🙁

    All the new data are saved and displayed but with every restart of the ESP the data is lost.
    I have compiled the Section5 code and also started from secion1 and went all the way of adding the code manually with the same result.

    Anyone experienced and solved this issue?

DIY Projects