I’ve added a few new features to the ever evolving thermostat code. In addition to fixing some minor bugs, here’s what’s new::
-
Humity compensation: It’s Summer here in Chicago, and I’ve noticed that whenever there’s a lot of humidity in the air, I set the air conditioning temperature lower to compensate. So, it occurred to me that my thermostat could and should do this for me. I’ve now added a “use perceived temperature” option in the settings. EDIT: I originally used a simplified version of the formula that meteorologists use for calculating the heat index, or “feels like” temperature, but have since changed it to a simple linear offset of 1- 6 degrees when the humidity is above 40%… Humidity is reported on pin V2.
-
Widget color setting: In response to the recent availability of a light color themed Blynk dashboard, I’ve added widget color themes to the SETTINGS menu. Red and blue are still reserved for showing the active heating and cooling status, but all other Blynk colors can now be selected as themes right from the dashboard. One caveat: avoid setting the widget color to white when you have the Blynk LIGHT theme enabled. You won’t be able to see the white widgets on the white background. If you do this, you’ll have to switch back to dark mode to see your widgets again.
-
Various timers have been replaced with the much simpler Blynk.timer.
/****************************************************************************
* ThermoX v0.18.0 Thermostatic HVAC control
*
* 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. 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 color coding (red/blue) for heating and cooling modes to dashboard widgets.
* Added a setting for widget color change. The red / blue "on" states will still be used
* heating or cooling.
*
* Added humidity sensing and a "perceived temperature" setting that takes into account the
* different way people perceive temperature when humidity is high. It's only for Summer mode,
* and only when the temperature is above 70 degrees F.
*
* Changed Simple Timer calls to Blynk Timer. Settings buttons and menu timers are all
* monitored by it now.
*
* Changed Manual mode to a 15 minute pulse mode.
*
* Added IFTTT / Amazon Echo integration for operating with voice commands. Uses
* the IFTTT Maker Channel in the same way as above, but with an Amazon Alexa voice
* trigger. To manually run the fan, use the following Maker Channel parameters:
* URL: http://blynk-cloud.com:8080/YOUR_TOKEN/pin/V6
* Method: PUT
* Content Type: application/json
* Body: ["1"]
* Make identical recipes (now called applets) for Temperature Up and Temperature
* Down, substituting Body values of ["2"] and ["3"], repsectively.
*
* 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 manual overrides to force system run or halt, 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 "DHT.h"
#include <EEPROM.h>
#define UpdateFrequency 10000 //How often a new temperature will be read
#define MenuTimeOut 7000 //Menu timeout from inactivity
#define LongPress 650 //How long SETTINGS button needs to be pressed to enter menu
#define RelayPin 2
const String BLYNK_BLUE = "#04C0F8";
const String BLYNK_RED = "#D3435C";
const String BLYNK_WHITE = "#FFFFFF";
const String BLYNK_BLACK = "#000000";
const String BLYNK_GREEN = "#23C48E";
const String BLYNK_YELLOW = "#ED9D00";
const String BLYNK_DARK_BLUE = "#5F7CD8";
String NormalWidgetColor = BLYNK_WHITE;
String PreviousWidgetColor = BLYNK_WHITE;
DHT dht(0,DHT11); //Initialize the sensor. Use pin 0. Sensor type is DHT11.
// Timer for temperature updates
BlynkTimer timer;
//WiFi and Blynk connection variables
char auth[] = "Your_token_here"; // 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
float LastRead = 70; // Previous temperature reading
int Humidity = 50;
// 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
boolean UsePerceivedTemp = false; // Use humidity-adjusted perceived temperature, instead of actual temperature
// Current condition variables
boolean Winter = true;
boolean Home = true;
boolean ManualRun = false; // used to run fan, overriding thermostat algorithm
boolean ManualStop = false; // used to stop fan, overriding thermostat algorithm
int MenuItem = 0; // Settings menu selection variable
boolean ButtonPressed = false;// Settings button state
boolean LongHold = false; // Flag showoing a long hold detected on the SETTINGS button
int ButtonTimer; // Timer for detecting long press of Settings button
String Response = ""; // Text output to SETTINGS value widget
boolean FanState = 0; // Is the fan on or off?
int MenuTimer; // Timer for resetting SETTINGS menu after a timeout has elapsed
String myHostname = "ThermoX";
void setup() {
// Create an access point if no wifi credentials are stored
WiFi.hostname(myHostname);
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 HIGH.
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
timer.setInterval(30000L, OtherUpdates); // Refreshes non-urgent dashboard info
MenuTimer = timer.setInterval(15000L, MenuReset);// 15 second inactivity timeout on Settings menu
}
// Main loop
void loop() {
Blynk.run();
timer.run();
}
//*********************** Thermostat Functions **********************************
// This is the decision algorithm for turning the HVAC on and off
void TempUpdate (){
float ReadF = dht.readTemperature(true); //Get a new reading from the temp sensor
if (isnan(ReadF)) {
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.
int TempAvg = int((ReadF + LastRead + (2 * TempCorrection))/2);
// Use "perceived temperature" offset (when turned on in SETTINGS)
if(UsePerceivedTemp == true && Winter == false && ReadF > 70 && Humidity > 40){
TempAvg += int((Humidity - 40) / 10);
}
if (TempAvg > TempAct){
TempAct += 1;
}
else if (TempAvg < TempAct){
TempAct -= 1;
}
LastRead = ReadF;
BadRead = 0; // Reset counter for failed sensor reads
Blynk.virtualWrite(V0,TempAct); //Report the corrected temperature in app
// Decision algorithm for running HVAC
if (!ManualRun && !ManualStop){ // Make sure it's not in one of the manual modes
// If I'm home, run the algorithm
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);
}
//Turn it off when the space is heated to the desired temp + a few degrees
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);
}
//Turn it off when the space is cooled to the desired temp - a few degrees
else if (TempAct <= (TempDes - Hysteresis_S)){
Fan(0);
}
}
}
// If I'm not home, turn the relay OFF
else {
Fan(0);
}
}
}
// Turn the HVAC on or off
void Fan(boolean RunFan){
FanState = RunFan;
// Set the proper color for the Desired Temp gauge and ON/OFF LED
//(red = heating, blue = cooling, white gauge or LED off = within desired range)
if (Winter && FanState){
Blynk.setProperty(V0, "color", BLYNK_RED);
Blynk.setProperty(V7, "color", BLYNK_RED);
}
else if (!Winter && FanState){
Blynk.setProperty(V0, "color", BLYNK_BLUE);
Blynk.setProperty(V7, "color", BLYNK_BLUE);
}
else{
// Return widgets to their "off" state color, depending on theme
Blynk.setProperty(V0, "color", NormalWidgetColor);
}
digitalWrite(RelayPin,!FanState); // Relay turns fan on with LOW input, off with HIGH
Blynk.virtualWrite(V7,FanState * 1023);// fan "ON" LED on dashboard
}
// Resets from manual run to normal mode
void KillManual(){
ManualRun = false;
}
//Match temp gauge to slider in Blynk app
BLYNK_WRITE(V3){
TempDes = param.asInt();
Blynk.virtualWrite(V1,TempDes);
}
// Updates dashboard information on the Blynk app
void OtherUpdates(){
Blynk.virtualWrite(V29,Home * 1023); // Update "home" LED on dashboard
Blynk.virtualWrite(V1,TempDes); //Update desired temp on the dashboard
// Notify when the temperature sensor fails repeatedly, and turn off the fan.
if(MenuItem == 0 && !ButtonPressed){
if (BadRead > 10){
Blynk.virtualWrite(V10, String("<<< SENSOR MALFUNCTION >>>"));
BadRead = 0;
if (!ManualRun){ //Manual mode supersedes a malfunction condition
Fan(0);
}
}
// Clear notification when sensor reads correctly again
else{
MenuReset();
}
}
if (TempDes != PreviousTempDes){ //update the EEPROM if desired temperature had changed.
EEPROM.write(3,TempDes);
EEPROM.commit();
PreviousTempDes = TempDes;
}
// Change widget colors
if(NormalWidgetColor != PreviousWidgetColor){
SetNewWidgetColor();
}
// To stabilize perceived temperature calculation, only update humidity readings between fan cycles
if(!FanState){
float ReadH = dht.readHumidity(); // Read humidity (percent)
// Only update humidity if it's a good read from the sensor. To mitigate any
// instability, average with previous reading, change by only 1% per reading
if(!(isnan(ReadH))){
int HumidityAvg = (ReadH + Humidity) / 2;
if (HumidityAvg > Humidity){
Humidity += 1;
}
if (HumidityAvg < Humidity){
Humidity -=1;
}
}
Blynk.virtualWrite(V2, Humidity);
}
}
//************************ External Data Sources ************************************
// Receives commands from IFTTT Maker Channel via the Amazon Echo.
// Pin V6 is not actually associated with a Blynk dashboard widget.
BLYNK_WRITE(V6){
int AlexaCommand = param.asInt();
switch(AlexaCommand){
// Turn manual running on / off
case 1:
if (ManualRun){
ManualRun = false;
}
else{
ManualRun = true;
ManualStop = false;
Fan(1);
}
break;
// Increase desired temperature by 2 degrees
case 2:
TempDes += 2;
Blynk.virtualWrite(V3, TempDes); //Update the slider widget
break;
// Decrease desired temperature by 2 degrees
case 3:
TempDes -= 2;
Blynk.virtualWrite(V3, TempDes); //Update the slider widget
break;
}
}
//Get location (home or away) from the IFTTT iOS location and Maker channels
BLYNK_WRITE(V31)
{
Home = param.asInt();
if (Home){ //Turn the HOME LED widget on or off
Blynk.virtualWrite(V29,1023);
}
else Blynk.virtualWrite(V29,0);
}
//************************** Settings Menu Functions *******************************
// Dashboard SETTINGS button. Press-and-hold to enter menu. Short press for next item.
BLYNK_WRITE(V4) {
// When the SETTINGS button is pressed, start a timer to check for a long press
if(param.asInt()){
ButtonTimer = timer.setTimeout(750, LongHoldDetect);
ButtonPressed = true;
timer.restartTimer(MenuTimer);
}
// Button has been released
else {
ButtonPressed = false; // Reset the button press flag
// If the long hold function wasn't called, it's a short press. Avance or reset the menu.
if (!LongHold){
timer.deleteTimer(ButtonTimer); // Kill the long hold timer if it hasn't been activated.
if (MenuItem == 0){
MenuReset(); // Remind user to hold 2 seconds to enter menu
}
else{
NextMenuItem(); // Advance to next menu item
}
}
// Reset the long press flag
LongHold = false;
}
}
// Checks for long press condition on SETTINGS button
void LongHoldDetect(){
// If the button is still depressed, it's a long hold
if (ButtonPressed){
LongHold = true;
// Enter or exit the SETTINGS menu, if it was a long press
if (MenuItem == 0){
NextMenuItem(); // Enter the SETTINGS menu
}
else{
MenuReset(); // Exit the SETTINGS menu
}
}
}
//Cycles through the Settings Menu in the Labeled Value widget
void NextMenuItem(){
MenuItem += 1;
if (MenuItem > 10){
MenuItem = 1;
}
switch(MenuItem){
case 1:
if (ManualRun){
Response = "CANCEL PULSE?";
}
else{
Response = "15 MIN PULSE?";
}
break;
case 2:
if (UsePerceivedTemp){
Response = "USE ACTUAL TEMP?";
}
else Response = "USE PERCEIVED TEMP?";
break;
case 3:
if (ManualStop){
Response = "END SYSTEM HALT?";
}
else{
Response = "HALT SYSTEM?";
}
break;
case 4:
if (Home){
Response = "LOCATION: HOME";
}
else Response = "LOCATION: AWAY";
break;
case 5:
if (Winter){
Response = "MODE : WINTER";
}
else Response = "MODE : SUMMER";
break;
case 6:
if (Winter){
Response = "HYSTERESIS: ";
Response += Hysteresis_W;
Response += " DEG";
}
else{
Response = "HYSTERESIS: ";
Response += Hysteresis_S;
Response += " DEG";
}
break;
case 7:
Response = "TEMP CORRECTION: ";
Response += TempCorrection;
Response += " DEGREES";
break;
case 8:
Response = "CHANGE WIDGET COLOR?";
break;
case 9:
Response = "CLEAR WIFI SETTINGS?";
break;
case 10:
Response = "RESET ALL DEFAULTS?";
break;
}
Blynk.virtualWrite(V10,Response);
}
//Dashboard MODIFY button. Executes change of selected menu item
BLYNK_WRITE(V5){
if ((MenuItem > 0) && (param.asInt())){
timer.restartTimer(MenuTimer);
switch(MenuItem){
//Forced on
case 1:
if (ManualRun){
ManualRun = false;
Response = "15 MIN PULSE?";
}
else{
ManualRun = true;
ManualStop = false;
Fan(1);
Response = "PULSE: ON";
timer.setTimeout(900000L, KillManual);
}
break;
//Change season
case 2:
if (UsePerceivedTemp){
Response = "ACTUAL TEMP MODE";
UsePerceivedTemp = false;
EEPROM.write(5,0);
EEPROM.commit();
}
else {
Response = "PERCEIVED TEMP MODE";
UsePerceivedTemp = true;
EEPROM.write(5,1);
EEPROM.commit();
}
if(UsePerceivedTemp){
Blynk.setProperty(V0, "label", " Perceived Temperature");
}
else{
Blynk.setProperty(V0, "label", " Actual Temperature");
}
break;
//Forced halt
case 3:
if (ManualStop){
ManualStop = false;
Response = "HALT SYSTEM?";
}
else {
ManualStop = true;
ManualRun = false;
Fan(0);
Response = "SYSTEM HALTED";
}
break;
//Change location manually
case 4:
if (Home){
Home = false;
Response = "LOCATION : AWAY";
}
else {
Home = true;
Response = "LOCATION : HOME";
}
break;
//Change season
case 5:
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 6:
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 += " DEG";
}
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 += " DEG";
}
break;
// Correct faulty DHT11 readings
case 7:
TempCorrection +=1;
if (TempCorrection > 5){
TempCorrection = -10;
}
EEPROM.write(0,(TempCorrection + 10));
EEPROM.commit();
Response = "TEMPERATURE CORRECTION: ";
Response += TempCorrection;
Response += " DEG";
break;
//Change default widget "off" color to contrast with light or dark theme
case 8:
if(NormalWidgetColor == BLYNK_BLACK){
NormalWidgetColor = BLYNK_WHITE;
Response = "WHITE";
EEPROM.write(6, 1);
}
else if(NormalWidgetColor == BLYNK_WHITE){
NormalWidgetColor = BLYNK_GREEN;
Response = "GREEN";
EEPROM.write(6, 3);
}
else if(NormalWidgetColor == BLYNK_GREEN){
NormalWidgetColor = BLYNK_DARK_BLUE;
Response = "DARK BLUE";
EEPROM.write(6, 5);
}
else if(NormalWidgetColor == BLYNK_DARK_BLUE){
NormalWidgetColor = BLYNK_YELLOW;
Response = "YELLOW";
EEPROM.write(6, 4);
}
else if(NormalWidgetColor == BLYNK_YELLOW){
NormalWidgetColor = BLYNK_BLACK;
Response = "BLACK";
EEPROM.write(6, 2);
}
SetNewWidgetColor();
break;
//Clear stored SSID and password
case 9:
Response = "ERASING WIFI CREDENTIALS";
WiFi.begin("FakeSSID","FakePW"); //replace current WiFi credentials with fake ones
delay(1000);
ESP.restart();
break;
//Clear current temperature settings
case 10:
Response = "All settings reset to default!";
Winter = true;
Hysteresis_W = 2;
Hysteresis_S = 2;
break;
}
Blynk.virtualWrite(V10, Response);
}
}
// Reset the Menu at startup or after timing out from inactivity
void MenuReset(){
MenuItem = 0;
Blynk.virtualWrite(V10, String("HOLD 2 SEC TO ENTER/EXIT MENU"));
}
//**************************** Miscellaneous *********************************
void SetNewWidgetColor(){
if(!FanState){
Blynk.setProperty(V0, "color", NormalWidgetColor);
}
Blynk.run();
Blynk.setProperty(V1, "color", NormalWidgetColor);
Blynk.run();
Blynk.setProperty(V2, "color", NormalWidgetColor);
Blynk.run();
Blynk.setProperty(V3, "color", NormalWidgetColor);
Blynk.run();
Blynk.setProperty(V4, "color", NormalWidgetColor);
Blynk.run();
Blynk.setProperty(V5, "color", NormalWidgetColor);
Blynk.run();
Blynk.setProperty(V10, "color", NormalWidgetColor);
Blynk.run();
Blynk.setProperty(V29, "color", NormalWidgetColor);
Blynk.run();
PreviousWidgetColor = NormalWidgetColor;
}
//Retrieves saved values from EEPROM
void GetPresets(){
TempCorrection = EEPROM.read(0);
if ((TempCorrection < 0) || (TempCorrection > 15)){
TempCorrection = 0;
}
else{
TempCorrection -= 10; // 10 was added at EEPROM save to account for negative values
}
UsePerceivedTemp = EEPROM.read(5);
if(UsePerceivedTemp){
Blynk.setProperty(V0, "label", " Perceived Temperature");
}
else{
Blynk.setProperty(V0, "label", " Actual Temperature");
}
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;
}
TempDes = EEPROM.read(3);
if ((TempDes < 50) || (TempDes > 80)){
TempDes = 70;
}
int DashboardColor = EEPROM.read(6);
if(DashboardColor > 0 && DashboardColor <= 5){
switch(DashboardColor){
case 1:
NormalWidgetColor = BLYNK_WHITE;
break;
case 2:
NormalWidgetColor = BLYNK_BLACK;
break;
case 3:
NormalWidgetColor = BLYNK_GREEN;
break;
case 4:
NormalWidgetColor = BLYNK_YELLOW;
break;
case 5:
NormalWidgetColor = BLYNK_DARK_BLUE;
break;
}
SetNewWidgetColor();
}
}