ESP8266 HVAC control

Another contribution to the Blynk IoT: an HVAC control for my apartment with a 2-pipe heating and cooling system. Hot or cold water is circulated through the radiator system, and temperature is controlled entirely by a fan that blows over the coils. There are no thermostats - just switches - to control the fans. For this project, I’m using a standalone ESP-01, a DHT11 temperature sensor, and a single relay board, at a total cost of less than $10.

About half of the code is just for getting and storing WiFi credentials. The second half contains the thermostat control. My Blynk dashboard is is intentionally simple, with a single control for setting the temperature, and a pair of displays showing desired and actual temperatures. A variety of additional settings (hysteresis, Summer / Winter modes, sensor offset, etc.) can be changed via the Terminal widget. Settings are all stored on the ESP in case of power cycling. A Home / Away mode is included, but is awaiting Blynk location sensing to be implemented.

/**************************************************************************** 
 *  HVAC control for a "2-pipe" radiator system.
 *  
 *  Compares readings from a DHT11 temperature sensor with desired temperature
 *  from the Blynk application, and runs the fan unit as necessary to heat or 
 *  cool.  Hysteresis levels for both Summer and Winter are independently 
 *  adjustable from 2 to 6 degrees. The temperature sensor readings can be 
 *  offset up or down by up to 5 degrees. All are saved to EEPROM, and 
 *  automatically reloaded on startup.
 *  
 *  Some settings are placeholders, awaiting integration of new Blynk triggers
 *  (e.g. "Home" setting will be toggled by a location trigger).
 *  
 *  The hardware is minimal: an ESP-01, a single relay on GPIO 0, and a DHT11
 *  temperature sensor on GPIO 2.
 *  
*****************************************************************************
*/

#include "ESP8266WiFi.h"
#include <ESP8266mDNS.h>
#include <WiFiClient.h>
#include <BlynkSimpleEsp8266.h>
#include <SimpleTimer.h>
#include "DHT.h"
#include <EEPROM.h>


//Temperature sensor
#define DHTPIN 0     // what digital pin we're connected to
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);


MDNSResponder mdns;
WiFiServer server(80);
SimpleTimer timer;

//WiFi and Blynk connection variables
char auth[] = "fcd3ba73b3434f728387714b8df65ea0"; // Blynk token "YourAuthToken"
const char* APssid = "ESP8266"; // Name of access point

String st;
String rsid;
String rpass;
boolean newSSID = false;

//Thermostat variables
int TempDes = 70;
int PreviousTempDes = 70;
int TempAct = 70;
int TempCorrection = 0;
int UpdateFrequency = 5000; //Update frequency in milliseconds
float LastRead;

int RelayPin = 2; //Relay pin to turn on fan

int Hysteresis_W = 2; //Summer and Winter hysteresis levels
int Hysteresis_S = 2;

boolean Winter = true; 
boolean Home = true;
boolean FirstRead = true; //flag to cycle DHT until a good first read is made

int MenuItem = 0;

// Attach virtual serial terminal to Virtual Pin V1
WidgetTerminal terminal(V2);


void setup() {
  dht.begin(); //Start temperature sensor
  delay(1000);

  pinMode(RelayPin,OUTPUT);
  digitalWrite(RelayPin,HIGH);
 

  Serial.begin(115200);
  delay(10);
  Serial.println("Startup");
  Serial.println("");
  
  EEPROM.begin(20);  //Get saved temperature correction from EEPROM
  Serial.println("Loading presets from EEPROM");
  GetPresets();

  // if the stored SSID and password connected successfully, exit setup
  if ( testWifi()) {

          //Frequency of temperature reads and updates from Blynk
          timer.setInterval(UpdateFrequency, TempUpdate);
           
          Blynk.config(auth);
          while (Blynk.connect() == false) {
            // Wait until connected
          }
          terminal.println("PRESS SETTINGS BUTTON TO ACCESS MENU");
          terminal.println("");
          terminal.println("");
          terminal.flush();
          return;
      }
  // otherwise, set up an access point to input SSID and password     
  else
      Serial.println("");
      Serial.println("Connect timed out, opening AP"); 
      setupAP();
}

// WiFi connection ***********************************************************
//****************************************************************************

int testWifi(void) {
  int c = 0;
  Serial.println("Waiting for Wifi to connect");  
  while ( c < 20 ) {
    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("WiFi connected.");
      return(1); 
      }      
    delay(500);
    Serial.print(WiFi.status());    
    c++;
  }
  return(0);
} 

void launchWeb(int webtype) {
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.println(WiFi.softAPIP());
    
    // Start the server
    server.begin();
    Serial.println("Server started");   
    int b = 20;
    int c = 0;
    while(b == 20) { 
       b = mdns1(webtype);

       //If a new SSID and Password were sent, close the AP, and connect to local WIFI
       if (newSSID == true){
          newSSID = false;

          //convert SSID and Password sting to char
          char ssid[rsid.length()];
          rsid.toCharArray(ssid, rsid.length());         
          char pass[rpass.length()];
          rpass.toCharArray(pass, rpass.length());

          Serial.println("Connecting to local Wifi");
          delay(500);
    
          WiFi.begin(ssid,pass);
          delay(1000);
          if ( testWifi()) {
 //           Blynk.config(auth);
          ESP.restart();
            return;
          }

         else{
            Serial.println("");
            Serial.println("New SSID or Password failed. Reconnect to server, and try again.");
            setupAP();
            return;
         }
       }
     }
}


