ESP8266 HVAC control

@structure7 Which widget are you using for “current status?” That would make a way nicer presentation of my settings menu than using the console. I’m guessing it’s something that hasn’t been released for iOS yet.

That would be the Labeled Value L (large). I checked an iOS device and it should be there.

Yes, iOS has the labeled value widget, but how on Earth did you get it to spit out “HVAC OFF since 12:53PM on 6/8”? I assumed that text labels were static, and that the widget could only display a numeric value. Are you sending that entire string as the value?

Ah! Yes it’s a string with the misc variables woven in:

if (digitalRead(blowerPin) == HIGH) // Runs when blower is OFF.
   {Blynk.virtualWrite(16, String("HVAC OFF since ") + offHour + ":" + offMinute + "AM on " + offMonth + "/" + offDay);}

I have about a million ifs supporting the different cases, which I need to turn into, well, switch/case or something!

If you’re a glutton for punishment, here’s the entire code.

6 Likes

You rock. I’ve gotta steal this idea for my settings menu. Thanks for sharing.

me too!!! been wanting to work out how to do it for a while now!

i have loads of serial.print’s that are crying out for this sort of attention!

but it is not ‘stealing’ if it is given with love :wink: TY @structure7

Why thanks! I fooled around with a couple different widgets and found that one to be the most elegant.

very cool!
thanks

great idea, I will explore it in my own projects :slight_smile:

@structure7 Thanks. Using your method cleans up my settings page nicely, and was actually simpler to code than the console. The new settings tab, and reworked code:

  /**************************************************************************** 
 *  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;
SimpleTimer quickTimer;

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

//Thermostat variables
int TempDes = 70; //Desired temperature setting
int PreviousTempDes;
int TempAct = 70; //Actual temperature, as measured by the DHT11 sensor
int BadRead = 0; //Counts consecutive failed readings of the DHT11 sensor
int LastRead = 70; 

// Preference variables
int Hysteresis_W = 2; //Summer and Winter hysteresis levels
int Hysteresis_S = 2;
int TempCorrection = 0; //Used to adjust readings, if the sensor needs calibration

// Current condition variables
boolean Winter = true; 
boolean Home = true;
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)
boolean FanState = 0; // is the fan on or off?

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,HIGH);
 
  Serial.begin(115200);
  delay(10);
  
  //Load any saved settings from the EEPROM
  EEPROM.begin(20);  
  Serial.println(F("STARTUP : LOADING SETTINGS FROM MEMORY"));
  Serial.println(F(""));
  GetPresets();

  PreviousTempDes = TempDes; 
  
  MenuReset();

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


// Main loop
void loop() {
  Blynk.run();
  timer.run();
  quickTimer.run();
}

// Checks for long press condition on SETTINGS button
void ButtonCheck(){
  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(500);
  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;
  }

  //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 = int(ReadF + .5);
    BadRead = 0;
  }
  
  Blynk.virtualWrite(V0,TempAct); //Report the corrected temperature in app
  Serial.print(F("Actual temperature: "));
  Serial.println(TempAct);

  // Decision algorithm for running HVAC
  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){
          FanState = 1;
          Fan();
        }
        //Turn it off when the space is heated to the desired temp + a few degrees
        else if (TempAct >= (TempDes + Hysteresis_W) && FanState) {
          FanState = 0;
          Fan();
        }
      }
      else if (!Winter){
        //If I'm home, it's Summer, and the temp is too high, turn the relay ON
        if (TempAct > TempDes){
          FanState = 1;
          Fan();
        }
        //Turn it off when the space is cooled to the desired temp - a few degrees
        else if (TempAct <= (TempDes - Hysteresis_S) && FanState){
          FanState = 0;
          Fan();
        }
     }
    }
    //If I'm not home, turn the relay OFF
    else {
      FanState = 0;
      Fan();
    }
  }
}


//Match temp gauge to slider in Blynk app 
BLYNK_WRITE(V3){
  TempDes = param.asInt();
  Blynk.virtualWrite(V1,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 Labeled Value widget
void NextMenuItem(){

  String Response = "";
  
  MenuItem += 1;
  if (MenuItem > 7){
    MenuItem = 1;
  }
    
  switch(MenuItem){
      case 1:
        if (ManualRun){
          Response += "END MANUAL RUN?";
        }
        else{
          Response += "RUN MANUALLY?";
        }
        break;
        
     case 2:
      if (Home){
        Response += "LOCATION : HOME";
      }
      else Response += "LOCATION : AWAY";
      break;


    case 3:
      if (Winter){
        Response += "MODE : WINTER";
      }
      else Response += "MODE : SUMMER";
      break;

    case 4:
      if (Winter){
        Response += "WINTER HYSTERESIS: ";
        Response +=  Hysteresis_W;
        Response += " DEGREES";   
      }
      else{
        Response += "SUMMER HYSTERESIS: ";
        Response += Hysteresis_S;
        Response += " DEGREES";
      }
      break;

    case 5:
      Response += "TEMP CORRECTION: ";
      Response += TempCorrection;
      Response += " DEGREES";
      break;

    case 6:
      Response += "CLEAR WIFI SETTINGS?";
      break;

    case 7:
       Response += "RESET ALL DEFAULTS?";
       break;
  }
  Blynk.virtualWrite(V10,Response);
}


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

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

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

         //Change location manually
      case 2:
        if (Home){
          Home = false;
          Response += "LOCATION : AWAY";
        }
        else {
          Home = true;
          Response += "LOCATION : HOME";
        }
        break;
        
      //Change season
      case 3:
        if (Winter){
          Response += "MODE : SUMMER";
          Winter = false;
          EEPROM.write(4,0);
          EEPROM.commit();
        }
        else {
          Response += "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();
          Response += "WINTER HYSTERESIS: ";
          Response += Hysteresis_W;
          Response += " DEGREES";
        }
        else{
          Hysteresis_S += 1;
          if (Hysteresis_S > 6){
            Hysteresis_S = 1;
          }
          EEPROM.write(2,(Hysteresis_S));
          EEPROM.commit();
          Response += "SUMMER HYSTERESIS: ";
          Response += Hysteresis_S;
          Response += " DEGREES";
          }
        break;

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

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

      //Clear current temperature settings
      case 7:
        Response += "All settings reset to default!";
        Winter = true;
        Hysteresis_W = 2;
        Hysteresis_S = 2;
        break;
    }
    Blynk.virtualWrite(V10, Response);
  }
}


// Turn the HVAC on or off
void Fan(){
    digitalWrite(RelayPin,!FanState);
    Blynk.virtualWrite(V7,FanState * 1023);// fan "ON" LED on dashboard
    Serial.print(F("Fan state: "));
    Serial.println(FanState);
}


// Reset the Menu at startup or after timing out from inactivity
void MenuReset(){
  MenuItem = 0;
  Blynk.virtualWrite(V10, String("*****************************"));
  Blynk.virtualWrite(V10, String("HOLD 2 SEC TO ENTER/EXIT MENU"));
}


// Updates dashboard information on the Blynk app
void OtherUpdates(){
  Blynk.virtualWrite(V1,TempDes); //Update desired temp on the dashboard
  Blynk.virtualWrite(V29,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 when the temperature sensor fails repeatedly, and turn off the fan.
   if (BadRead > 10){
     Blynk.virtualWrite(V10, String("<<< SENSOR MALFUNCTION >>>"));
     BadRead = 0;
     if (!ManualRun){ //Manual mode supersedes a malfunction condition
      FanState = 0;
      Fan();
     }
   }
   
   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;  
   }
}

//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("");
}
2 Likes

LCD and Terminal have their place, but that does clean it up!

@tzapulica FYI, WiFiManager v11 is causing an “isnan was not declared in this scope” error. Backdating the library to v10 in the Arduino IDE fixes the problem.

hi,

there should be an update or two after that version fixing that specific issue

cheers

Hey,
your project looks amazing, thanks for sharing it.
I really like the following three things about it:
1 - the current status - top notch
2 - the OUTSIDE temperature - nice touch
3 - the RUNTIME - pretty cool idea.

I most probably start using #1 and #2 soon in my project, and expand your #3 into cost $$, so to give an idea of how much money it actually meant to cool or heat the house so far.

great stuff, and thanks again for the host of this thread for allowing us to use it :slight_smile:
Gustavo.

That would be cool! Meter the kwh x cost. I’ve got one of those plug-in usage monitors but I haven’t yet tried my own with Arduino. I know there’s lot of guides out there.

Using a meter will give you the $$ for the whole house, yes.
what I was thinking in this case, is that since your arduino is keeping track of the minutes the HVAC was ON, then by configuring the cost of kwh and the consumption of your HVAC (via blynk probably) one could calculate the cost of running the HVAC in money.

Example:
cost of kw/h= $ 0.10
HVAC consumption=1kw

That would give:
Yesterdays runtime: 3 hours
Yesterdays cost: $0.30

that would put things in perspective, at least for me.

This leads me to toe following: why not build on top of this concept an accumulated cost in $$ from start of month so you don’t get bad surprises when the electricity bill comes around? :scream:
Thank you for the idea
Gustavo.

I was even thinking of just measuring with a CT right at the unit(s). Then onto other major appliances… see where that money is going!

1 Like

Hi Eric,
I was looking at your code and noticed you address the long press (with the ButtonDown variable) in the loop() function as well as in the BLYNK_WRITE(V4) function.

Do you recall the reason why you added code in the loop() function?
thanks!
Gustavo.

The BLYNK_WRITE(V4) function only runs when there is a change to the V4 button state. When the button is being held down, there’s no change to that state, so I needed a way for the code to catch the timeout for a long hold.

Originally, I had all the code contained in the BLYNK_WRITE(V4) function, but the users had to guess when they’d held the button long enough to be considered a long hold. I thought it felt clumsy, and didn’t work as one would normally expect a press-and-hold to work, so I added the timeout code in the loop().

You might have also noticed that the messaging tells the user to hold for 2 seconds, but the timeout is actually only 1 second. That’s intentional. Usability experts have found that users almost always overestimate time, so you have to make the actual timeout less than what they’re expecting, in order for it to ‘feel’ like the right amount of time.

ok, I get it now, thanks for the clarification.

cool! seems like you know quite few tricks :slight_smile:

Gustavo.

Well, I’m a designer for a major appliance manufacturer, so I may have an unfair advantage when it comes to usability “tricks”. :wink: