LibTeleinfo/examples/Wifinfo/webserver.cpp

813 lines
23 KiB
C++

// **********************************************************************************
// ESP8266 Teleinfo WEB Server, route web function
// **********************************************************************************
// Creative Commons Attrib Share-Alike License
// You are free to use/extend this library but please abide with the CC-BY-SA license:
// Attribution-NonCommercial-ShareAlike 4.0 International License
// http://creativecommons.org/licenses/by-nc-sa/4.0/
//
// For any explanation about teleinfo ou use, see my blog
// http://hallard.me/category/tinfo
//
// This program works with the Wifinfo board
// see schematic here https://github.com/hallard/teleinfo/tree/master/Wifinfo
//
// Written by Charles-Henri Hallard (http://hallard.me)
//
// History : V1.00 2015-06-14 - First release
//
// All text above must be included in any redistribution.
//
// **********************************************************************************
// Include Arduino header
#include "webserver.h"
// Optimize string space in flash, avoid duplication
const char FP_JSON_START[] PROGMEM = "{\r\n";
const char FP_JSON_END[] PROGMEM = "\r\n}\r\n";
const char FP_QCQ[] PROGMEM = "\":\"";
const char FP_QCNL[] PROGMEM = "\",\r\n\"";
const char FP_RESTART[] PROGMEM = "OK, Redémarrage en cours\r\n";
const char FP_NL[] PROGMEM = "\r\n";
/* ======================================================================
Function: formatSize
Purpose : format a asize to human readable format
Input : size
Output : formated string
Comments: -
====================================================================== */
String formatSize(size_t bytes)
{
if (bytes < 1024){
return String(bytes) + F(" Byte");
} else if(bytes < (1024 * 1024)){
return String(bytes/1024.0) + F(" KB");
} else if(bytes < (1024 * 1024 * 1024)){
return String(bytes/1024.0/1024.0) + F(" MB");
} else {
return String(bytes/1024.0/1024.0/1024.0) + F(" GB");
}
}
/* ======================================================================
Function: getContentType
Purpose : return correct mime content type depending on file extension
Input : -
Output : Mime content type
Comments: -
====================================================================== */
String getContentType(String filename) {
if(filename.endsWith(".htm")) return F("text/html");
else if(filename.endsWith(".html")) return F("text/html");
else if(filename.endsWith(".css")) return F("text/css");
else if(filename.endsWith(".json")) return F("text/json");
else if(filename.endsWith(".js")) return F("application/javascript");
else if(filename.endsWith(".png")) return F("image/png");
else if(filename.endsWith(".gif")) return F("image/gif");
else if(filename.endsWith(".jpg")) return F("image/jpeg");
else if(filename.endsWith(".ico")) return F("image/x-icon");
else if(filename.endsWith(".xml")) return F("text/xml");
else if(filename.endsWith(".pdf")) return F("application/x-pdf");
else if(filename.endsWith(".zip")) return F("application/x-zip");
else if(filename.endsWith(".gz")) return F("application/x-gzip");
else if(filename.endsWith(".otf")) return F("application/x-font-opentype");
else if(filename.endsWith(".eot")) return F("application/vnd.ms-fontobject");
else if(filename.endsWith(".svg")) return F("image/svg+xml");
else if(filename.endsWith(".woff")) return F("application/x-font-woff");
else if(filename.endsWith(".woff2")) return F("application/x-font-woff2");
else if(filename.endsWith(".ttf")) return F("application/x-font-ttf");
return "text/plain";
}
/* ======================================================================
Function: handleFileRead
Purpose : return content of a file stored on SPIFFS file system
Input : file path
Output : true if file found and sent
Comments: -
====================================================================== */
bool handleFileRead(String path) {
if ( path.endsWith("/") )
path += "index.htm";
String contentType = getContentType(path);
String pathWithGz = path + ".gz";
DebugF("handleFileRead ");
Debug(path);
if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) {
if( SPIFFS.exists(pathWithGz) ){
path += ".gz";
DebugF(".gz");
}
DebuglnF(" found on FS");
File file = SPIFFS.open(path, "r");
size_t sent = server.streamFile(file, contentType);
file.close();
return true;
}
Debugln("");
server.send(404, "text/plain", "File Not Found");
return false;
}
/* ======================================================================
Function: handleFormConfig
Purpose : handle main configuration page
Input : -
Output : -
Comments: -
====================================================================== */
void handleFormConfig(void)
{
String response="";
int ret ;
LedBluON();
// We validated config ?
if (server.hasArg("save"))
{
int itemp;
DebuglnF("===== Posted configuration");
// WifInfo
strncpy(config.ssid , server.arg("ssid").c_str(), CFG_SSID_SIZE );
strncpy(config.psk , server.arg("psk").c_str(), CFG_PSK_SIZE );
strncpy(config.host , server.arg("host").c_str(), CFG_HOSTNAME_SIZE );
strncpy(config.ap_psk , server.arg("ap_psk").c_str(), CFG_PSK_SIZE );
strncpy(config.ota_auth,server.arg("ota_auth").c_str(), CFG_PSK_SIZE );
itemp = server.arg("ota_port").toInt();
config.ota_port = (itemp>=0 && itemp<=65535) ? itemp : DEFAULT_OTA_PORT ;
// Emoncms
strncpy(config.emoncms.host, server.arg("emon_host").c_str(), CFG_EMON_HOST_SIZE );
strncpy(config.emoncms.url, server.arg("emon_url").c_str(), CFG_EMON_URL_SIZE );
strncpy(config.emoncms.apikey, server.arg("emon_apikey").c_str(),CFG_EMON_APIKEY_SIZE );
itemp = server.arg("emon_node").toInt();
config.emoncms.node = (itemp>=0 && itemp<=255) ? itemp : 0 ;
itemp = server.arg("emon_port").toInt();
config.emoncms.port = (itemp>=0 && itemp<=65535) ? itemp : CFG_EMON_DEFAULT_PORT ;
itemp = server.arg("emon_freq").toInt();
if (itemp>0 && itemp<=86400){
// Emoncms Update if needed
Tick_emoncms.detach();
Tick_emoncms.attach(itemp, Task_emoncms);
} else {
itemp = 0 ;
}
config.emoncms.freq = itemp;
// jeedom
strncpy(config.jeedom.host, server.arg("jdom_host").c_str(), CFG_JDOM_HOST_SIZE );
strncpy(config.jeedom.url, server.arg("jdom_url").c_str(), CFG_JDOM_URL_SIZE );
strncpy(config.jeedom.apikey, server.arg("jdom_apikey").c_str(),CFG_JDOM_APIKEY_SIZE );
strncpy(config.jeedom.adco, server.arg("jdom_adco").c_str(),CFG_JDOM_ADCO_SIZE );
itemp = server.arg("jdom_port").toInt();
config.jeedom.port = (itemp>=0 && itemp<=65535) ? itemp : CFG_JDOM_DEFAULT_PORT ;
itemp = server.arg("jdom_freq").toInt();
if (itemp>0 && itemp<=86400){
// Emoncms Update if needed
Tick_jeedom.detach();
Tick_jeedom.attach(itemp, Task_jeedom);
} else {
itemp = 0 ;
}
config.jeedom.freq = itemp;
if ( saveConfig() ) {
ret = 200;
response = PSTR("OK");
} else {
ret = 412;
response = PSTR("Unable to save configuration");
}
showConfig();
}
else
{
ret = 400;
response = PSTR("Missing Form Field");
}
DebugF("Sending response ");
Debug(ret);
Debug(":");
Debugln(response);
server.send ( ret, "text/plain", response);
LedBluOFF();
}
/* ======================================================================
Function: handleRoot
Purpose : handle main page /
Input : -
Output : -
Comments: -
====================================================================== */
void handleRoot(void)
{
LedBluON();
handleFileRead("/");
LedBluOFF();
}
/* ======================================================================
Function: formatNumberJSON
Purpose : check if data value is full number and send correct JSON format
Input : String where to add response
char * value to check
Output : -
Comments: 00150 => 150
ADCO => "ADCO"
1 => 1
====================================================================== */
void formatNumberJSON( String &response, char * value)
{
// we have at least something ?
if (value && strlen(value))
{
boolean isNumber = true;
uint8_t c;
char * p = value;
// just to be sure
if (strlen(p)<=16) {
// check if value is number
while (*p && isNumber) {
if ( *p < '0' || *p > '9' )
isNumber = false;
p++;
}
// this will add "" on not number values
if (!isNumber) {
response += '\"' ;
response += value ;
response += F("\"") ;
} else {
// this will remove leading zero on numbers
p = value;
while (*p=='0' && *(p+1) )
p++;
response += p ;
}
} else {
Debugln(F("formatNumberJSON error!"));
}
}
}
/* ======================================================================
Function: tinfoJSONTable
Purpose : dump all teleinfo values in JSON table format for browser
Input : linked list pointer on the concerned data
true to dump all values, false for only modified ones
Output : -
Comments: -
====================================================================== */
void tinfoJSONTable(void)
{
ValueList * me = tinfo.getList();
String response = "";
// Just to debug where we are
Debug(F("Serving /tinfo page...\r\n"));
// Got at least one ?
if (me) {
uint8_t index=0;
boolean first_item = true;
// Json start
response += F("[\r\n");
// Loop thru the node
while (me->next) {
// we're there
ESP.wdtFeed();
// go to next node
me = me->next;
// First item do not add , separator
if (first_item)
first_item = false;
else
response += F(",\r\n");
/*
Debug(F("(")) ;
Debug(++index) ;
Debug(F(") ")) ;
if (me->name) Debug(me->name) ;
else Debug(F("NULL")) ;
Debug(F("=")) ;
if (me->value) Debug(me->value) ;
else Debug(F("NULL")) ;
Debug(F(" '")) ;
Debug(me->checksum) ;
Debug(F("' "));
// Flags management
if ( me->flags) {
Debug(F("Flags:0x"));
Debugf("%02X => ", me->flags);
if ( me->flags & TINFO_FLAGS_EXIST)
Debug(F("Exist ")) ;
if ( me->flags & TINFO_FLAGS_UPDATED)
Debug(F("Updated ")) ;
if ( me->flags & TINFO_FLAGS_ADDED)
Debug(F("New ")) ;
}
Debugln() ;
*/
response += F("{\"na\":\"");
response += me->name ;
response += F("\", \"va\":\"") ;
response += me->value;
response += F("\", \"ck\":\"") ;
if (me->checksum == '"' || me->checksum == '\\' || me->checksum == '/')
response += '\\';
response += (char) me->checksum;
response += F("\", \"fl\":");
response += me->flags ;
response += '}' ;
}
// Json end
response += F("\r\n]");
} else {
Debugln(F("sending 404..."));
server.send ( 404, "text/plain", "No data" );
}
Debug(F("sending..."));
server.send ( 200, "text/json", response );
Debugln(F("OK!"));
}
/* ======================================================================
Function: getSysJSONData
Purpose : Return JSON string containing system data
Input : Response String
Output : -
Comments: -
====================================================================== */
void getSysJSONData(String & response)
{
response = "";
char buffer[32];
int32_t adc = ( 1000 * analogRead(A0) / 1024 );
// Json start
response += F("[\r\n");
response += "{\"na\":\"Uptime\",\"va\":\"";
response += sysinfo.sys_uptime;
response += "\"},\r\n";
response += "{\"na\":\"WifInfo Version\",\"va\":\"" WIFINFO_VERSION "\"},\r\n";
response += "{\"na\":\"Compile le\",\"va\":\"" __DATE__ " " __TIME__ "\"},\r\n";
response += "{\"na\":\"SDK Version\",\"va\":\"";
response += system_get_sdk_version() ;
response += "\"},\r\n";
response += "{\"na\":\"Chip ID\",\"va\":\"";
sprintf_P(buffer, "0x%0X",system_get_chip_id() );
response += buffer ;
response += "\"},\r\n";
response += "{\"na\":\"Boot Version\",\"va\":\"";
sprintf_P(buffer, "0x%0X",system_get_boot_version() );
response += buffer ;
response += "\"},\r\n";
response += "{\"na\":\"Flash Real Size\",\"va\":\"";
response += formatSize(ESP.getFlashChipRealSize()) ;
response += "\"},\r\n";
response += "{\"na\":\"Firmware Size\",\"va\":\"";
response += formatSize(ESP.getSketchSize()) ;
response += "\"},\r\n";
response += "{\"na\":\"Free Size\",\"va\":\"";
response += formatSize(ESP.getFreeSketchSpace()) ;
response += "\"},\r\n";
response += "{\"na\":\"Analog\",\"va\":\"";
adc = ( (1000 * analogRead(A0)) / 1024);
sprintf_P( buffer, PSTR("%d mV"), adc);
response += buffer ;
response += "\"},\r\n";
FSInfo info;
SPIFFS.info(info);
response += "{\"na\":\"SPIFFS Total\",\"va\":\"";
response += formatSize(info.totalBytes) ;
response += "\"},\r\n";
response += "{\"na\":\"SPIFFS Used\",\"va\":\"";
response += formatSize(info.usedBytes) ;
response += "\"},\r\n";
response += "{\"na\":\"SPIFFS Occupation\",\"va\":\"";
sprintf_P(buffer, "%d%%",100*info.usedBytes/info.totalBytes);
response += buffer ;
response += "\"},\r\n";
// Free mem should be last one
response += "{\"na\":\"Free Ram\",\"va\":\"";
response += formatSize(system_get_free_heap_size()) ;
response += "\"}\r\n"; // Last don't have comma at end
// Json end
response += F("]\r\n");
}
/* ======================================================================
Function: sysJSONTable
Purpose : dump all sysinfo values in JSON table format for browser
Input : -
Output : -
Comments: -
====================================================================== */
void sysJSONTable()
{
String response = "";
getSysJSONData(response);
// Just to debug where we are
Debug(F("Serving /system page..."));
server.send ( 200, "text/json", response );
Debugln(F("Ok!"));
}
/* ======================================================================
Function: getConfigJSONData
Purpose : Return JSON string containing configuration data
Input : Response String
Output : -
Comments: -
====================================================================== */
void getConfJSONData(String & r)
{
// Json start
r = FPSTR(FP_JSON_START);
r+="\"";
r+=CFG_FORM_SSID; r+=FPSTR(FP_QCQ); r+=config.ssid; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_PSK; r+=FPSTR(FP_QCQ); r+=config.psk; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_HOST; r+=FPSTR(FP_QCQ); r+=config.host; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_AP_PSK; r+=FPSTR(FP_QCQ); r+=config.ap_psk; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_EMON_HOST; r+=FPSTR(FP_QCQ); r+=config.emoncms.host; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_EMON_PORT; r+=FPSTR(FP_QCQ); r+=config.emoncms.port; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_EMON_URL; r+=FPSTR(FP_QCQ); r+=config.emoncms.url; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_EMON_KEY; r+=FPSTR(FP_QCQ); r+=config.emoncms.apikey; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_EMON_NODE; r+=FPSTR(FP_QCQ); r+=config.emoncms.node; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_EMON_FREQ; r+=FPSTR(FP_QCQ); r+=config.emoncms.freq; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_OTA_AUTH; r+=FPSTR(FP_QCQ); r+=config.ota_auth; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_OTA_PORT; r+=FPSTR(FP_QCQ); r+=config.ota_port; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_JDOM_HOST; r+=FPSTR(FP_QCQ); r+=config.jeedom.host; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_JDOM_PORT; r+=FPSTR(FP_QCQ); r+=config.jeedom.port; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_JDOM_URL; r+=FPSTR(FP_QCQ); r+=config.jeedom.url; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_JDOM_KEY; r+=FPSTR(FP_QCQ); r+=config.jeedom.apikey; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_JDOM_ADCO; r+=FPSTR(FP_QCQ); r+=config.jeedom.adco; r+= FPSTR(FP_QCNL);
r+=CFG_FORM_JDOM_FREQ; r+=FPSTR(FP_QCQ); r+=config.jeedom.freq;
r+= F("\"");
// Json end
r += FPSTR(FP_JSON_END);
}
/* ======================================================================
Function: confJSONTable
Purpose : dump all config values in JSON table format for browser
Input : -
Output : -
Comments: -
====================================================================== */
void confJSONTable()
{
String response = "";
getConfJSONData(response);
// Just to debug where we are
Debug(F("Serving /config page..."));
server.send ( 200, "text/json", response );
Debugln(F("Ok!"));
}
/* ======================================================================
Function: getSpiffsJSONData
Purpose : Return JSON string containing list of SPIFFS files
Input : Response String
Output : -
Comments: -
====================================================================== */
void getSpiffsJSONData(String & response)
{
char buffer[32];
bool first_item = true;
// Json start
response = FPSTR(FP_JSON_START);
// Files Array
response += F("\"files\":[\r\n");
// Loop trough all files
Dir dir = SPIFFS.openDir("/");
while (dir.next()) {
String fileName = dir.fileName();
size_t fileSize = dir.fileSize();
if (first_item)
first_item=false;
else
response += ",";
response += F("{\"na\":\"");
response += fileName.c_str();
response += F("\",\"va\":\"");
response += fileSize;
response += F("\"}\r\n");
}
response += F("],\r\n");
// SPIFFS File system array
response += F("\"spiffs\":[\r\n{");
// Get SPIFFS File system informations
FSInfo info;
SPIFFS.info(info);
response += F("\"Total\":");
response += info.totalBytes ;
response += F(", \"Used\":");
response += info.usedBytes ;
response += F(", \"ram\":");
response += system_get_free_heap_size() ;
response += F("}\r\n]");
// Json end
response += FPSTR(FP_JSON_END);
}
/* ======================================================================
Function: spiffsJSONTable
Purpose : dump all spiffs system in JSON table format for browser
Input : -
Output : -
Comments: -
====================================================================== */
void spiffsJSONTable()
{
String response = "";
getSpiffsJSONData(response);
server.send ( 200, "text/json", response );
}
/* ======================================================================
Function: sendJSON
Purpose : dump all values in JSON
Input : linked list pointer on the concerned data
true to dump all values, false for only modified ones
Output : -
Comments: -
====================================================================== */
void sendJSON(void)
{
ValueList * me = tinfo.getList();
String response = "";
// Got at least one ?
if (me) {
// Json start
response += FPSTR(FP_JSON_START);
response += F("\"_UPTIME\":");
response += seconds;
// Loop thru the node
while (me->next) {
// go to next node
me = me->next;
response += F(",\"") ;
response += me->name ;
response += F("\":") ;
formatNumberJSON(response, me->value);
}
// Json end
response += FPSTR(FP_JSON_END) ;
} else {
server.send ( 404, "text/plain", "No data" );
}
server.send ( 200, "text/json", response );
}
/* ======================================================================
Function: wifiScanJSON
Purpose : scan Wifi Access Point and return JSON code
Input : -
Output : -
Comments: -
====================================================================== */
void wifiScanJSON(void)
{
String response = "";
bool first = true;
// Just to debug where we are
Debug(F("Serving /wifiscan page..."));
int n = WiFi.scanNetworks();
// Json start
response += F("[\r\n");
for (uint8_t i = 0; i < n; ++i)
{
int8_t rssi = WiFi.RSSI(i);
uint8_t percent;
// dBm to Quality
if(rssi<=-100) percent = 0;
else if (rssi>=-50) percent = 100;
else percent = 2 * (rssi + 100);
if (first)
first = false;
else
response += F(",");
response += F("{\"ssid\":\"");
response += WiFi.SSID(i);
response += F("\",\"rssi\":") ;
response += rssi;
response += FPSTR(FP_JSON_END);
}
// Json end
response += FPSTR("]\r\n");
Debug(F("sending..."));
server.send ( 200, "text/json", response );
Debugln(F("Ok!"));
}
/* ======================================================================
Function: handleFactoryReset
Purpose : reset the module to factory settingd
Input : -
Output : -
Comments: -
====================================================================== */
void handleFactoryReset(void)
{
// Just to debug where we are
Debug(F("Serving /factory_reset page..."));
ResetConfig();
ESP.eraseConfig();
Debug(F("sending..."));
server.send ( 200, "text/plain", FPSTR(FP_RESTART) );
Debugln(F("Ok!"));
delay(1000);
ESP.restart();
while (true)
delay(1);
}
/* ======================================================================
Function: handleReset
Purpose : reset the module
Input : -
Output : -
Comments: -
====================================================================== */
void handleReset(void)
{
// Just to debug where we are
Debug(F("Serving /reset page..."));
Debug(F("sending..."));
server.send ( 200, "text/plain", FPSTR(FP_RESTART) );
Debugln(F("Ok!"));
delay(1000);
ESP.restart();
while (true)
delay(1);
}
/* ======================================================================
Function: handleNotFound
Purpose : default WEB routing when URI is not found
Input : -
Output : -
Comments: -
====================================================================== */
void handleNotFound(void)
{
String response = "";
boolean found = false;
// Led on
LedBluON();
// try to return SPIFFS file
found = handleFileRead(server.uri());
// Try Teleinfo ETIQUETTE
if (!found) {
// We check for an known label
ValueList * me = tinfo.getList();
const char * uri;
// convert uri to char * for compare
uri = server.uri().c_str();
Debugf("handleNotFound(%s)\r\n", uri);
// Got at least one and consistent URI ?
if (me && uri && *uri=='/' && *++uri ) {
// Loop thru the linked list of values
while (me->next && !found) {
// go to next node
me = me->next;
//Debugf("compare to '%s' ", me->name);
// Do we have this one ?
if (stricmp (me->name, uri) == 0 )
{
// no need to continue
found = true;
// Add to respone
response += F("{\"") ;
response += me->name ;
response += F("\":") ;
formatNumberJSON(response, me->value);
response += F("}\r\n");
}
}
}
// Got it, send json
if (found)
server.send ( 200, "text/json", response );
}
// All trys failed
if (!found) {
// send error message in plain text
String message = F("File Not Found\n\n");
message += F("URI: ");
message += server.uri();
message += F("\nMethod: ");
message += ( server.method() == HTTP_GET ) ? "GET" : "POST";
message += F("\nArguments: ");
message += server.args();
message += FPSTR(FP_NL);
for ( uint8_t i = 0; i < server.args(); i++ ) {
message += " " + server.argName ( i ) + ": " + server.arg ( i ) + FPSTR(FP_NL);
}
server.send ( 404, "text/plain", message );
}
// Led off
LedBluOFF();
}