void setupAP(void) {
  
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(100);
  int n = WiFi.scanNetworks();
  Serial.println("scan done");
  if (n == 0)
    Serial.println("no networks found");
  else
  {
    Serial.print(n);
    Serial.println(" networks found");
  }
  Serial.println(""); 
  st = "<ul>";
  for (int i = 0; i < n; ++i)
    {
      // Print SSID and RSSI for each network found
      st += "<li>";
      st += WiFi.SSID(i);
      st += " (";
      st += WiFi.RSSI(i);
      st += ")";
      st += (WiFi.encryptionType(i) == ENC_TYPE_NONE)?" ":"*";
      st += "</li>";
    }
  st += "</ul>";
  delay(100);
  WiFi.softAP(APssid);
  Serial.println("softAP");
  Serial.println("");
  launchWeb(1);
  WiFi.softAPdisconnect(true); // kill softAP after completing WiFi connection
}


String urldecode(const char *src){ //fix encoding
  String decoded = "";
    char a, b;
    
  while (*src) {     
    if ((*src == '%') && ((a = src[1]) && (b = src[2])) && (isxdigit(a) && isxdigit(b))) {      
      if (a >= 'a')
        a -= 'a'-'A';       
      if (a >= 'A')                
        a -= ('A' - 10);                   
      else               
        a -= '0';                  
      if (b >= 'a')                
        b -= 'a'-'A';           
      if (b >= 'A')                
        b -= ('A' - 10);            
      else                
        b -= '0';                        
      decoded += char(16*a+b);            
      src+=3;        
    } 
    else if (*src == '+') {
      decoded += ' ';           
      *src++;       
    }  
    else {
      decoded += *src;           
      *src++;        
    }    
  }
  decoded += '\0';        
  return decoded;
}


int mdns1(int webtype){
  
  // Check if a client has connected
  WiFiClient client = server.available();
  if (!client) {
    return(20);
  }
  Serial.println("");
  Serial.println("New client");

  // Wait for data from client to become available
  while(client.connected() && !client.available()){
    delay(1);
   }
  
  // Read the first line of HTTP request
  String req = client.readStringUntil('\r');
  
  // First line of HTTP request looks like "GET /path HTTP/1.1"
  // Retrieve the "/path" part by finding the spaces
  int addr_start = req.indexOf(' ');
  int addr_end = req.indexOf(' ', addr_start + 1);
  if (addr_start == -1 || addr_end == -1) {
    Serial.print("Invalid request: ");
    Serial.println(req);
    return(20);
   }
  req = req.substring(addr_start + 1, addr_end);
  Serial.print("Request: ");
  Serial.println(req);
  client.flush(); 
  String s;
  if ( webtype == 1 ) {
      if (req == "/")
      {
        IPAddress ip = WiFi.softAPIP();
        String ipStr = String(ip[0]) + '.' + String(ip[1]) + '.' + String(ip[2]) + '.' + String(ip[3]);
        s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>";
        s += "<font face='arial,helvetica' size='7'>";
        s += "<b><label>Hello from ESP8266 at ";
        s += ipStr;
        s += "</label></b><p>";
        s += st;
        s += "<form method='get' action='a'><label>SSID: </label><input name='ssid' style='width:200px; height:60px; font-size:50px;'>   ";
        s += "<label>Password: </label><input name='pass' style='width:200px; height:60px; font-size:50px;'>";
  //      s += "<p><label>Blynk Token (optional): </label><input name='token' style='width:200px; height:60px; font-size:50px;'>";
        s += "<p><input type='submit' style='font-size:60px'></form>";
        s += "</html>\r\n\r\n";
        Serial.println("Sending 200");
      }
      else if ( req.startsWith("/a?ssid=") ) {

        newSSID = true;
        String qsid; //WiFi SSID 
        qsid = urldecode(req.substring(8,req.indexOf('&')).c_str()); //correct coding for spaces as "+"
        Serial.println(qsid);
        Serial.println("");
        rsid = qsid;
        
        String qpass; //Wifi Password
        qpass = urldecode(req.substring(req.lastIndexOf('=')+1).c_str());//correct for coding spaces as "+"
        Serial.println(qpass);
        Serial.println("");
        rpass = qpass;
 
        s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>";
        s += "<font face='arial,helvetica' size='7'><b>Hello from ESP8266 </b>";
        s += "<p> New SSID and Password received</html>\r\n\r\n"; 
      }
      else
      {
        s = "HTTP/1.1 404 Not Found\r\n\r\n";
        Serial.println("Sending 404");
      }
  } 
  else
  {
      if (req == "/")
      {
        s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>";
        s += "<font face='arial,helvetica' size='7'>Hello from ESP8266";
        s += "<p>";
        s += "</html>\r\n\r\n";
        Serial.println("Sending 200");
      }
      else
      {
        s = "HTTP/1.1 404 Not Found\r\n\r\n";
        Serial.println("Sending 404");
      }       
  }
  client.print(s);
  Serial.println("Done with client");
  return(20);
}

//HVAC Control*********************************************************************
//*********************************************************************************

//Match temp gauge to slider in Blynk app 
BLYNK_WRITE(3){
  TempDes = param.asInt();
  Blynk.virtualWrite(1,TempDes);
}


// Turn the radiator fan on or off 
void TempUpdate (){
  float ReadF = dht.readTemperature(true);
    
  if (isnan(ReadF)) {
    Serial.println("Failed to read from DHT sensor!");
    return;
  }
  
  if (FirstRead == true){
    TempAct = (int)(ReadF + TempCorrection);
    FirstRead = false;
    Serial.print("First temperature reading (corrected): ");
    Serial.println(TempAct);
    LastRead = ReadF;
    return;   
  }
    
  else   { //Read gets averaged with previous read and limited to 1 degree at a time change 
    int TempAvg = (int)((ReadF + LastRead + (2 * TempCorrection))/2);
    if (TempAvg >= TempAct + 1){
      TempAct = TempAct + 1;
    }
    if (TempAvg <= TempAct - 1){
      TempAct = TempAct -1;
    }

    LastRead = ReadF;
  }
  Blynk.virtualWrite(0,TempAct); //Report actual temperature in app
  Serial.print("Actual temperature (corrected): ");
  Serial.println(TempAct);

  if (Winter){
    if (TempAct < TempDes){
      digitalWrite(RelayPin,LOW);
    }
    else if (TempAct >= (TempDes + Hysteresis_W)) {
    digitalWrite(RelayPin,HIGH);
    }
  }
  else if (!Winter){
    if (TempAct > TempDes){
    digitalWrite(RelayPin,LOW);
    }
    else if (TempAct <= (TempDes - Hysteresis_S)){
      digitalWrite(RelayPin,HIGH);
    }
  else{
    digitalWrite(RelayPin,HIGH);
  }
 }

 if (TempDes != PreviousTempDes){ //update the EEPROM if desired temperature had changed.
  EEPROM.write(3,TempDes);
  EEPROM.commit();
  Serial.print("New desired temperature saved to EEPROM: ");
  Serial.println(TempDes);
  PreviousTempDes = TempDes;  
 }
}


// Menu button. Selects settings menu item. 
BLYNK_WRITE(V4) {
  if (param.asInt()){
    MenuItem += 1;
    if (MenuItem > 7){
      MenuItem = 1;
    }
    switch(MenuItem){
      case 1:
        if (Winter){
          terminal.println("Mode: Winter / heating. CHANGE?");
        }
        else terminal.println("Mode: Summer / cooling. CHANGE?");
        break;

      case 2:
        if (Winter){
          terminal.print("Winter hysteresis: ");
          terminal.print(Hysteresis_W);
          terminal.println(" degrees. CHANGE?");   
        }
        else{
          terminal.print("Summer hysteresis: ");
          terminal.print(Hysteresis_S);
          terminal.println(" degrees. CHANGE?");    
        }
        break;

      case 3:
        terminal.print("Sensor correction: ");
        terminal.print(TempCorrection);
        terminal.println("degree(s). CHANGE?");
        break;

      case 4:
        if (Home){
          terminal.println("Location: home. CHANGE?");
        }
        else terminal.println("Location: away. CHANGE?");
        break;

      case 5:
        terminal.println("CLEAR WiFi SETTINGS?");
        break;

      case 6:
         terminal.println("RESET THERMOSTAT DEFAULTS?");
         break;
        
      case 7:
        terminal.println("EXIT SETTINGS?");
    }
  }
  // Move to top of terminal window, and ensure everything is sent
  terminal.println("");
  terminal.println("");
  terminal.flush();
}

//Select button. Executes change of selected menu item 
BLYNK_WRITE(V5){
  if ((MenuItem > 0) && (param.asInt())){
    switch(MenuItem){
      //Change season
      case 1:
        if (Winter){
          terminal.println("Mode: Summer / cooling. CHANGE?");
          Winter = false;
          EEPROM.write(4,0);
          EEPROM.commit();
        }
        else {
          terminal.println("Mode: Winter / heating. CHANGE?");
          Winter = true;
          EEPROM.write(4,1);
          EEPROM.commit();
        } 
        break;
        
      //Change hysteresis level of currently selected season
      case 2:
        if (Winter){
          Hysteresis_W += 1;
          if (Hysteresis_W > 6){
            Hysteresis_W = 1;
          }
          EEPROM.write(1,(Hysteresis_W));
          EEPROM.commit();
          terminal.print("Winter hysteresis: ");
          terminal.print(Hysteresis_W);
          terminal.println(" degrees. CHANGE?");
        }
        else{
          Hysteresis_S += 1;
          if (Hysteresis_S > 6){
            Hysteresis_S = 1;
          }
          EEPROM.write(1,(Hysteresis_S));
          EEPROM.commit();
          terminal.print("Summer hysteresis: ");
          terminal.print(Hysteresis_S);
          terminal.println(" degrees. CHANGE?");
          }
        break;

      case 3:
        TempCorrection +=1;
        if (TempCorrection > 5){
          TempCorrection = -5;
        }
        EEPROM.write(0,(TempCorrection + 5));
        EEPROM.commit();
        terminal.print("Temp Sensor correction: ");
        terminal.print(TempCorrection);
        terminal.println(". CHANGE?");
        break;

      //Change location manually
      case 4:
        if (Home){
          Home = false;
          terminal.println("Location: away. CHANGE");
        }
        else {
          Home = true;
          terminal.println("Location: home. CHANGE?");
        }
        break;

      //Clear stored SSID and password
      case 5:
        terminal.println("Erasing SSID and restarting unit.");
        terminal.flush();
        WiFi.begin("FakeSSID","FakePW"); //replace current WiFi credentials with fake ones
        delay(1000);
        ESP.restart();
        break;

      //Clear current temperature settings
      case 6:
        terminal.println("All settings reset to default.");
        Winter = true;
        Hysteresis_W = 2;
        Hysteresis_S = 2;
        break;

      //Exit Settings menu
      case 7:
        terminal.println("PRESS SETTINGS BUTTON TO ACCESS MENU");
        break;
        
    }
  }
  terminal.println("");
  terminal.println("");
  terminal.flush();
}

