Internet de les coses amb ESP32 i ESP8266

Exemples Referència Plaques   Recursos CITCEA
Projectes Programació Perifèrics   Inici

Termòstat

Programa del grup 3

En aquest cas el microcontrolador funciona com a client i es comunica amb dues taules de Google. En una de les taules es van guardant periòdicament els valors reals de temperatura i humitat. En l'altra s'hi guarda la temperatura de consigna i el mode de funcionament triat. El programa del microcontrolador és el següent:

// PROJECTE I Termòstat IOT 
// El termòstat té 4 modalitats de funcionament:
// I / i - estem a casa (IN): temp. cosigna val 21 °C
// O / o - no estem a casa (OUT): temp. consigna val 15 °C
// H / h - estem de vacances (HOLIDAY): temp. de consigna val 5 °C
// 5<T<30 - podem escollir una temperatura T qualsevol sempre i quan estigui compresa entre 5 °C i 30°C
//
// la modalitat desitjada s'escull a través de google calendar, on existeix la possibilitat de crear una rutina
//
// la informació sobre com està funcionant la caldera es pot consultar a través de la aplicació que hem dissenyat
//
#include <Adafruit_NeoPixel.h>
#include <SPI.h>    // Carreguem la biblioteca SPI
#include <WiFiNINA.h>    // Carreguem la biblioteca WiFiNINA
#include "DHT.h"
#define DHTPIN 2
#define DHTTYPE DHT22
#define server_len 50
#define pag_len 400
#define mis_len 2  // Nombre màxim de línies de la resposta
//definim les variables que utilitzarem
float consTemp = 15.0;
int estat = 0;
//variables connexió internet
const char idXarxa[] = "xarxa-wifi";    // Nom del punt d'accés 
const char contrasenya[] = "contrasenya-wifi";    // Contrasenya de connexió 
//variables de connexió a servidors
//google calendar
char server[server_len];
char pagina[pag_len];
unsigned long darreraConnexio = 0;
const unsigned long periodeConnexio = 10000UL;
bool pendent, completa, redir;
bool ara = false;
bool ini_msg = false;
int comp_lin = 0;
String peticio = "";    // Aquí guardarem una línia de la petició del client
String peticioAux = "";    // la petició anterior (també ho farem servir de reserva)
String missatge[mis_len];  // Aquí hi guardarem el text rebut
String server1, pagina1;
int status = WL_IDLE_STATUS;
String modalitat1; //ens indicarà quin mode de funcionament desitja el client (indicat al calendar)
int modalitat;
String horaInici1; //ens indicarà l'hora d'inici de la modalitat seleccionada
float horaInici;
String horaFinal1; //ens indicarà l'hora de finalització de la modalitat seleccionada
float horaFinal;
String horaActual1; //ens indica l'hora actual
float horaActual;
//Variables per escriure al full de cálcul (app)
const char formulari[] = "1zF9RrmMr-u7375oNP-ipsACeXpnFr96WaAQvlmVgMBU";
const char adrePost[] = "/forms/u/1/d/e/1FAIpQLSfNU0DqezlDe5sZJ0LTfLQSvkylKfmrS2itDQlUA-1Q_Z1RtA/formResponse";
String camp1 = "entry.1004986425";
String camp2 = "entry.1676120579";
String camp3 = "entry.1320931023";
String camp4 = "entry.1218586113";
char serverApp[] = "docs.google.com";
float hum_App, temp_App;
String data;
String estat_App = "OFF";
//canvi: ful de càlcul(app) (false) / calendar(true)
bool canvi = false;
Adafruit_NeoPixel cadena = Adafruit_NeoPixel(3, 1, NEO_GRB + NEO_KHZ800);
DHT dht(DHTPIN, DHTTYPE);
WiFiSSLClient client;
//funció per tractar un missatge
void processa(String missat){
    String Acte, Inici, Final, Actual;    // Aquí guardarem les dades
    Acte = missat.substring(0, missat.indexOf(","));
    missat = missat.substring(missat.indexOf(",") +1);
    Inici = missat.substring(0, missat.indexOf(","));
    missat = missat.substring(missat.indexOf(",") +1);
    Final = missat.substring(0, missat.indexOf(","));
    Actual = missat.substring(missat.indexOf(",") +1);
    Serial.print("Modalitat: ");
    Serial.println(Acte);
    Serial.print("Inici: ");
    Serial.println(Inici);
    Serial.print("Final: ");
    Serial.println(Final);
    Serial.print("Actual: ");
    Serial.println(Actual);
}
void setup() {
    cadena.begin();  // Inicialitza els NeoPixels        
    cadena.show();
    dht.begin();
    Serial.begin(9600);
    cadena.setPixelColor(0, 10, 0, 0);  // pixel vermell encès - calefacció aturada
    cadena.show(); // Actualitza
    estat = 0; // inicialitzem el sistema amb la calefacció en estat aturat
    if (WiFi.status() == WL_NO_MODULE) {
        Serial.println(F("No s'ha trobat el dispositiu Wi-Fi"));
        // La funció F obliga al compilador a guardar el text a la memòria de programa
        while (true);    // Bloquegem el programa
    }
    String versio = WiFi.firmwareVersion();
    if (versio < "1.0.0") {
        Serial.println(F("Convindria actualitzar el firmware"));
    }
    while (status != WL_CONNECTED) {
        Serial.print(F("Connectant a la xarxa "));
        Serial.println(idXarxa);
        status = WiFi.begin(idXarxa, contrasenya);
        delay(10000);    // Ho tornarem a intentar passats 10 s
    }
    Serial.print(F("Connectat a ")); 
    Serial.println(WiFi.SSID());
    Serial.print(F("Estat de la connexió: "));
    Serial.println(WiFi.status()); 
    Serial.print(F("Adreça IP del dispositiu: "));
    Serial.println(WiFi.localIP()); 
    Serial.print(F("Intensitat del senyal: "));
    Serial.print(WiFi.RSSI()); 
    Serial.println(F(" dBm"));
    Serial.println(); 
    Serial.println(F("Anem a connectar al servidor"));
    Serial.println();
    redir = false;
}
void loop() {
    //part que escriu les dades que volem mostrar a l'usuari en un full de càlcul (app)
    if (canvi==false){
        canvi=true;
        if (estat==1){
            estat_App = "ON";
        }
        if (estat==0){
            estat_App = "OFF";
        }
        hum_App = dht.readHumidity();
        temp_App = dht.readTemperature();
        if (status != WiFi.status()) {    // Mirem si ha canviat l'estat de la connexió
            status = WiFi.status();
            if (status == WL_AP_CONNECTED) {
                Serial.println("Dispositiu connectat al punt d'accés");
            } else {
                Serial.println("El dispositiu s'ha desconnectat del punt d'accés");
            }
        }
        data = "";
        data += camp2;
        data += "=";
        data += hum_App;
        data += "&";
        data += camp1;
        data += "=";
        data += temp_App;
        data += "&";
        data += camp3;
        data += "=";
        data += estat_App;
        data += "&";
        data += camp4;
        data += "=";
        data += consTemp;
        data += "&submit=Submit";
        if (client.connect(serverApp, 443)) {
            Serial.println("Connectat");
            client.print("POST ");
            client.print(adrePost);
            client.print("?formkey=");
            client.print(formulari);
            client.println("&ifq HTTP/1.1");
            client.print("Host: ");
            client.println(serverApp);
            client.println("Content-Type: application/x-www-form-urlencoded");
            client.println("Connection: close");
            client.print("Content-Length: ");
            client.println(data.length());
            client.println();
            client.print(data);
            client.println();
            Serial.print("Enviat     T = ");
            Serial.print(temp_App);
            Serial.print("     H = ");
            Serial.println(hum_App);
            Serial.println(estat_App);
            client.stop();
        }
        delay(1000);
        if (!client.connected()) {
            Serial.println();
            Serial.println("Desconnectant");
            client.stop();
        }
    }
    //part que s'encarrega de llegir i interpretar la informació que l'usuari proporciona a través de Google Calendar
    if (canvi==true){
        float hum = dht.readHumidity();
        float temp = dht.readTemperature();
        //control de l'activació de la calefacció segons el valor de la temperatura de consigna
        if (estat == 1 && temp > consTemp+1){     
            cadena.setPixelColor(0, 10, 0, 0);  // pixel vermell encès - calefacció aturada
            cadena.show(); // Actualitza
            estat = 0;
        } else if (estat == 0 && temp < consTemp-1){      
            cadena.setPixelColor(0, 0, 10, 0);  // pixel verd encès - calefacció en marxa
            cadena.show(); // Actualitza
            estat = 1;
        }
        // El bucle principal té tres parts: 
        //     1. Gestió dels caràcters que arriben
        //     2. Tractament de les dades rebudes
        //     3. Nova petició quan ha passat el temps
        while (client.available()) {
            // Gestió dels caràcters que arriben
            // Aquest bucle va guardant els caràcters rebuts
            // i espera al moment en que arriba un salt de línia
            char c = client.read();    // Rebem caràcters del servidor
            if (c == '\n') {    // Mirem si és un salt de línia
                peticioAux = peticio;    // Guardem la petició anterior
                peticio = "";    // Ens preparem per a la línia següent
                completa = true;    // Preparat per tractar-ho
            } else {
                peticio += c;    // Afegim el caràcter rebut
            }
            // Quan ha arribat un salt de línia, hem de mirar què ha arribat
            if (completa){  // Ha arribat una línia completa
                // El nostre script envia un salt de línia al final i, per tant, 
                // totes les dades les podrem anar agafant de peticioAux
                // Si la darrera tasca no tingués salt de línia no ens arribaria 
                // aquí però estaria guardada a peticio
                if ((ini_msg) && (comp_lin < mis_len) && (peticioAux.length() > 1)){
                    // Si la línia ja és la resposta la guardem
                    missatge[comp_lin++] = peticioAux;
                }
                if (peticioAux.startsWith(F("HTTP/1.1 200"))){    // Resposta bona
                    pendent = true;
                    redir = false;
                }
                if (peticioAux.startsWith(F("HTTP/1.1 302"))){    // Redireccionament
                    redir = true;
                }
                if (redir && (peticioAux.startsWith(F("Location:")))){
                    // Si hi ha redireccionament, hem de buscar l'adreça
                    // i extreure'n el servidor i la pàgina
                    String adre = peticioAux.substring(peticioAux.indexOf("//") +2);
                    server1 = adre.substring(0, adre.indexOf(".com") +4);
                    pagina1 = adre.substring(adre.indexOf(".com") +4);
                    server1.toCharArray(server, server_len);
                    pagina1.toCharArray(pagina, pag_len);
                    ara = true;
                }
                if (pendent && peticioAux.startsWith(F("Connection: close"))){
                    ini_msg = true;
                }
                completa = false;
            }
        }
        // Hi ha una resposta per processar
        if (pendent) {
            pendent = false;
            if (comp_lin > 0){
                if (missatge[0].length() > 1){    // Si no està buit
                    //Escrivim també les dades captades pel sensor
                    Serial.print("Temperatura consigna: ");
                    Serial.print(consTemp);
                    Serial.print(" Estat: ");
                    Serial.println(estat);
                    Serial.print("Temperatura: ");
                    Serial.print(temp);
                    Serial.print(" °C");
                    Serial.print(" Humitat: ");
                    Serial.print(hum);
                    Serial.println(" %");
                    Serial.println(" ");                    
                    //Escrivim informació sobre la tasca actual, o la més propera si no en tenim cap d'actual
                    Serial.print(F("--- Tasca "));
                    Serial.print(0+1);
                    Serial.println(F(" ---"));
                    processa(missatge[0]);
                    Serial.println();
                    //variables que utilitzarem per imposar les condicions de canvi de consigna
                    modalitat1 = missatge[0].substring(0, missatge[0].indexOf(","));
                    //modalitat = modalitat1.toInt();
                    Serial.println(modalitat1);
                    missatge[0] = missatge[0].substring(missatge[0].indexOf(",") +1);
                    horaInici1 = missatge[0].substring(0, missatge[0].indexOf(","));
                    horaInici = horaInici1.toFloat();
                    missatge[0] = missatge[0].substring(missatge[0].indexOf(",") +1);
                    horaFinal1 = missatge[0].substring(0, missatge[0].indexOf(","));
                    horaFinal = horaFinal1.toFloat();
                    horaActual1 = missatge[0].substring(missatge[0].indexOf(",") +1);
                    horaActual = horaActual1.toFloat();
                    //condicions de canvi de consigna
                    if (modalitat1 == "I" || modalitat1 == "i"){
                        if (horaInici<=horaActual && horaActual<=horaFinal){
                            consTemp = 21;
                        }
                    }
                    if (modalitat1 == "O" || modalitat1 == "o"){
                        if (horaInici<=horaActual && horaActual<=horaFinal){
                            consTemp = 15;
                        }
                    }
                    if (modalitat1 == "H" || modalitat1 == "h"){
                        if (horaInici<=horaActual && horaActual<=horaFinal){
                            consTemp = 5;
                        }
                    }
                    modalitat = modalitat1.toInt();
                    if (modalitat>5 && modalitat<30 && horaInici<=horaActual && horaActual<=horaFinal){
                        consTemp = modalitat;
                    }
                    if (horaActual<horaInici)  {
                        consTemp = 15;
                    }
                    Serial.print("Nova temperatura consigna: ");
                    Serial.println(consTemp);
                    Serial.println();
                }
            } else {
                Serial.println(F("No queden tasques per al dia d'avui"));
                Serial.println();
                //Escrivim també les dades captades pel sensor
                Serial.print("Temperatura consigna: ");
                Serial.print(consTemp);
                Serial.print(" Estat: ");
                Serial.println(estat);
                Serial.print("Temperatura: ");
                Serial.print(temp);
                Serial.print(" °C");
                Serial.print(" Humitat: ");
                Serial.print(hum);
                Serial.println(" %");
                Serial.println(" "); 
                consTemp = 15;
            }
            canvi=false; // ara que ja hem interpretat totes les dades, permetem que
                         // es faci el canvi a escriure les dades al full de cálcul
        }
        // Quan toca, tornem a fer una petició
        if (ara || ((millis() - darreraConnexio > periodeConnexio))) {
            ini_msg = false;
            comp_lin = 0;
            for (byte k = 0; k < mis_len; k++){
                missatge[k] = "";
            }        
            if (!redir){
                server1 = "script.google.com";
                pagina1 = "/macros/s/AKfycbx4lM1DGpnSH0yJ7G5Dgwn2x6nvSD8dyCRTnMEvt7eoAgKLRmY/exec";
                server1.toCharArray(server, server_len);
                pagina1.toCharArray(pagina, pag_len);
            }
            ara = false;
            client.stop();
            if (client.connect(server, 443)) {
                Serial.println(F("S'ha fet la connexió al servidor"));
                Serial.println();
                client.print(F("GET "));
                client.print(pagina);
                client.println(F(" HTTP/1.1"));
                client.print(F("Host: "));
                client.println(server);
                client.println(F("Connection: close"));
                client.println();
                // Guardem quan hem fet la connexió
                darreraConnexio = millis();
            } else {
                Serial.println(F("connection failed"));
                Serial.println();
            }
        }
    }  
}

També hi ha un script al Google Drive que permetia interactuar amb el calendari en el que es guarda la informació dels modes de funcionament de la caldera.

var idCal = "f2bjf3idp4ihdakjpkj9k5rghc@group.calendar.google.com";
function doGet(e) {
  var salt = "\n";
  var cal = CalendarApp.getCalendarById(idCal);
  if (!cal) {  // Si el calendari no existeix o no tenim permís
    resultat = "Calendari no trobat!";
    return ContentService.createTextOutput(resultat);
  }
  var ara = new Date();  // La data i l'hora del moment d'executar l'script
  var final = new Date();
  final.setHours(23);  // Li canviem l'hora a les 23.59
  final.setMinutes(59);
  // Agafem tots els esdeveniments des d'ara (inclosos els ja iniciats) fins les 23.59 h
  var esdev = cal.getEvents(ara, final);
  var numEsdev = esdev.length;  // Quants n'hi ha?
  var resultat = "";
  if (numEsdev > 0){  // Hi ha, com a mínim, un esdeveniment
    resultat = resultat + dades(esdev[0]) + salt;  // Primer esdeveniment
  }
  if (numEsdev > 1){  // Hi ha, com a mínim, un segon  esdeveniment
    resultat = resultat + salt + dades(esdev[1]) + salt;  // Segon esdeveniment
  }
  return ContentService.createTextOutput(resultat);  // Enviem la resposta
}
function dades(esdAct) {  // Organitza les dades d'un esdeveniment
  var descrip = esdAct.getTitle();  // Títol de l'esdeveniment
  var dataIni = esdAct.getStartTime();  // Data i hora d'inici
  var dataFi = esdAct.getEndTime();  // Data i hora d'acabament
  // Ens interessen només les hores d'inici i acabament
  // i les volem en el format habitual en català
  var ini = dataIni.getHours() + "." + dataIni.getMinutes();
  var fi = dataFi.getHours() + "." + dataFi.getMinutes();
  var ara2 = new Date();
  var Hora = ara2.getHours();
  var Minut = ara2.getMinutes();
  var actual = Hora + "," + Minut;
  var resul = descrip + "," + ini + "," + fi + "," + actual;
  return resul;
}

Aquest grup va fer una aplicació amb App Inventor que permetia interaccionar amb el termòstat. La disposició d'elements a la pantalla és com es mostra a les imatges següents:

Pantalla         Pantalla

El programa d'aquesta pantalla és el següent:

Programa
Programa
Programa

 

 

Llicència de Creative Commons
Aquesta obra d'Oriol Boix està llicenciada sota una llicència no importada Reconeixement-NoComercial-SenseObraDerivada 3.0.