La idea principal de esta entrada es que la estación escriba el dato enviado el mismo a dos lugares: la base de datos Realtime de Firebase y la hoja de cálculo de Google Spreadsheet. La primera para una comunicación fluida con la central y con otros posibles dispositivos en un futuro; la segunda con propósitos de registro y tratamiento de datos.
Tengo dos posibilidades de hacerlo:
- Que el NodeMCU de la estación envíe los datos a ambos lugares. Primero a Firebase y después a Google Spreadsheet.
- Que el NodeMCU de la estación envíe los datos a Google Spreadsheet y el script de la hoja de cálculo envíe a su vez los datos a Firebase.
La primera opción me pareció más adecuada en un principio, pero también es verdad que ocuparía más espacio en la memoria del dispositivo, pues usamos 4 bibliotecas. De todas formas el código presenta muchas instrucciones con salidas al Monitor Serie que se pueden obviar.
Paso pues a indicar los códigos de programación....
NodeMCU
/*
* Created by Aurelio Gallardo Rodríguez
* Based on: K. Suwatchai (Mobizt)
*
* Email: aurelio@seritium.es
*
* ESTACION. Escritura. Envío de información a Firebase y Google Spreadsheet
*
* Julio - 2019
*
*/
//FirebaseESP8266.h must be included before ESP8266WiFi.h
#include "FirebaseESP8266.h"
#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#define FIREBASE_HOST "[mibasededatosfirebase].firebaseio.com"
#define FIREBASE_AUTH "kfcm8934jf7894ch93c092342039h2309h" // token o secreto inventado
#define WIFI_SSID "miSSID"
#define WIFI_PASSWORD "miCONTRASEÑA"
const char* host = "script.google.com"; // Este es el host de los scripts de google.
const int httpsPort = 443;
// Huella digital del script de Google:
// D4:9E:40:F4:53:7A:04:93:38:F7:6B:4B:DC:70:02:A9:03:98:C2:DE
const char* fingerprint = "D4 9E 40 F4 53 7A 04 93 38 F7 6B 4B DC 70 02 A9 03 98 C2 DE";
// const char* fingerprint = "46 B2 C3 44 9C 59 09 8B 01 B6 F8 BD 4C FB 00 74 91 2F EF F6";
String googleSheetsID = "[Identificación de Google web app]"; // El que me da al implementar una aplicación web en el script.
// objeto comunicación con Google Spreadsheet
WiFiClientSecure cliente;
//Define FirebaseESP8266 data object
FirebaseData bd;
DynamicJsonDocument doc(1024);
String path = "/Estaciones"; // path a FireBase
String estacion = "A"; // Estacion que voy a controlar
int i=1; // contador general
int activo=0;
#define LED 5 // D1(gpio5)
#define BUTTON 4 //D2(gpio4)
int estado=0;
int estadoAnterior=0;
int estadoLuz=0;
void setup()
{
Serial.begin(115200);
pinMode(LED, OUTPUT);
pinMode(BUTTON, INPUT);
// conectando a la WIFI
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Conectando a la WiFi");
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(300);
}
Serial.println();
Serial.print("Conectado con IP: ");
Serial.println(WiFi.localIP());
Serial.println();
// Conectando a la bd real Time Firebase
Firebase.begin(FIREBASE_HOST, FIREBASE_AUTH);
Firebase.reconnectWiFi(true);
//Set database read timeout to 1 minute (max 15 minutes)
Firebase.setReadTimeout(bd, 1000 * 60);
//tiny, small, medium, large and unlimited.
//Size and its write timeout e.g. tiny (1s), small (10s), medium (30s) and large (60s).
Firebase.setwriteSizeLimit(bd, "unlimited");
wFB(0); // Escribe un cero en la estación al arrancar
}
// Bucle principal
void loop() {
estado=digitalRead(BUTTON);
if (estado!=estadoAnterior){
// el estadoAnterior ahora es el estado
estadoAnterior=estado;
// si estado es alto, se pasa de bajo a alto
if (estado) {
estadoLuz=!estadoLuz; // El estado de la luz cambia
// sendDataToGoogleSheets(estacion,estadoLuz); // envío a googleSheet esa información
wFB(estadoLuz); // en vez de enviarlo a Google Spreadsheet, se lo envío a Real Time BD Firebase
digitalWrite(LED,estadoLuz);
Serial.println("Estado del LED: "String(estadoLuz)); //Manda al monitor serie ese estado.
delay(10);
sendDataToGoogleSheets(estacion,estadoLuz); // envío a googleSheet esa información
}
}
digitalWrite(LED,estadoLuz);
// digitalWrite(LED,rFB()); // Escribe el estado en un LED, PERO leyéndolo de la BD.
}
// Función que escribe un dato en Firebase.
// Además, actualiza el dato si tiene la misma ruta, no genera un error.
void wFB(int dato) { //Write Data in FB
if (Firebase.setInt(bd,path+"/"+estacion,dato)) {
Serial.println("PASSED");
Serial.println("PATH: " +bd.dataPath());
Serial.println("TYPE: " +bd.dataType());
Serial.println("ETag: " + bd.ETag());
Serial.print("VALUE: ");
if (bd.dataType() == "int")
Serial.println(bd.intData());
else if (bd.dataType() == "float")
Serial.println(bd.floatData(), 5);
else if (bd.dataType() == "double")
printf("%.9lf\n", bd.doubleData());
else if (bd.dataType() == "boolean")
Serial.println(bd.boolData() == 1 ? "true" : "false");
else if (bd.dataType() == "string")
Serial.println(bd.stringData());
else if (bd.dataType() == "json")
Serial.println(bd.jsonData());
Serial.println("------------------------------------");
Serial.println();
} else {
Serial.println("FAILED");
Serial.println("REASON: "+ bd.errorReason());
Serial.println("------------------------------------");
Serial.println();
}
}
// Función de lectura
int rFB(){// lee el dato de la entrada correspondiente
if (Firebase.getInt(bd, path+"/+"estacion)) {
if (bd.dataType() == "int") {
Serial.println("Dato leído: "+ (String) bd.intData());
return bd.intData();
}
} else {
Serial.println(bd.errorReason());
}
}
/* Función de conexión. Importante la instrucción cliente.setInsecure(); para conectar de forma anónima
*/
void sendDataToGoogleSheets(String nombreEstacion, int Activacion) {
String cadena="";
Serial.print("Conectando a: ");
Serial.println(host);
// cliente.setInsecure();
if (!cliente.connect(host, httpsPort)) {
Serial.println("Falla la conexión a Google Sheets -->" String(host) ": " String(httpsPort) );
return;
}
if (cliente.verify(fingerprint, host)) {
Serial.println("Certificado OK");
}
else {
Serial.println("Debes comprobar certificado");
}
String url = "/macros/s/"+ googleSheetsID + "/exec?estacion="+ nombreEstacion+ "&activo="+ (String) Activacion;
Serial.print("Petición URL ");
Serial.println(url);
cliente.print(String("GET ") url " HTTP/1.1\r\n"
"Host: " host "\r\n"
"User-Agent: BuildFailureDetectorESP8266\r\n"
"Connection: close\r\n\r\n");
// Es necesario leer las cabeceras
Serial.println("Request enviada");
while (cliente.connected()) {
String line = cliente.readStringUntil('\n');
if (line == "\r") {
Serial.println("Cabeceras Recibidas");
Serial.println(line);
break;
}
}
// lee el contenido de la respuesta y lo almacena en la variable cadena
while (cliente.available()) {
char c = cliente.read();
cadena = cadena (String) c;
}
Serial.println("Respuesta directa del servidor");
Serial.println(cadena);
Serial.println("==============================");
Serial.println();
// PROBLEMA: la respuesta, por motivos de seguridad, no es jamás el texto, o el html, sino que está codificada.
// He resuelto este problema encerrando la respuesta (JSON) entre dos palabras, EMPEZAR y TERMINAR.
// Una vez localizada la información, la extraigo con la función midString.
// Los caracteres, en codificación unicode, los transformo en sus caracteres con varias líneas de código...
// Y voalá... ¡obtiene la respuesta en formato JSON de texto!
String respuesta =midString(cadena,"EMPEZAR","TERMINAR");
char comillas = 34;
respuesta.replace("x7b","{");
respuesta.replace("x7d","}");
respuesta.replace("x22",String(comillas));
respuesta.replace("\\",""); // doble slash para que lo reconozca ??
// Serial.println(respuesta);
Serial.println("Respuesta filtrada desde el servidor");
Serial.println(respuesta);
Serial.println("==============================");
Serial.println();
// Usar bibliteca JSON
deserializeJson(doc,respuesta);
JsonObject obj = doc.as<JsonObject>();
String exito = obj[String("exito")];
String comentario = obj[String("comentario")];
Serial.println("Respuesta extraída en variables deserializando la cadena en un objeto JSON");
Serial.println("Ha tenido éxito: "exito);
Serial.println("Envía el comentario: "comentario);
Serial.println("==============================");
Serial.println();
delay(500);
cliente.stop(); //cierra la conexión
}
/* función texto intermedio */
String midString(String str, String start, String finish){
int locStart = str.indexOf(start);
if (locStart==-1) return "";
locStart += start.length();
int locFinish = str.indexOf(finish, locStart);
if (locFinish==-1) return "";
return str.substring(locStart, locFinish);
}
- El código es una fusión de las llamadas a Firebase y a Google Spreadsheet que hemos visto en anteriores entradas. Lo único reseñable es que el método cliente.setInsecure() genera un error, pero el programa funciona sin él. No sé por qué.
- Solo cambia la definición de estacion="A"; en el programa anterior estaba escrito como estacion="/A".
Script Web App de Google Spreadsheet.
function doGet(e) {
var estacion = e.parameter.estacion; // variables que se han recibido. ESTACION
estacion = estacion.toUpperCase();
var activo = e.parameter.activo; // variables que se han recibido. ACTIVO
var columna = estacion.charCodeAt(0)-62; // Código ascii -62; la "A" es 65, luego da el valor 3, correspondiente a la tercera columna
// Dirección de la base de datos RealTime de Firebase
var bd = "[mibasededatosfirebase].firebaseio.com" ;
// Secreto de la base de datos Firebase
var secreto = 'lkfjweo45roi56oifUIUEoierfioerfncoc'; // inventado
// Ruta de la base de datos RealTime
var ruta = "Estaciones";
var options = { method:'get', contentType: 'application/json' }; //opciones de la llamada a fetch
var firebaseURL = 'https://'+bd+'/'+ruta+'.json?auth='+secreto; // construyo la ruta
if (columna>=3 && columna<=7 && estacion.length==1) { // si es una sola letra, una columna
var resultado = UrlFetchApp.fetch(firebaseURL, options);
// Hoja de cálculo activa
var sheet = SpreadsheetApp.getActiveSheet();
var ultimalinea = sheet.getLastRow(); // obtiene la última línea
// Fechas y horas actuales.
var fecha=Utilities.formatDate(new Date, "GMT+1", "dd/MM/yyyy");
var d = new Date();
var hora = d.toLocaleTimeString();
// Objeto JSON según el resultado
var e = JSON.parse(resultado);
for (clave in e) {
var columna = clave.charCodeAt(0)-62; // columna de cada valor
var valorActual = sheet.getRange(2,columna).getValue();
if (valorActual!=e[clave]) { // Si los valores son distintos
// 1) Cambio fecha y hora en la fila dos
sheet.getRange(2,1).setValue(fecha);
sheet.getRange(2,2).setValue(hora);
// 2) Cambio valor actual en la fila dos
sheet.getRange(2,columna).setValue(parseInt(e[clave]));
// 3) Pongo fecha y hora en la última línea
sheet.getRange(ultimalinea1,1).setValue(fecha);
sheet.getRange(ultimalinea1,2).setValue(hora);
// 4) Escribo en la última línea un registro de lo que acontece
segunActivo(clave,e[clave],ultimalinea1,columna,sheet);
}
// Logging de los datos...
Logger.log(clave);
Logger.log(e[clave]);
} // Fin del for
// *** Refresco ***
SpreadsheetApp.flush();
var output = HtmlService.createHtmlOutput('EMPEZAR'JSON.stringify(construyeJSON("1","Estación "estacion" activo: "activo))'TERMINAR');
// output.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
return output;
} else { // Fin del if que reconoce la columna
Logger.log("Estación fuera del rango");
}
}
// funcion que escribe en el registro según si está activo o no
function segunActivo(estacion,dato,fila,columna,sheet) {
if (dato==1) {
sheet.getRange(fila,columna).setValue("Estación "estacion" se activa");
sheet.getRange(fila,columna).setFontColor("darkgreen");
sheet.getRange(fila,columna).setBackground("#AAFFAA");
} else if (dato==0) {
sheet.getRange(fila,columna).setValue("Estación "estacion" se desactiva");
sheet.getRange(fila,columna).setFontColor("darkred");
sheet.getRange(fila,columna).setBackground("#FFAAAA");
}
}
function construyeJSON(success, comentario) {
var json = {
'exito':success,
'comentario':comentario,
};
return json;
}
- El código del script es también una fusión de scripts a los que ya nos hemos referido.
- Básicamente recibe una petición GET (por eso el script es una función doGet) y extraigo los valores de la estación, y si está activo o no.
- Si tengo un valor correcto de estación, accedo a Firebase y leo los datos registrados (que deberían haberse escrito ANTES desde el NodeMCU), detecta los cambios, y, si estos existen los refleja en la hoja de cálculo.
= = = = = = = = =
No hay comentarios:
Publicar un comentario