Webserver for SonOff basic, adressing Red LED, including sensor function

Webserver output

Sure there is Tasmota, ESPhome, Espruino or ESPEasy, but here is a simple webserver that controls the relay, the Green LED and also the Red LED.
On the WiFi version of the SonOff Basic, the Red LED is not connected (it is for the 433Mhz version), but it can be easily utilized by soldering a wire (See here and here how it is done). I wanted to pick picked gpio4 to control the LED, but with my ageing eyes gpio 14 was easier.

The server software is for a large part taken from randommerdtutorials (check them out of you already have not done so) and altered to my needs. I have added OTA, but as the SONOFF only has 1 Mb of memory so it might become a thigh squeeze. Understand that there might not be that much use in being able to switch on the LED’s, I am just showing how to do it. You may well want to use those LED’s as a signal function for something else.

I have added a readout of the voltage on the vcc pin. This might be quite futile for a mains fed esp8266, but it illustrates how to add sensor data. If with better eyes you could manage to switch the red LED from gpio4, you have gpio14 easily available for for instance for a DS18B20 temperature sensor. Also, gpio0 is easily accessible (it attaches to the push button), and realise that Tx and Rx can be used as gpio pins as well. (text continues below the program listing). If you decide to solder on the SonOff PCB: be very careful: the tracks are delicate. Don’t pull them off.

/*********
Sonoffbasic webserver
 Uses a big piece of code from
 https://RandomNerdTutorials.com/esp8266-relay-module-ac-web-server/

  GPIO0  Button (inverted)
  GPIO1  TX pin (C1 Pin 2)
  GPIO3  RX pin (C1 Pin 3)
  GPIO12 Relay
  GPIO13 Green LED (inverted)
  GPIO14 Optional Sensor (C1 Pin 5)
*********/

// Import required libraries
#include "ESP8266WiFi.h"
#include "ESPAsyncWebServer.h"
//---------------for OTA----------------------
#include <ESP8266mDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
//---------------------------------------------
ADC_MODE(ADC_VCC);
int vcc;
double dvcc;


// Set to true to define Relay as Normally Open (NO)
#define RELAY_NO    true

// Set number of relays
#define NUM_RELAYS  3
// Assign each GPIO to a relay
int relayGPIOs[NUM_RELAYS] = { 12, 13, 14};
String devices[NUM_RELAYS] = {"Switch", "GreenLed","RedLed"};

// Replace with your network credentials
const char* ssid = "YourSSID";
const char* password = "YourPW";

const char* PARAM_INPUT_1 = "relay";
const char* PARAM_INPUT_2 = "state";

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);
AsyncEventSource events("/events");// voor SSE

signed long lastTime = -10000;
unsigned long timerDelay = 10000;  // send readings timer: 10sec

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href='https://use.fontawesome.com/releases/v5.7.2/css/all.css' integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
    <link rel="icon" href="data:,">

  <style>
    html {font-family: Arial; display: inline-block; text-align: center;}
    h2 {font-size: 3.0rem;}
    p {font-size: 1.0rem;}
    .solid {border-style: solid;border-padding: 1em;}
    .center {
      margin-left: auto;
      margin-right: auto;
      }
table{
  border: 1px solid;
  }
    sensor-head {font-size: 12px; font-style: italic;font-weight: bold; vertical-align:middle;}
    sensor-labels {font-size: 6px;vertical-align:middle;padding-bottom: 10px;}
    body {max-width: 600px; margin:0px auto; padding-bottom: 25px;}
    .switch {position: relative; display: inline-block; width: 89px; height: 34px} 
    .switch input {display: none}
    .slider {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 34px}
    .slider:before {position: absolute; content: ""; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 68px}
    input:checked+.slider {background-color: #2196F3}
    input:checked+.slider:before {-webkit-transform: translateX(52px); -ms-transform: translateX(52px); transform: translateX(52px)}
  </style>
</head>
<body>
  <h2>SonOff Basic</h2>
  %BUTTONPLACEHOLDER%
<script>function toggleCheckbox(element) {
  var xhr = new XMLHttpRequest();
  if(element.checked){ xhr.open("GET", "/update?relay="+element.id+"&state=1", true); }
  else { xhr.open("GET", "/update?relay="+element.id+"&state=0", true); }
  xhr.send();
}</script>
<p>
<table class='center' id='solid'><tr>
<th><i class="fa-thin fa-bolt" style="color:#ff9e8a;"></i></td> <td><span class="sensor-labels">Vcc</span></td>
    <td><span id="vcc">%VCC%</span></td>
    <td><sup class="units">V</sup></td></tr>
</table>
</p>
 <!-- code voor event -->
  <script>
if (!!window.EventSource) {
 var source = new EventSource('/events');

 source.addEventListener('open', function(e) {
  console.log("Events Connected");
 }, false);
 source.addEventListener('error', function(e) {
  if (e.target.readyState != EventSource.OPEN) {
    console.log("Events Disconnected");
  }
 }, false);

 source.addEventListener('message', function(e) {
  console.log("message", e.data);
 }, false);

source.addEventListener('vcc', function(e) {
  console.log("vcc", e.data);
  document.getElementById("vcc").innerHTML = e.data;
 }, false);

}
</script>
 

</body>
</html>
)rawliteral";

// Replaces placeholder with button section in your web page
String processor(const String& var){
  //Serial.println(var);
  if(var == "BUTTONPLACEHOLDER"){
    String buttons ="";
    for(int i=1; i<=NUM_RELAYS; i++){
      String relayStateValue = relayState(i);
      buttons+= "<h4>"+devices[i-1]+" - Output #" + String(i) + " - GPIO " + relayGPIOs[i-1] + "</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleCheckbox(this)\" id=\"" + String(i) + "\" "+ relayStateValue +"><span class=\"slider\"></span></label>";
    }
    return buttons;
  }
  
 if (var=="VCC"){
      vcc = ESP.getVcc();
      dvcc = 1*(float)vcc / 1024;
      return String(dvcc, 3);
      }

  return String();
}

String relayState(int numRelay){
  if(RELAY_NO){
    if(digitalRead(relayGPIOs[numRelay-1])){
      return "";
    }
    else {
      return "checked";
    }
  }
  else {
    if(digitalRead(relayGPIOs[numRelay-1])){
      return "checked";
    }
    else {
      return "";
    }
  }
  return "";
}

void setup(){
  // Serial port for debugging purposes
  Serial.begin(115200);

  // Set all relays to off when the program starts - if set to Normally Open (NO), the relay is off when you set the relay to HIGH
  for(int i=1; i<=NUM_RELAYS; i++){
    pinMode(relayGPIOs[i-1], OUTPUT);
    if(RELAY_NO){
      digitalWrite(relayGPIOs[i-1], HIGH);
    }
    else{
      digitalWrite(relayGPIOs[i-1], LOW);
    }
  }
  
  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }

  // Print ESP8266 Local IP Address
  Serial.println(WiFi.localIP());

  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html, processor);
  });


 // Handle Web Server Events
events.onConnect([](AsyncEventSourceClient *client){
  if(client->lastId()){
    // //Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
  }
  // send event with message "hello!", id current millis
  // and set reconnect delay to 1 secondtemperature_koudebak = sensors.getTempC(ProbeBinnen);
  client->send("hello!", NULL, millis(), 10000);
});
server.addHandler(&events);

  // Send a GET request to <ESP_IP>/update?relay=<inputMessage>&state=<inputMessage2>
  server.on("/update", HTTP_GET, [] (AsyncWebServerRequest *request) {
    String inputMessage;
    String inputParam;
    String inputMessage2;
    String inputParam2;
    // GET input1 value on <ESP_IP>/update?relay=<inputMessage>
    if (request->hasParam(PARAM_INPUT_1) & request->hasParam(PARAM_INPUT_2)) {
      inputMessage = request->getParam(PARAM_INPUT_1)->value();
      inputParam = PARAM_INPUT_1;
      inputMessage2 = request->getParam(PARAM_INPUT_2)->value();
      inputParam2 = PARAM_INPUT_2;
      if(RELAY_NO){
        Serial.print("NO ");
        digitalWrite(relayGPIOs[inputMessage.toInt()-1], !inputMessage2.toInt());
      }
      else{
        Serial.print("NC ");
        digitalWrite(relayGPIOs[inputMessage.toInt()-1], inputMessage2.toInt());
      }
    }
    else {
      inputMessage = "No message sent";
      inputParam = "none";
    }
    Serial.println(inputMessage + inputMessage2);
    request->send(200, "text/plain", "OK");
  });
  // Start server
  server.begin();
  //----------------------------OTA--------------------
  
  ArduinoOTA.setHostname("Sonoff");

  // No authentication by default
 //  ArduinoOTA.setPassword("admin");

  ArduinoOTA.onStart([]() {
    String type;
    if (ArduinoOTA.getCommand() == U_FLASH) {
      type = "sketch";
    } else { // U_FS
      type = "filesystem";
    }

    // NOTE: if updating FS this would be the place to unmount FS using FS.end()
    Serial.println("Start updating " + type);
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\nEnd");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("Error[%u]: ", error);
    if (error == OTA_AUTH_ERROR) {
      Serial.println("Auth Failed");
    } else if (error == OTA_BEGIN_ERROR) {
      Serial.println("Begin Failed");
    } else if (error == OTA_CONNECT_ERROR) {
      Serial.println("Connect Failed");
    } else if (error == OTA_RECEIVE_ERROR) {
      Serial.println("Receive Failed");
    } else if (error == OTA_END_ERROR) {
      Serial.println("End Failed");
    }
  });
  ArduinoOTA.begin();
  Serial.println("Ready");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  
}
  
void loop() {
 ArduinoOTA.handle();
if ((millis() - lastTime) > timerDelay) {
  sendEvents();
  lastTime = millis();
}
}

void sendEvents()
{
vcc = ESP.getVcc();
dvcc =1*(float)vcc / 1024;
events.send(String(dvcc, 3).c_str(),"vcc",millis());
}

Rather than checking for millis() in the loop() to determine the passed time, I could have uses a ‘setInterval()’ function in JavaScript, but in this case just a quick check in the loop was easier

As said, I added a read out of the voltage to illustrate how you could add a sensor. We can spice up that readout for instance by making it change color at a certain level. Suppose we want the voltage readout to turn red when it rises above 3.3Volt and back to black when it drops back to 3.3volt or less.
that is quite easy. First, keep in mind that the voltage readout is sent to the server at two instances: initially when the webpage is built and then at every 10 secs (which is the chosen length of the interval).
First we will change the section where the initial build up of the page is taking place: for this locate the section that begins with if var=="VCC){.
Amend that entire section as follows:

 if (var=="VCC"){
      vcc = ESP.getVcc();
      dvcc = 1*(float)vcc / 1024;
      if (dvcc>3.3){
         return "<span style='color:red'>"+String(dvcc, 3)+"</span>";
             }
      if (dvcc<=3.3){
         return String(dvcc, 3);
             }
      }
  return String();
}

That should deal with the initial buildup of the page. Where you to upload this, you would see the voltage readout in red (if it were above 3.3 Volt), but it would turn back to black after 10 secs.

To fix that, go to the end of the program to the “void sendEvent()” function.
amend this as follows:

void sendEvents()
{
vcc = ESP.getVcc();
dvcc =1*(float)vcc / 1024;
if(dvcc>3.3){
  formatdvcc="<span style='color:red'>"+String(dvcc,3)+"</span>";}
if (dvcc<=3.3){
  formatdvcc=String(dvcc,3);
  }
events.send(formatdvcc.c_str(),"vcc",millis());
}

One final thing to do is to go to the declaration section of the program and add: String formatdvcc;.

In case you wonder why I do “dvcc =1*(float)vcc / 1024;” rather than just “dvcc =(float)vcc / 1024;“, that is because the ADC of the ESP8266 is not very accurate and you may need to add a specific multiplication factor to let it 3V3 actually show as 3V3. I have chosen ‘1” for that. Given that my readout is commonly 3.425 Volt, it should probably have been 0.963 rather than 1, but it is not so important. Obviously it is also possible to attach some warning function this way (a buzzer, a flashing LED).

If you like the box around the Vcc value to have a bit more rounded corners in order to be less of a contrast with the round slider buttons, change the table section in style section as follows:

table{
border: 1px solid;
border-radius: 10px;
}

Other software:

Real time graph for data logging