// Load available presets from EEPROM 
void GetPresets(){
  TempCorrection = EEPROM.read(0);
  if ((TempCorrection < 0) || (TempCorrection > 10)){
    TempCorrection = 0;
    Serial.println("No saved temperature correction in EEPROM.");
  }
  else{
    TempCorrection -= 5; // 5 was added at EEPROM save to account for negative values
    Serial.print("Temperature correction from EEPROM: ");
    Serial.println(TempCorrection);   
  }

  Hysteresis_W = EEPROM.read(1);
  if ((Hysteresis_W < 2) || (Hysteresis_W > 6)){
    Hysteresis_W = 2;
    Serial.println("No saved Winter hysteresis in EEPROM.");
  }
  else{
    Serial.print("Winter hysteresis from EEPROM: ");
    Serial.println(Hysteresis_W);   
  }

  Hysteresis_S = EEPROM.read(2);
  if ((Hysteresis_S < 2) || (Hysteresis_S > 6)){
    Hysteresis_S = 2;
    Serial.println("No saved Summer hysteresis in EEPROM.");
  }
  else{
    Serial.print("Winter hysteresis from EEPROM: ");
    Serial.println(Hysteresis_S);   
  }
  TempDes = EEPROM.read(3);
  if ((TempDes < 50) || (TempDes > 80)){
    TempDes = 70;
    Serial.println("No deisred temperature in EEPROM. Default temp setting: 70");
  }
  else {
    Serial.print("Desired temperature from EEPROM: ");
    Serial.println(TempDes);
  }
  PreviousTempDes = TempDes;
  Winter = EEPROM.read(4);
  if (Winter){
    Serial.println("Season setting from EEPROM: Winter / heating");
  }
  else Serial.println("Season setting from EEPROM: Summer / cooling");
}


void loop() {

  Blynk.run();
  timer.run();
  yield();
}
17 Likes

Terminal use is amazing!
I was waiting who will be the first to use it for user input.

Love the terminal. I just wish that it used the same font as the other widgets.

We should consider that, sure.

Hi. Nice terminal menu.
I would like to test your code to modify for my needs, but I didn’t figured out how to connect my esp with blynk.
Can you help me?

this is my serial printout

Loading presets from EEPROM
No saved temperature correction in EEPROM.
No saved Winter hysteresis in EEPROM.
No saved Summer hysteresis in EEPROM.
No deisred temperature in EEPROM. Default temp setting: 70
Season setting from EEPROM: Winter / heating
Waiting for Wifi to connect
00000000000000000000
Connect timed out, opening AP
scan done
5 networks found

softAP


WiFi connected
192.168.4.1
Server started

On this ip the page is not available.
Thanks

@ferox Connect your computer, phone or tablet to the WiFi access point ‘Blynk’. Then open a browser, and type '192.168.4.1’ in the address bar (without the quotes), and hit Enter. A page should load with spaces to input your home WiFi SSID and password. Enter them, and click the ‘Submit’ button. That’s it.

If I were to write this today, I’d skip the WiFi code, and use WiFiManager library (shout out to Alex @tzapulica ).

2 Likes

I’ve completely revamped my HVAC thermostat project. New features include:

1) Expanded dashboard. I’ve divided it into two tabs – one for the basic thermostat, and one for settings and detailed feedback. The tab bar is placed one row up from the bottom of the screen, so that the dashboard is functional on both iPhones and iPad.

2) Simplified WiFi connection with Alex’s (@tzapulica) awesome library.

3) Activated location awareness, so “home” and “away” modes are automated. Uses IFTTT and the Blynk cloud for geo-fencing.

4) Press-and-hold 2 seconds to enter or exit the settings menu, as well as a settings menu time out. This should eliminate accidental triggering of the menu.

5) A Manual Run mode. I think this will come in handy when I’m traveling, and the system has been in “away” mode for several days. I’ll be able to force the system to start heating or cooling long before I enter the “home” zone.

