filipmu
Published © GPL3+

WiFi Thermostat ESP32 and Arduino

Make a thermostat from available modules that you can control via a web browser over local WiFi.

IntermediateFull instructions provided8 hours31,256
WiFi Thermostat ESP32 and Arduino

Things used in this project

Hardware components

ESP32-DevKitC V4
×1
DHT22/AM2302 Digital Temperature And Humidity Sensor Module
×1
2 Channel DC 5V Relay Module with Optocoupler Low Level Trigger Expansion Board for Arduino
×1
Breadboard Jumper Wires Prototype Board Dupont Wire Male to Male, Male to Female, Female to Female
×1
USB Wall Charger
You might have one laying around already. You will also need a usb cable with micro connector, which everyone has laying around.
×1
solderless prototype breadboard
You might have this laying around.
×1

Software apps and online services

Arduino IDE
Arduino IDE
Firefox web browser
This is what I had. I am sure other browsers with built in dev tools could be used for troubleshooting.

Story

Read more

Custom parts and enclosures

Enclosure for the ESP32 thermostat

3D Print it.

Schematics

Schematic

Drawing showing how to connect the modules.

Code

sketch_esp32_thermostat.zip

Arduino
Complete Arduino code for the ESP32 thermostat as a zip file. Download and unzip to have everything.
No preview (download only).

sketch_esp32_thermostat.ino

Arduino
Main arduino code file.
/*
   Garage theromstat
   Sketch to read temperatures with an ESP32 board and a DHTxx sensor connected to pin 26, and to control a Relay connected to Pin 27
*/

#include <Time.h>
#include <TimeLib.h>
#include <WiFi.h>

#include <ESPmDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>

#include <DHTesp.h>
#include "esp_system.h"
#include "soc/rtc.h"
#include "rom/uart.h"


/****** Start configuration section ******/

const char* ssid     = "XXXXXXX"; //Replace with your network SSID
const char* password = "XXXXXXXX"; //Replace with your network password
const String nodeName = "Cave"; //Replace with the name for this Thermostat

/****** End configuration section ******/

DHTesp::DHT_MODEL_t DHT_TYPE = DHTesp::DHT22;





const String sketchVersion = "3";
const String nodeType = "TH&Relay";
unsigned long lastReceivedRelayCommandMilis;
const unsigned int TIMEOUT_REICEIVED_COMMANDS_MILIS = 1000 * 60 * 2; //Two minutes
String relayError = "";
String thError = "";
WiFiServer server(80);
WiFiClient client;
const int RELAY_PIN = 27;
const int ONBOARD_LED_PIN = 2;
const int DHT_PIN = 26;
DHTesp dht;
int Tset = 72.0; //Use a number higher than 40 to avoid being on edge of screen fix for #2
double Td = 0.5;

int hys=0;

int control =0;
time_t currentTime = 0,startTime=0;

      double humidity = dht.getHumidity();
      double celsiusTemp = dht.getTemperature();
      double fahrenheitTemp = -10000;
      int currentStatus = digitalRead(RELAY_PIN);

hw_timer_t *timer = NULL;

void IRAM_ATTR resetModule(){
    //ets_printf("reboot\n"); //comment out to avoid issues fix #1
    //esp_restart_noos();
    esp_restart();
}


void setup() {
  
  Serial.begin(115200);
  slowDownCpu();
  pinMode(ONBOARD_LED_PIN, OUTPUT);

  pinMode(RELAY_PIN, OUTPUT);
  
  digitalWrite(RELAY_PIN, HIGH); //shut off relay
  dht.setup(DHT_PIN, DHT_TYPE);

  setUpWifi();

  lastReceivedRelayCommandMilis = millis();


    // Port defaults to 3232
  // ArduinoOTA.setPort(3232);

  // Hostname defaults to esp3232-[MAC]
  // ArduinoOTA.setHostname("myesp32");

  // No authentication by default
  // ArduinoOTA.setPassword("admin");

  // Password can be set with it's md5 value as well
  // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
  // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");

  ArduinoOTA
    .onStart([]() {
      String type;
      if (ArduinoOTA.getCommand() == U_FLASH)
        type = "sketch";
      else // U_SPIFFS
        type = "filesystem";

      // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
      Serial.println("Start updating " + type);
    })
    .onEnd([]() {
      Serial.println("\nEnd");
    })
    .onProgress([](unsigned int progress, unsigned int total) {
      Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
    })
    .onError([](ota_error_t error) {
      Serial.printf("Error[%u]: ", error);
      if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
      else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
      else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
      else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
      else if (error == OTA_END_ERROR) Serial.println("End Failed");
    });

  ArduinoOTA.begin();
  

}