While scowering the internet for some hardware info on data logging i came upon a logging program (using chartjs) that seemed interesting at first, but that kept crashing my ESP8266 the moment the webpage was loaded. So I set out to find the fault and fix it. In the end it involved altering the entire structure of the program. (Edit: their program will work correctly if in the ‘void handleRoot()’ routine, they had changed String s = MAIN_page; to be String s = FPSTR(MAIN_page);.
Another issue with that program was that it loaded 4 fake values before it started recording some real values. This was obviously done to have some initial values in the graph during the first interval time. There is a better way to do that so I fixed that too. Finally, I made it suitable to be viewed on a tablet or phone as well.

This is the final program: (note, there is a mistake in the html because wordpress cannot display it correctly. Best get the file here.

/*
 * ESP8266 NodeMCU Real Time Graphs Demo
 * Updates and Gets data from webpage without page refresh
 */
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>

#define LED 2  //On board LED

//SSID and Password of your WiFi router
const char* ssid = "YourSSID";
const char* password = "YourPW";

ESP8266WebServer server(80); //Server on port 80
//---------------------
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>

<head>
  <title>Data logger</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
//the below line is wrong and wordpress cannot show the right way. The link should be in a 'script src=' tag and closed with '/script' html tag
  https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.min.js
  <style>
  canvas{
    -moz-user-select: none;
    -webkit-user-select: none;
    -ms-user-select: none;
  }

  /* Data Table Styling */
  #dataTable {
    font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
    border-collapse: collapse;
    width: 100%;
  }

  #dataTable td, #dataTable th {
    border: 1px solid #ddd;
    padding: 8px;
  }

  #dataTable tr:nth-child(even){background-color: #f2f2f2;}

  #dataTable tr:hover {background-color: #ddd;}

  #dataTable th {
    padding-top: 12px;
    padding-bottom: 12px;
    text-align: left;
    background-color: #4CAF50;
    color: white;
  }
  </style>
</head>

<body>
    <div style="text-align:center;">Real Time Voltage logging</div>
    <div class="chart-container" position: relative; height:350px; width:100%">
        <canvas id="Chart" width="400" height="400"></canvas>
    </div>
<div>
  <table id="dataTable">
    <tr><th>Time</th><th>ADC Value</th></tr>
  </table>
</div>
<br>
<br>  

<script>
//Graphs visit: https://www.chartjs.org
var values = [];
var timeStamp = [];
function showGraph()
{
    for (i = 0; i < arguments.length; i++) {
      values.push(arguments[i]);    
    }

    var ctx = document.getElementById("Chart").getContext('2d');
    var Chart2 = new Chart(ctx, {
        type: 'line',
        data: {
            labels: timeStamp,  //Bottom Labeling
            datasets: [{
                label: "Voltage",
                fill: false, 
                backgroundColor: 'rgba( 18, 156, 243 , 1)', //Dot marker color
                borderColor: 'rgba( 18, 156, 243 , 1)', //Graph Line Color
                data: values,
            }],
        },
        options: {
            title: {
                    display: true,
                    text: "ADC Voltage"
                },
            maintainAspectRatio: false,
            elements: {
            line: {
                    tension: 0.2 //Smoothening (Curved) of data lines
                }
            },
            scales: {
                    yAxes: [{
                        ticks: {
                            beginAtZero:false
                        }
                    }]
            }
        }
    });

}

//On Page load show graphs
window.onload = function() {
  console.log(new Date().toLocaleTimeString());
  getData();
  showGraph();
};

//Ajax script to get ADC voltage at every 5 Seconds 

setInterval(function() {
  // Call a function repetatively with 5 Second interval
  getData();
}, 5000); 
 
function getData() {
  var xhttp = new XMLHttpRequest();
  xhttp.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
     //Push the data in array
  var time = new Date().toLocaleTimeString();
  var ADCValue = this.responseText; 
      values.push(ADCValue);
      timeStamp.push(time);
      showGraph();  //Update Graphs
  //Update Data Table
    var table = document.getElementById("dataTable");
    var row = table.insertRow(1); //Add after headings
    var cell1 = row.insertCell(0);
    var cell2 = row.insertCell(1);
    cell1.innerHTML = time;
    cell2.innerHTML = ADCValue;
    }
  };
  xhttp.open("GET", "readADC", true); //Handle readADC server on ESP8266
  xhttp.send();
}
    
</script>
</body>

</html>
)rawliteral";
//--------------------

void handleRoot() {
 server.send(200, "text/html", index_html); //Send web page
  
}

void handleADC() {
 int a = analogRead(A0);
 String adcValue = String(a);
 digitalWrite(LED,!digitalRead(LED)); //Toggle LED on data request ajax
 server.send(200, "text/plane", adcValue); //Send ADC value only to client ajax request
}

void setup(){
  Serial.begin(115200);
  
  WiFi.begin(ssid, password);     //Connect to your WiFi router
  Serial.println("");

  //Onboard LED port Direction output
  pinMode(LED,OUTPUT); 
  
  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  //If connection successful show IP address in serial monitor
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());  //IP address assigned to your ESP

  server.on("/", handleRoot);      
  server.on("/readADC", handleADC); 
  server.begin();                  
  Serial.println("HTTP server started");
}

void loop(){
  server.handleClient();          //Handle client requests
}

The below picture shows what the corrupted line should look like, but best download the file from here.

Ofcourse choosing Influx and Grafana to store and show data is also a good choice.