The hardware remains unchanged, and is dead simple: an ESP-01, a relay, a 3.3v power supply, and a DHT-11 sensor. Here’s the code. Enjoy, comment, critique:

  /**************************************************************************** 
 *  HVAC control for a "2-pipe" radiator system.
 *  
 *  Compares readings from a DHT11 temperature sensor with desired temperature
 *  from the Blynk application, and runs the fan unit as necessary to heat or 
 *  cool.  Hysteresis levels for both Summer and Winter are independently 
 *  adjustable from 2 to 6 degrees. The temperature sensor readings can be 
 *  offset up or down by up to 5 degrees. All settings are saved to EEPROM, and 
 *  automatically reloaded on startup.
 *  
 *  "Home" setting is triggered by IFTTT iOS location channel, and results in an
 *  action on the Maker channel. The Maker channel parameters are as follows:
 *       URL: http://blynk-cloud.com:8080/YOUR_TOKEN/pin/V31
 *       Method: PUT
 *       Content Type: application/json
 *       Body: ["1"]    
 *  "Away" mode requires an identical IFTTT recipe, but with
 *       Body: ["0"]
 *  
 *  Added a press-and-hold requirement to enter the settings menu, as well as
 *  a Menu timeout and reset after a period of inactivity.
 *  
 *  Added a manual override to turn on the system, independent of other factors
 *  
 *  WiFi connection is now simplified with Tapzu's WiFiManager. Wifi automatically
 *  reconnects to last working credentials. If the last SSID is unavailable, it
 *  creates an access point ("BlynkAutoConnect"). Connect any wifi device to the
 *  access point, and a captive portal pop up to receive new wifi credentials.
 *  
 *  The hardware is minimal: an ESP-01, a single relay on GPIO 0, and a DHT11
 *  temperature sensor on GPIO 2.
 *  
*****************************************************************************
*/
#include <ESP8266WiFi.h>  //https://github.com/esp8266/Arduino
#include <BlynkSimpleEsp8266.h>
#include <ESP8266WebServer.h>
#include <DNSServer.h>
#include <WiFiManager.h>  //https://github.com/tzapu/WiFiManager
#include <SimpleTimer.h>
#include "DHT.h"
#include <EEPROM.h>

#define UpdateFrequency 6000 //How often a new temperature will be read
#define MenuTimeOut 15000
#define RelayPin 2

DHT dht(0,DHT11); //Initialize the sensor. Use pin 0. Sensor type is DHT11.

// Timer for temperature updates
SimpleTimer timer;

//WiFi and Blynk connection variables
char auth[] = "BLYNK_AUTH"; // Blynk token "YourAuthToken"

//Thermostat variables
int TempDes = 70; //Desired temperature setting
int PreviousTempDes = 70;
int TempAct = 70; //Actual temperature, as measured by the DHT11 sensor
int TempCorrection = 0; //Used to adjust readings, if the sensor needs calibration
int BadRead = 0; //Counts consecutive failed readings of the DHT11 sensor
float LastRead;

int Hysteresis_W = 2; //Summer and Winter hysteresis levels
int Hysteresis_S = 2;

boolean Winter = true; 
boolean Home = true;
boolean FirstRead = true; //flag to cycle DHT until a good first read is made
boolean ManualRun = false; // used for manual override of thermostat algorithm

int MenuItem = 0; //Settings menu selection variable
long buttonRelease; //time button was released
long buttonPress; // time button was last pressed
boolean ButtonDown = false; //Settings button state (pressed = true)

// Attach virtual serial terminal to Virtual Pin V1
WidgetTerminal terminal(V2);


void setup() {
  //Creates an AP if no wifi credentials are stored
  WiFiManager wifi;
  wifi.autoConnect("ThermoX"); 
  Blynk.config(auth);
  
  dht.begin(); //Start temperature sensor
  delay(1500);

  //Initialize the fan relay. Mine is "off" when the relay is set LOW.
  pinMode(RelayPin,OUTPUT); 
  digitalWrite(RelayPin,LOW);
 
  Serial.begin(115200);
  delay(10);
  Serial.println(F("Startup"));
  Serial.println(F(""));
  
  //Load any saved settings from the EEPROM
  EEPROM.begin(20);  
  Serial.println(F("LOADING SETTINGS FROM MEMORY"));
  Serial.println(F(""));

  GetPresets();

  PreviousTempDes = TempDes;
  
  MenuReset();

  timer.setInterval(UpdateFrequency, TempUpdate); // Update temp reading and relay state
}


// Main loop
void loop() {
  Blynk.run();
  timer.run();
  
  if (ButtonDown){
    if (millis() - buttonPress > 1000){ // Was it a long press?
      if (MenuItem == 0){
        NextMenuItem(); // Enter the SETTINGS menu
      }
      else MenuReset(); // Exit the SETTINGS menu

      ButtonDown = false; // Prevent repeat triggering
    }
  }
}