void slowDownCpu() {
  rtc_clk_cpu_freq_set(RTC_CPU_FREQ_160M);
  uart_tx_wait_idle(0);
  int clockspeed = rtc_clk_cpu_freq_get();

  char* clockSpeeds[5] = {"XTAL", "80Mhz", "160Mhz", "240Mhz", "2Mhz"};
  Serial.print("Setting CPU freq to: ");
  Serial.println(clockSpeeds[clockspeed]);
  Serial.println("");
}

void setUpWifi() {
  //WiFi.config(ip,dns,gateway,subnet);


  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);


  // attempt to connect to Wifi network:
  int counter = 0;
  while (WiFi.status() != WL_CONNECTED) {
    WiFi.disconnect();
    WiFi.begin(ssid, password);
    int counter = 0;
    while (WiFi.status() != WL_CONNECTED) {
      delay(1000);
      Serial.print(".");
      counter++;
      if (counter > 10) {
        break;
      }
    }
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  Serial.print("MAC: ");
  Serial.println(WiFi.macAddress());
  // print the received signal strength:
  long rssi = WiFi.RSSI();
  Serial.print("Signal strength (RSSI):");
  Serial.print(rssi);
  Serial.println(" dBm");
  

  server.begin();

  Serial.println(" Server started");

//Create a timer for watchdog timer  
timer = timerBegin(0, 80, true); //timer 0, div 80
    timerAttachInterrupt(timer, &resetModule, true);
    timerAlarmWrite(timer, 15000000, false); //set time in us to 15 seconds fixes #1
    timerAlarmEnable(timer); //enable interrupt

}



double celsius2Fahrenheit(double celsius)
{
  return celsius * 9 / 5 + 32;
}



void loop() {

timerWrite(timer, 0); //reset timer (feed watchdog)

ArduinoOTA.handle();

  if (WiFi.status() != WL_CONNECTED) {
    setUpWifi();
  } else {
    // listen for incoming clients
    client = server.available();
    if (client.connected()) {
      digitalWrite(ONBOARD_LED_PIN, HIGH);
      client.setTimeout(5);  //set timeout to 5 second - must be done after connecting fix #1
      String req = client.readStringUntil('\r');  //read first line
      Serial.println("Received request: " + req);
      
      String response = String();
      if (req.startsWith("GET /ajax_inputs")){
      
      // send a standard http response header for json
      
      json_HTTP_header(response);
      response += "{\n";
      response += "\"Type\":\"" + nodeType + "\",\n";
      response += "\"Name\":\"" + nodeName + "\",\n";
   
      String ff = String(fahrenheitTemp);
      ff.trim();
      response += "\"Temp\":\"" + ff + "\",\n";

      String hh = String(humidity);
      hh.trim();
      response += "\"RelH\":\"" + hh + "\",\n";

      String jj = String(Tset);
      jj.trim();
      
      response += "\"Tset\":\"" + jj + "\",\n";
      
      response += "\"Heater\": \"" + String(currentStatus) + "\",\n";
      response += "\"Control\": \"" + String(control) + "\",\n"; 
      response += "\"StartTime\": \"";
      //time_and_date_string(response, startTime);
      unix_time_string(response,startTime);
      response += "\",\n";
      response += "\"CurrentTime\": \"";
      //time_and_date_string(response, now());
      unix_time_string(response,now());
      
      response += "\",\n";
      
      //response += "\"Mac\":\"" + WiFi.macAddress() + "\",\n";
            
      long rssi = WiFi.RSSI();
      response += "\"Wifi_ssi\": \"" + String(rssi) + "\"\n";
      response += "}\n";
      Serial.println("GET: Resp sent:" + response);
      client.println(response);
          
      }else if (req.startsWith("OPTIONS")) {
       response += "HTTP/1.1 200 OK\n";
       response += "Access-Control-Allow-Origin: *\n";

        response += "Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE\n";
        response += "Access-Control-Allow-Headers: access-control-allow-origin,Content-Type\n";
        response += "Access-Control-Max-Age: 100\n";
        response += "\n"; //This carry return is very important. Without it the response will not be sent.
        Serial.println("OPTIONS: Resp sent:" + response);   
        client.println(response);    
        //String msg = client.readString();
        //Serial.println(msg);

      }else if (req.startsWith("POST")) {
        
      
      
      Serial.print("POST: Remaining message:");
      int ct=0;
      while (ct<4 && client.available()){  //check for \r\n\r\n designating a blank line
        char c=client.read();
        Serial.print(c);
        if(c=='\r' || c=='\n')
          ct=ct+1;
        else
          ct=0;
      }

      //Crudely parse the JSON.  {Tset: 70, Control: 1}
      //parsing ignores the non numerics and stops after a non numeric character
      Serial.println("parsing 1...");
      Tset=client.parseInt();
      Serial.println("parsing 2...");
      control=client.parseInt();
      Serial.println("parsing complete");


      
      
     
      
      response += "HTTP/1.1 200 OK\n";
      response += "Access-Control-Allow-Origin: *\n";
      response += "\n"; //This carry return is very important. Without it the response will not be sent.
      Serial.println("POST: Resp sent:" + response);
      client.println(response);
        
      
      } else //standard HTML Page
      {
        
      response += "HTTP/1.1 200 OK\n";
      response += "Content-Type: text/html\n";
      response += "Access-Control-Allow-Origin: *\n";
      response += "\n"; //This carry return is very important. Without it the response will not be sent.

     //Below is the web page HTML that is served by the ESP32 server.
     //the index.html is converted to a string via http://tomeko.net/online_tools/cpp_text_escape.php?lang=en
      response += "<!doctype html>\n<html lang = \"en\">\n  <head>\n\t\t<meta http-equiv=\"content-type\" content=\"text/html\" charset=\"utf-8\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    \n\n    \n\t\t<link rel=\"stylesheet\" href=\"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css\" integrity=\"sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T\" crossorigin=\"anonymous\">\n\n\t\t<link rel=\"stylesheet\" href=\"https://use.fontawesome.com/releases/v5.7.2/css/all.css\" integrity=\"sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr\" crossorigin=\"anonymous\">  \n    \n\t\t<script src=\"https://code.jquery.com/jquery-3.3.1.slim.min.js\" integrity=\"sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo\" crossorigin=\"anonymous\"></script>\n\t\t<script src=\"https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js\" integrity=\"sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1\" crossorigin=\"anonymous\"></script>\n\t\t<script src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js\" integrity=\"sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM\" crossorigin=\"anonymous\"></script>\n\t\t<script src=\"https://cdn.jsdelivr.net/npm/knockout@3.3.0/build/output/knockout-latest.debug.min.js\"></script>\n\n\t\t<style>\t\n\t\t\t/*Make slider thumb black in all browsers*/\n\t\t\tinput[type=range]::-webkit-slider-thumb {background: #000000;}\n\t\t\tinput[type=range]::-moz-range-thumb {background: #000000;}\n\t\t\tinput[type=range]::-ms-thumb {background: #000000;}\n\t\n\t\t</style>\n  </head>\n  \n  \n  <body>\n  \n\t<nav class=\"navbar navbar-l bg-white\">\t\t\t\n\t\t\t<span class=\"navbar-text\">\n\t\t\t\t<h4 class=\"text-black\"><bdi data-bind=\"text:Name\"></bdi> Thermostat</h4>\n\t\t\t</span>\n\t\t\n\t\t <div class=\"custom-control custom-switch\">\n\t\t\t<input type=\"checkbox\" data-bind=\"checked: ControlUI, click: save\" class=\"custom-control-input\" checkedValue=\"1\" id=\"switch1\" name=\"example\">\n\t\t\t<label class=\"custom-control-label\" for=\"switch1\">Control</label>\n\t\t</div>\n\t</nav>\n\t\t\n\t<div data-bind=\"visible: refresh()\"> </div>\t\n\t\n\t<div class=\"container-fluid\">\n\t\t<div class=\"row\">\n\t\t\t<div class=\"col m-2 bg-primary text-white\">\n\t\t\t\t<h5 class=\"text-left\" > Temperature </h5>\n\t\t\t\t<h4 class=\"text-right\"><bdi data-bind=\"text: Math.round(Temp()*10)/10\"></bdi></h4>\n\t\t\t</div>\n  \n\t\t\t<div class=\"col m-2 bg-primary text-white\">\n\t\t\t\t<h5 class=\"text-left\">Humidity</h5>\n\t\t\t\t<h4 class=\"text-right\"><bdi data-bind=\"text: Math.round(RelH())\"></bdi> % </h4>\n\t\t\t</div>\n\t\t</div>\n\t\n\t\t<div class=\"row\" data-bind=\"visible: ControlUI\">\n\t\t\t<div class=\"col  m-2 bg-primary text-white\">\n\t\t\t\t<h4> Set point </h4>\t\t\t\t\n\t\t\t\t<span style=\"display:inline-block\">40</span> \n\t\t\t\t<span style=\"float: right;\">100</span>\n\t\t\t\t\n\t\t\t\t<input type=\"range\" class=\"custom-range\" data-bind=\" value: Tset, valueUpdate: 'input'\" min=\"40\" max=\"100\" >\n\t\t\t\t\n\t\t\t\t \n\t\t\t\t<span style=\"float: right;\">\n\t\t\t\t\t<h4 class=\"text-right\"><bdi data-bind=\"text: Math.round(Tset()*10)/10\"></bdi>\n\t\t\t\t\t<i class=\"fas fa-burn\" data-bind=\"visible: Heater()&gt;0 ? 1 : 0 \" ></i> </h4>\n\t\t\t\t</span>\t\t\t\t\t\t\t\t\t\n\t\t\t</div>\n\t\t</div>\n\t\t\t\n\t\t<button type=\"button\" class=\"btn btn-primary btn-sm\" data-bind=\"click: refresh\">   <i class=\"fas fa-redo-alt\"></i> </button> \n\t\n\t</div>  \n\t  \n    <footer class=\"container-fluid\">\n\t\t<h6 class=\"text-right text-muted small\">Start Time: <bdi data-bind=\"text: StartDate().toUTCString()\"></bdi></h6>\n\t\t<h6 class=\"text-right text-muted small\">Current Time: <bdi data-bind=\"text: CurrentDate().toUTCString()\"></bdi></h6>\n\t\t<h6 class=\"text-right text-muted small\">WiFi dbm= <bdi data-bind=\"text: Wifi_ssi\"></bdi></h6>\n    </footer>\n\t\n    <script>\n     \n\n\t \n    function status(response) {\n\t\tif (response.status >= 200 && response.status < 300) {\n\t\treturn Promise.resolve(response)\n\t\t} else {\n\t\treturn Promise.reject(new Error(response.statusText))\n\t\t}\n\t};\n\n   \n    // This is a simple *viewmodel*\n\tfunction AppViewModel() {\n    //data\n\tvar self = this;\n  \tself.Name = ko.observable(\"\");\n    self.Temp = ko.observable(\"\");\n    self.RelH = ko.observable(\"\");\n    self.Tset = ko.observable(\"\");\n  \tself.Heater = ko.observable(\"\");   \n    self.CurrentTime = ko.observable(\"\");\n    self.StartTime = ko.observable(\"\");\n\tself.ControlUI = ko.observable(\"\");\n\tself.Wifi_ssi = ko.observable(\"\");\n\t\n\tself.TsetDelayed = ko.pureComputed(self.Tset)\n        .extend({ rateLimit: { method: \"notifyWhenChangesStop\", timeout: 400 } });\n\t\n\tself.RefreshDelayed =ko.pureComputed(self.TsetDelayed).extend({ rateLimit: { method: \"notifyWhenChangesStop\", timeout: 400 } });\n\t\t\n\tself.Control = ko.computed({\n\tread: function(){return this.ControlUI() ? 1 : 0},\n\twrite: function(value){\n\tvar r=(value>0);\n\tthis.ControlUI(r);\n\t\n\t},\n\towner: this\n\t});\n\t\n\t\n\t\n\t\t\n\tself.StartDate = ko.computed(function() {\n\t\tvar event = new Date(self.StartTime()*1000);   \n\t\treturn event;\n    });\n  \n\tself.CurrentDate = ko.computed(function() {\n\t\tvar event = new Date(self.CurrentTime()*1000);   \n\t\treturn event;\n    });\n \n  \n  \n\tself.refresh =function(){\n\t\tfetch(\"http://192.168.0.222/ajax_inputs\").then (status)\n\t\t.then(function(response) {\n\t\treturn response.json();\n\t\t}).then(function(myJson) {\n\t\tconsole.log('Get request succeeded with resp ',myJson);\n\t\tself.Name(myJson.Name); \n\t\tself.Temp(myJson.Temp);\n\t\tself.RelH(myJson.RelH);\n\t\tself.Tset(myJson.Tset);\n\t\tself.Heater(myJson.Heater);\n\t\tself.Control(myJson.Control);\n\t\tself.CurrentTime(myJson.CurrentTime);\n\t\tself.StartTime(myJson.StartTime);\n\t\tself.Wifi_ssi(myJson.Wifi_ssi);\n\t\t}).catch(function(error){\n\t\tconsole.log('Request Failed',error);\n\t\t})\n\t};\n\n\tself.save = function(){\n\t\tvar plainJs = ko.toJS(self); \n\t\tconsole.log('Testing, Testing',plainJs.Tset);  \n\t\tfetch('http://192.168.0.222', {\n\t\tmethod: 'POST',\n\t\theaders: {\n\t\t'Accept': 'application/json',\n\t\t'Access-Control-Allow-Origin': '*',\n\t\t'Content-Type': 'application/json'\n\t\t},\n\t\tcredentials: 'same-origin',\n\t\tbody: JSON.stringify({\"Tset\":plainJs.TsetDelayed, \"Control\":plainJs.Control})\n\t\t}).then (status).catch(function(error){\n\t\tconsole.log('Request Failed',error);  \n\t\t});\n\treturn true};  \n  \n    \n\t\n  \n\tself.TsetDelayed.subscribe(function(newValue) {self.save()});\n\tself.RefreshDelayed.subscribe(function(newValue) {self.refresh()});\n  \n};\n    \n// Activates knockout.js\nko.applyBindings(new AppViewModel());    \n   \n</script>\n</body>\n</html>\n";
      response += "\n";   

       Serial.println("HTML Page: Resp sent:" + response);
       client.println(response);
      }

      humidity = dht.getHumidity();
      celsiusTemp = dht.getTemperature();
      fahrenheitTemp = -10000;
     currentStatus = 1-digitalRead(RELAY_PIN);


      if (dht.getStatus() != 0) {
        thError = String(dht.getStatusString());
        Serial.println("DHT Error status: " + thError);
        humidity = -10000;
        celsiusTemp = -10000;
      } else {
        thError = "";
        fahrenheitTemp = celsius2Fahrenheit(celsiusTemp);
        Serial.println("DHT OK");
      }

     
      
      
      delay(100);
      client.stop();
      digitalWrite(ONBOARD_LED_PIN, LOW);
      String tad = String();
      time_and_date_string(tad, now());
      Serial.println("Time: " + tad);
      Serial.println("Client_Stop _________________________________________");
    }

    
 
      
  }



 if(startTime<24*60*60) //Check if start time is less than 24 hrs, meaning no internet time fix #1
      {
        setTime(webUnixTime());
        setSyncProvider(webUnixTime);
       // adjustTime(-6*SECS_PER_HOUR);  //set to central time
        startTime=now();
        String tad = String();
        time_and_date_string(tad, startTime);
        Serial.println("Server time sync: " + tad);
      }

      humidity = dht.getHumidity();
      celsiusTemp = dht.getTemperature();
      fahrenheitTemp = 10000;
     


      if (dht.getStatus() != 0) {
        thError = String(dht.getStatusString());
        Serial.println("DHT Error status: " + thError);
        humidity = -10000;
        celsiusTemp = -10000;
      } else {
        thError = "";
        fahrenheitTemp = celsius2Fahrenheit(celsiusTemp);
        //Serial.println("DHT OK");
      }
     
        if (control==1)
      {
        if( fahrenheitTemp<(float(Tset)-Td))  //add some hysteresis
          {
            digitalWrite(RELAY_PIN, LOW); 
            currentStatus=1;
            
          }
        if ( fahrenheitTemp>(float(Tset)+Td))
        {
            digitalWrite(RELAY_PIN, HIGH); 
            currentStatus=0;          
        }
      }else
        {
            digitalWrite(RELAY_PIN, HIGH);
        }
}

index.html

HTML
This is the HTML and embedded javascript code for the web page. This code is incorporated as a string in the arduino code so that the ESP32 will serve this web page. For debugging, it is possible to save this on a PC, open in a browser and it will communicate with the ESP32.
<!doctype html>
<html lang = "en">
  <head>
		<meta http-equiv="content-type" content="text/html" charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    

    
		<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

		<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">  
    
		<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
		<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
		<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
		<script src="https://cdn.jsdelivr.net/npm/knockout@3.3.0/build/output/knockout-latest.debug.min.js"></script>

		<style>	
			/*Make slider thumb black in all browsers*/
			input[type=range]::-webkit-slider-thumb {background: #000000;}
			input[type=range]::-moz-range-thumb {background: #000000;}
			input[type=range]::-ms-thumb {background: #000000;}
	
		</style>
  </head>
  
  
  <body>
  
	<nav class="navbar navbar-l bg-white">			
			<span class="navbar-text">
				<h4 class="text-black"><bdi data-bind="text:Name"></bdi> Thermostat</h4>
			</span>
		
		 <div class="custom-control custom-switch">
			<input type="checkbox" data-bind="checked: ControlUI, click: save" class="custom-control-input" checkedValue="1" id="switch1" name="example">
			<label class="custom-control-label" for="switch1">Control</label>
		</div>
	</nav>
		
	<div data-bind="visible: refresh()"> </div>	
	
	<div class="container-fluid">
		<div class="row">
			<div class="col m-2 bg-primary text-white">
				<h5 class="text-left" > Temperature </h5>
				<h4 class="text-right"><bdi data-bind="text: Math.round(Temp()*10)/10"></bdi></h4>
			</div>
  
			<div class="col m-2 bg-primary text-white">
				<h5 class="text-left">Humidity</h5>
				<h4 class="text-right"><bdi data-bind="text: Math.round(RelH())"></bdi> % </h4>
			</div>
		</div>
	
		<div class="row" data-bind="visible: ControlUI">
			<div class="col  m-2 bg-primary text-white">
				<h4> Set point </h4>				
				<span style="display:inline-block">40</span> 
				<span style="float: right;">100</span>
				
				<input type="range" class="custom-range" data-bind=" value: Tset, valueUpdate: 'input'" min="40" max="100" >
				
				 
				<span style="float: right;">
					<h4 class="text-right"><bdi data-bind="text: Math.round(Tset()*10)/10"></bdi>
					<i class="fas fa-burn" data-bind="visible: Heater()&gt;0 ? 1 : 0 " ></i> </h4>
				</span>									
			</div>
		</div>
			
		<button type="button" class="btn btn-primary btn-sm" data-bind="click: refresh">   <i class="fas fa-redo-alt"></i> </button> 
	
	</div>  
	  
    <footer class="container-fluid">
		<h6 class="text-right text-muted small">Start Time: <bdi data-bind="text: StartDate().toUTCString()"></bdi></h6>
		<h6 class="text-right text-muted small">Current Time: <bdi data-bind="text: CurrentDate().toUTCString()"></bdi></h6>
		<h6 class="text-right text-muted small">WiFi dbm= <bdi data-bind="text: Wifi_ssi"></bdi></h6>
    </footer>
	
    <script>
     

	 
    function status(response) {
		if (response.status >= 200 && response.status < 300) {
		return Promise.resolve(response)
		} else {
		return Promise.reject(new Error(response.statusText))
		}
	};

   
    // This is a simple *viewmodel*
	function AppViewModel() {
    //data
	var self = this;
  	self.Name = ko.observable("");
    self.Temp = ko.observable("");
    self.RelH = ko.observable("");
    self.Tset = ko.observable("");
  	self.Heater = ko.observable("");   
    self.CurrentTime = ko.observable("");
    self.StartTime = ko.observable("");
	self.ControlUI = ko.observable("");
	self.Wifi_ssi = ko.observable("");
	
	self.TsetDelayed = ko.pureComputed(self.Tset)
        .extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 400 } });
	
	self.RefreshDelayed =ko.pureComputed(self.TsetDelayed).extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 400 } });
		
	self.Control = ko.computed({
	read: function(){return this.ControlUI() ? 1 : 0},
	write: function(value){
	var r=(value>0);
	this.ControlUI(r);
	
	},
	owner: this
	});
	
	
	
		
	self.StartDate = ko.computed(function() {
		var event = new Date(self.StartTime()*1000);   
		return event;
    });
  
	self.CurrentDate = ko.computed(function() {
		var event = new Date(self.CurrentTime()*1000);   
		return event;
    });
 
  
  
	self.refresh =function(){
		fetch("http://192.168.0.222/ajax_inputs").then (status)
		.then(function(response) {
		return response.json();
		}).then(function(myJson) {
		console.log('Get request succeeded with resp ',myJson);
		self.Name(myJson.Name); 
		self.Temp(myJson.Temp);
		self.RelH(myJson.RelH);
		self.Tset(myJson.Tset);
		self.Heater(myJson.Heater);
		self.Control(myJson.Control);
		self.CurrentTime(myJson.CurrentTime);
		self.StartTime(myJson.StartTime);
		self.Wifi_ssi(myJson.Wifi_ssi);
		}).catch(function(error){
		console.log('Request Failed',error);
		})
	};

	self.save = function(){
		var plainJs = ko.toJS(self); 
		console.log('Testing, Testing',plainJs.Tset);  
		fetch('http://192.168.0.222', {
		method: 'POST',
		headers: {
		'Accept': 'application/json',
		'Access-Control-Allow-Origin': '*',
		'Content-Type': 'application/json'
		},
		credentials: 'same-origin',
		body: JSON.stringify({"Tset":plainJs.TsetDelayed, "Control":plainJs.Control})
		}).then (status).catch(function(error){
		console.log('Request Failed',error);  
		});
	return true};  
  
    
	
  
	self.TsetDelayed.subscribe(function(newValue) {self.save()});
	self.RefreshDelayed.subscribe(function(newValue) {self.refresh()});
  
};
    
// Activates knockout.js
ko.applyBindings(new AppViewModel());    
   
</script>
</body>
</html>

stringmanipulation.ino

Arduino
This is a helper file for the code that handles some string manipulation that is reused in the main program.
void time_and_date_string(String & s, time_t t)
{
      s +=  String((hour(t))) + ":";
      s +=  String((minute(t))) + ":";
      s +=  String((second(t)))  + "  ";

      s +=  String((month(t))) + "/";
      s +=  String((day(t))) + "/";
      s +=  String((year(t))) ;
}

void unix_time_string (String & s, time_t t)
{
   s+= String(t);
}

void json_HTTP_header(String & s)
  {
      s += "HTTP/1.1 200 OK\n";
      s += "Access-Control-Allow-Origin: *\n";
      s += "Content-Type: application/json\n";
      s += "\n"; //This carry return is very important. Without it the response will not be sent.
  }

webunixtime.ino

Arduino
This is some helper code for the main program that can get the time stamp from a server on the internet and convert it to unix time.
/*
 *  Francesco Potort 2013 - GPLv3
 *
 * Send an HTTP packet and wait for the response, return the Unix time
 */

//time_t webUnixTime (Client &client)
time_t webUnixTime()
{
  time_t time = 0;

  // Just choose any reasonably busy web server, the load is really low
  if (client.connect("g.cn", 80))
    {
      // Make an HTTP 1.1 request which is missing a Host: header
      // compliant servers are required to answer with an error that includes
      // a Date: header.
      client.print(F("GET / HTTP/1.1 \r\n\r\n"));

      char buf[5];      // temporary buffer for characters
      client.setTimeout(5000);
      if (client.find((char *)"\r\nDate: ") // look for Date: header
    && client.readBytes(buf, 5) == 5) // discard
  {
    unsigned day = client.parseInt();    // day
    client.readBytes(buf, 1);    // discard
    client.readBytes(buf, 3);    // month
    int year = client.parseInt();    // year
    byte hour = client.parseInt();   // hour
    byte minute = client.parseInt(); // minute
    byte second = client.parseInt(); // second

    int daysInPrevMonths;
    switch (buf[0])
      {
      case 'F': daysInPrevMonths =  31; break; // Feb
      case 'S': daysInPrevMonths = 243; break; // Sep
      case 'O': daysInPrevMonths = 273; break; // Oct
      case 'N': daysInPrevMonths = 304; break; // Nov
      case 'D': daysInPrevMonths = 334; break; // Dec
      default:
        if (buf[0] == 'J' && buf[1] == 'a')
    daysInPrevMonths = 0;   // Jan
        else if (buf[0] == 'A' && buf[1] == 'p')
    daysInPrevMonths = 90;    // Apr
        else switch (buf[2])
         {
         case 'r': daysInPrevMonths =  59; break; // Mar
         case 'y': daysInPrevMonths = 120; break; // May
         case 'n': daysInPrevMonths = 151; break; // Jun
         case 'l': daysInPrevMonths = 181; break; // Jul
         default: // add a default label here to avoid compiler warning
         case 'g': daysInPrevMonths = 212; break; // Aug
         }
      }

    // This code will not work after February 2100
    // because it does not account for 2100 not being a leap year and because
    // we use the day variable as accumulator, which would overflow in 2149
    day += (year - 1970) * 365; // days from 1970 to the whole past year
    day += (year - 1969) >> 2;  // plus one day per leap year 
    day += daysInPrevMonths;  // plus days for previous months this year
    if (daysInPrevMonths >= 59  // if we are past February
        && ((year & 3) == 0)) // and this is a leap year
      day += 1;     // add one day
    // Remove today, add hours, minutes and seconds this month
    time = (((day-1ul) * 24 + hour) * 60 + minute) * 60 + second;
  }
    }
  delay(10);
  client.flush();
  client.stop();

  return time;
}

Github repository for code

Here is the link to the github repository for this project

Credits

filipmu

filipmu

2 projects • 12 followers
Engineer since the age of 10. In that time let the smoke out of many components and had many burned fingers while soldering.

Comments