// This is the decision algorithm for turning the HVAC on and off
void TempUpdate (){
  OtherUpdates(); //Refeshes dashboard information

  delay(1000);
  float ReadF = dht.readTemperature(true); //Get a new reading from the temp sensor
    
  if (isnan(ReadF)) {
    Serial.println(F("Failed to read from DHT sensor!"));
    BadRead++;
    return;
  }
  
  if (FirstRead == true){
    TempAct = (int)(ReadF + TempCorrection);
    FirstRead = false;
    Serial.print(F("First temperature reading: "));
    Serial.println(TempAct);
    LastRead = ReadF;
    return;   
  }

  //To compensate for some instability in the DHT11, the corrected temperature is
  //averaged with previous read, and any change is limited to 1 degree at a time. 
  else   { 
    int TempAvg = (int)((ReadF + LastRead + (2 * TempCorrection))/2);
    if (TempAvg >= TempAct + 1){
      TempAct = TempAct + 1;
    }
    if (TempAvg <= TempAct - 1){
      TempAct = TempAct -1;
    }

    LastRead = ReadF;
    BadRead = 0;
  }
  
  Blynk.virtualWrite(0,TempAct); //Report the corrected temperature in app
  Serial.print(F("Actual temperature: "));
  Serial.println(TempAct);

  if (!ManualRun){
    if (Home){
      if (Winter){
        //If I'm home, it's Winter, and the temp is too low, turn the relay ON
        if (TempAct < TempDes){
          Fan(1);
        }
        else if (TempAct >= (TempDes + Hysteresis_W)) {
          Fan(0);
        }
      }
      else if (!Winter){
        //If I'm home, it's Summer, and the temp is too high, turn the relay ON
        if (TempAct > TempDes){
          Fan(1);
        }
        else if (TempAct <= (TempDes - Hysteresis_S)){
          Fan(0);
        }
      else{
        Fan(0);
      }
     }
    }
    //If I'm not home, turn the relay OFF
    else {
      Fan(0);
    }
  }
}


//Match temp gauge to slider in Blynk app 
BLYNK_WRITE(3){
  TempDes = param.asInt();
  Blynk.virtualWrite(1,TempDes);
}

//Get location (home or away) from the IFTTT iOS location and Maker channels
BLYNK_WRITE(V31)
{   
  if (param.asInt()){
    Home = true;
    Blynk.virtualWrite(V29,1023);
  }
  else{
    Home = false;
    Blynk.virtualWrite(V29,0);
  }
}

   
// Dashboard SETTINGS button. Press-and-hold to enter menu. Short press for next item.
BLYNK_WRITE(V4) {
  // Check for a button press
   if (param.asInt()){ 
     buttonPress = millis();
     ButtonDown = true;
   }
    // check for button release
    else {
      buttonRelease = millis();
      ButtonDown = false;
      if (buttonRelease - buttonPress < 1000){  // It was a short press.
        if (MenuItem == 0){
        MenuReset(); // Remind user to hold 2 seconds to enter menu
        }
      else NextMenuItem(); // Advance to next menu item
      }
   }
}


//Cycles through the Settings Menu in the Terminal widget
void NextMenuItem(){
    MenuItem += 1;
    if (MenuItem > 7){
      MenuItem = 1;
    }
    
  switch(MenuItem){
      case 1:
        if (ManualRun){
          terminal.println("END MANUAL RUN?");
        }
        else{
          terminal.println("RUN MANUALLY?");
        }
        break;
        
     case 2:
      if (Home){
        terminal.println("LOCATION : HOME");
      }
      else terminal.println("LOCATION : AWAY");
      break;


    case 3:
      if (Winter){
        terminal.println("MODE : WINTER");
      }
      else terminal.println("MODE : SUMMER");
      break;

    case 4:
      if (Winter){
        terminal.print("WINTER HYSTERESIS: ");
        terminal.print(Hysteresis_W);
        terminal.println(" DEGREES");   
      }
      else{
        terminal.print("SUMMER HYSTERESIS: ");
        terminal.print(Hysteresis_S);
        terminal.println(" DEGREES");    
      }
      break;

    case 5:
      terminal.print("TEMPERATURE CORRECTION: ");
      terminal.print(TempCorrection);
      terminal.println(" DEGREES");
      break;

    case 6:
      terminal.println("CLEAR WIFI SETTINGS?");
      break;

    case 7:
       terminal.println("RESET ALL DEFAULTS?");
       break;
  }
  TerminalSend();
}


//Dashboard MODIFY button. Executes change of selected menu item 
BLYNK_WRITE(V5){

  buttonRelease = millis(); //Resets menu timeout for inactivity
  
  if ((MenuItem > 0) && (param.asInt())){
    switch(MenuItem){

        //Forced on
      case 1:
        if (ManualRun){
          ManualRun = false;
          Fan(0);
          terminal.println("MANUAL RUNNING: OFF");
        }
        else{
          ManualRun = true;
          Fan(1);
          terminal.println("MANUAL RUNNING: ON");
        }   
        break;

         //Change location manually
      case 2:
        if (Home){
          Home = false;
          terminal.println("LOCATION : AWAY");
        }
        else {
          Home = true;
          terminal.println("LOCATION : HOME");
        }
        break;
        
      //Change season
      case 3:
        if (Winter){
          terminal.println("MODE : SUMMER");
          Winter = false;
          EEPROM.write(4,0);
          EEPROM.commit();
        }
        else {
          terminal.println("MODE : WINTER");
          Winter = true;
          EEPROM.write(4,1);
          EEPROM.commit();
        } 
        break;
        
      //Change hysteresis level of currently selected season
      case 4:
        if (Winter){
          Hysteresis_W += 1;
          if (Hysteresis_W > 6){
            Hysteresis_W = 1;
          }
          EEPROM.write(1,(Hysteresis_W));
          EEPROM.commit();
          terminal.print("WINTER HYSTERESIS: ");
          terminal.print(Hysteresis_W);
          terminal.println(" DEGREES");
        }
        else{
          Hysteresis_S += 1;
          if (Hysteresis_S > 6){
            Hysteresis_S = 1;
          }
          EEPROM.write(2,(Hysteresis_S));
          EEPROM.commit();
          terminal.print("SUMMER HYSTERESIS: ");
          terminal.print(Hysteresis_S);
          terminal.println(" DEGREES");
          }
        break;

      case 5:
        TempCorrection +=1;
        if (TempCorrection > 5){
          TempCorrection = -5;
        }
        EEPROM.write(0,(TempCorrection + 5));
        EEPROM.commit();
        terminal.print("TEMPERATURE CORRECTION: ");
        terminal.print(TempCorrection);
        terminal.println(" DEGREES");
        break;

      //Clear stored SSID and password
      case 6:
        terminal.println("Erasing WiFi credentials and restarting!");
        terminal.flush();
        WiFi.begin("FakeSSID","FakePW"); //replace current WiFi credentials with fake ones
        delay(1000);
        ESP.restart();
        break;

      //Clear current temperature settings
      case 7:
        terminal.println("All settings reset to default!");
        Winter = true;
        Hysteresis_W = 2;
        Hysteresis_S = 2;
        break;
    }
    TerminalSend();
  }
}


// Turn the HVAC on or off
void Fan(boolean FanState){
  if (FanState){
    digitalWrite(RelayPin,HIGH);
    Blynk.virtualWrite(V7,1023);
    Serial.println(F("Fan is on."));
  } 
  else{
    digitalWrite(RelayPin,LOW);
    Blynk.virtualWrite(V7,0); 
    Serial.println(F("Fan is off"));
  }
}


// Reset the Menu at startup or after timing out from inactivity
void MenuReset(){
  MenuItem = 0;
  terminal.println("HOLD 2 SECONDS TO ENTER / EXIT SETTING MENU");
  TerminalSend();
}


// Updates dashboard information on the Blynk app
void OtherUpdates(){
  Blynk.virtualWrite(1,TempDes); //Update desired temp on the dashboard
  Blynk.virtualWrite(29,Home * 1023); // Update "home" LED on dashboard
  
  //Reset the Settings Menu if there's been no activity for a while
   if (MenuItem > 0 && (millis() - buttonRelease > MenuTimeOut)){
     MenuReset();
   }
   
   // Notify via the Terminal widget when the temperature sensor fails 
   // repeatedly, and turn off the fan, unless manual override is active
   if (BadRead > 10){
     terminal.println("<<< Temperature sensor malfunction. >>>");
     TerminalSend();
     BadRead = 0;
     if (!ManualRun){
      Fan(0);
     }
   }
   
   if (TempDes != PreviousTempDes){ //update the EEPROM if desired temperature had changed.
    EEPROM.write(3,TempDes);
    EEPROM.commit();
    Serial.print(F("New desired temperature saved: "));
    Serial.println(TempDes);
    PreviousTempDes = TempDes;  
   }
}


//Sends messages to the Terminal widget, and moves them to the top of the window
void TerminalSend(){
  for (int i=0; i<8; i++){
    terminal.println("");
  }
  terminal.flush();
}


//Retrieves saved values from EEPROM
void GetPresets(){
  TempCorrection = EEPROM.read(0);
  if ((TempCorrection < 0) || (TempCorrection > 10)){
    TempCorrection = 0;
    Serial.println(F("No saved temperature correction."));
  }
  else{
    TempCorrection -= 5; // 5 was added at EEPROM save to account for negative values
    Serial.print(F("Temperature correction: "));
    Serial.print(TempCorrection);
    Serial.println(F(" degrees."));      
  }

  Winter = EEPROM.read(4);
  Hysteresis_W = EEPROM.read(1);
  Hysteresis_S = EEPROM.read(2);

  if ((Hysteresis_W < 2) || (Hysteresis_W > 6)){
      Hysteresis_W = 2;
  }
  if ((Hysteresis_S < 2) || (Hysteresis_S > 6)){
      Hysteresis_S = 2;
  }
  
  if (Winter){
    Serial.println(F("Season setting: Winter / heating"));
    Serial.print(F("Winter hysteresis: "));
    Serial.print(Hysteresis_W);
    Serial.println(F(" degrees."));   
  }
  else {
    Serial.println(F("Season setting: Summer / cooling"));
    Serial.print(F("Summer hysteresis: "));
    Serial.print(Hysteresis_S);
    Serial.println(F(" degrees."));      
  }
 
  TempDes = EEPROM.read(3);
  if ((TempDes < 50) || (TempDes > 80)){
    TempDes = 70;
    Serial.println(F("No saved temperature setting."));
  }
  else {
    Serial.print(F("Desired temperature: "));
    Serial.print(TempDes);
    Serial.println(F(" degrees."));   
  }
  Serial.println("");
}
8 Likes

very very very nice.
extra points for the innovative Press-and-hold feature!
thanks for posting it,
Gustavo.

1 Like

Thanks, @gusgonnet. The press-and-hold works fine, but you actually have to release the button before the code will test how long the button was held. I would have preferred that the function calls be made while the user still has the button depressed. I tried it like this, but it crashed the ESP:

// Dashboard SETTINGS button. Press-and-hold to enter menu. Short press for next item.
BLYNK_WRITE(V4) {
  // Check for a button press
   if (param.asInt()){ 
     buttonPress = millis();

     // wait for the 2 second time out while the button is being pressed
     while((param.asInt()) && millis() - buttonPress < 2000){
     }

     // check if 2 seconds was exceeded
     if (millis() - buttonPress >= 2000{
          // if already in the menu, exit it
          if(MenuItem > 0){
              MenuReset();
          }
        // if not in the menu, enter it
        else{
             NextMenuItem();
        }
     }
     // Otherwise, the button wasn't held long enough
     else{
         MenuReset();
     }
   }
 }

I assume it was a WDT timeout. If anyone has another idea about how to achieve the desired button timeout, I’d love to hear it.

1 Like

Hi @chrome1000,
What about the OneButton library and its implementation for this purpose?
I’m not a coder (unfortunately) but maybe it can work with Blynk…

Should work with Blynk as a charm.

Hey, one thing I can tell you is that I prefer a bit more the first blynk app, where you did not use an extra tab for the config. I don’t know, it’s just that the screen feels empty with only the temperature widgets and the slider.

This is what I did in my HVAC controller with a Particle:


(the tabs on the bottom I used for other projects around the house)

My project is described in hackster, here:

One thing I added in my thermostat, that you may or may not be interested in, is a heat (or cool) pulse. A pulse is a state where your HVAC starts for a fixed period of time (example: 10 minutes) and cools or heats (according to the mode set) regardless of the target/current temp settings. We use this at home for instance in a cold morning, to give the temperature of the house a boost while we get up from bed.

I post my pic and project here not to hijack your thread but to talk about improvements or just give feedback for each of our projects, if you wish.

Hey, I really like the geofencing -> auto-away feature you added! I think it’s a MUST for any smart thermostat.
Gustavo.

3 Likes

@psoro Cool. I’ll look into OneButton.

Thanks @gusgonnet. I had something similar to your “pulse” mode in one version of the code, but it didn’t make the final cut. :slight_smile: And you’re not hijacking the thread. It’s always interesting to see how others approach a similar problem.

As for the layout of the dashboard, my goal was an intuitive, minimal interface. Without the ability to resize widgets, it’s difficult to create any sort of visual hierarchy. Having just one control makes the user interaction very clear. Your dashboard, by contrast, is loaded with information. But without reading the labels, I don’t get an immediate sense of which of those 9 displays and 4 controls is most important. It gets more complicated by the fact that some widget locations are dictated more by space than a desired user experience. Please don’t take that as any sort of insult. It’s a limitation of the tool we’re using.

I’m a designer for an appliance company, and we struggle all the time between an “engineer’s interface” (all the displays and controls, available all the time), and the “designer’s interface” (just the pertinent information for the moment). Each has strengths and limitations. BTW, the use of tabs to arrange all of your home automation into a single dashboard is a nice touch.

1 Like

Hey,

oh yeah? and what happened to it? wasn’t used?

thanks for the tip, I’ll keep this in mind for future improvements.[quote=“chrome1000, post:13, topic:2586”]
we struggle all the time between an “engineer’s interface” (all the displays and controls, available all the time), and the “designer’s interface” (just the pertinent information for the moment)
[/quote]
You made me realize that I made this interface more for myself (troubleshooting the project) than for the users (the rest of my family). Totally agree with your comments.
thank you!
Gustavo.

That’s why tabs are so good, “simple” tab and “expert” tab(s)

And with your mode is that for hot/cold? Now we have button labels, you can have a switch button that is either heat (on) or cool (off)

I have cool/warm as my mode:

3 Likes

I’m not going to hijack this discussion, but since I can’t get enough of looking at other people’s HVAC-Blynk creations, here’s mine!

Just monitoring for now:

6 Likes

I live in a highrise, and the HVAC is a “2-pipe” system. Hot or chilled water are circulated through the system, but it’s controlled by the building maintenance department. Since that’s changed only twice a year, I left the heating / cooling settings (which I chose to call Summer and Winter) in the console settings menu.

Exactly. I just wasn’t using it.

Rather than post the whole sketch again, I just edited the previous update. Four fixes:

  1. The press-and-hold function now works as intended, without having to release the button. I wasn’t able to use the OneButton library, but the new code does the same thing.
  2. An improperly declared variable was causing frequent failures of the DHT sensor. Now running flawlessly.
  3. Re-ordered the Settings menu to put most frequently used settings first.
  4. Reduced the number of carriage returns after sending text to the console widget for platform compatibility. The console widget on older iPhone and iPads has fewer lines that that of newer iPhones.
1 Like