NodeMCU web controlled IoT thermostat with Maxim DS1624


Introduction

The aim of present project is both remotely and manually control the home temperature, acting on a relay connected to the main heating system.

The thermostat is intended to run in four different modes:

  1. “Living” maintaining a comfortable temperature when anyone is at home;
  2. “Night” that avoids too low room temperature when people is out or sleeping;
  3. “Off” mode that maintains powered off the heating system (but temperature reading is still performed and values are provided via web interface);
  4. “Fire” that maintains active the heating system for half an hour, and returns to the previous mode.

NodeMCU 1.0 platform

NodeMCU platform was chosen for the ease of use, the compatibility with Arduino IDE and the super low-cost. The limitations are the few GPIOs and the need of a power supply circuit.

For the purpose of this project it is sufficient to add few power handling components, a temperature sensor and a relay to have a full working web-controlled thermostat.

Temperature sensor

Due to the presence of the Wifi antenna some analog output sensors can present noise in output. So for this project a Maxim DS1624 digital sensor was chosen.

The Maxim DS1614 is a digital temperature sensor with 12-bit resolution and +/-0.5°C accuracy from 0°C to 70°C. It requires a supply voltage between 2.7V and 5.5V making it perfect to interface with a NodeMCU platform that offers several output pins from the 3.3V onboard voltage regulator.

DS1624 pinout

The sensor will be connected to the I2C pins of the NodeMCU platform: D1(SCL) and D2(SDA).

Note that it is important to add two pull-up resistors to I2C bus lines. Commonly used values are 20kohm and 4.7kohm in case of power supply of 5V. In this case a more accurate analysis is necessary:

  1. NodeMCU platform and DS1624 sensor works with 3.3V;
  2. Reading the DS1624 datasheet one can found that Low-Level Output Voltage (SDA) with symbol VOL is 0.4V, with a sink current of 3mA;
  3. So minimum pullup resistor value is (3.3 - 0.4) / 0.003 = 967 ohm.

A more general analysis can be found here: https://rheingoldheavy.com/i2c-pull-resistors/.

DS1624 wirings

Official web page of DS1624 arduino library

Power supply circuit

Low-drop 5v regulator: L4940

Final circuit

Installing libraries

The first library to add to your Arduino IDE is the NodeMCU platform code.

  1. Open Arduino IDE
  2. Go to File -> Preferences
  3. Add “http://arduino.esp8266.com/stable/package_esp8266com_index.json” to the “Additional Boards Manager URLs”, as showed in following figure:

Add ESP8266 platform to Arduino IDE

The second library to add to your Arduino IDE is Timer, by Simon Monk.

  1. Download library to ZIP file from github: https://github.com/JChristensen/Timer
  2. Rename Timer-master.zip in Timer.zip
  3. Open Arduino IDE
  4. Go to Sketch -> Include library -> Add .ZIP library…
  5. Select just downloaded file
  6. Library is installed.

The last library to add to your Arduino IDE is DS1624, by Alessio Leoncini.

  1. Open Arduino IDE
  2. Select Sketch -> Include Library -> Manage Libraries… Add ESP8266 platform to Arduino IDE
  3. Type "ds1624" in search bar, and install it Add ESP8266 platform to Arduino IDE

Code

/*****************************************************************************
* IoT Thermostat
* 
* Circuit connected with the home Wifi network.
* It reads the air temperature and it controls the heating system.
*
* The circuit can work on two main modes:
* - "Living" maintaining a comfortable temperature when anyone is at home;
* - "Night" that avoids too low room temperature when people is out or 
*   sleeping;
* - "Off" mode that maintain disabled the heating system.
* Another mode can be always set overriding others:  
* - "Fire" that maintains active the heating system for a specified time, and 
*   finally returns to the previous mode
*  
* The internal status of the circuit includes:
*  - relayStatus, heating system control (of/off)
*  - currentMode, storing the current circuit mode between Living, Night, 
*    Off, Fire (char, 'L', 'N', 'O' 'F' respectively)
*  - timeStamp, storing when the circuit was set for the first time to 
*    "Fire" mode (milliseconds, unsigned long)
*  - lastMode, the last mode different from Fire that will be set after the 
*    Fire timeout.
*****************************************************************************/

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <Event.h>
#include <Timer.h>  // Timer library by Simon Monk
#include <DS1624.h> // DS1624 library by Alessio Leoncini

#define CONNECTEDLED D0
#define NIGHTLED D3
#define FIRELED D4
#define MODEBUTTON D5
#define RELAY D6
#define LIVINGLED D7

const char * ssid = "yourwifissid";
const char * password = "yourwifipassword";
bool relayStatus = false;
unsigned long timeStamp = 0;
char currentMode = 'O';
char lastMode = 'O';
float livingTemp = 19.5;
float nightTemp = 17.5;
unsigned long fireTimeout = 1200000; //  20 minutes
float currentTemperature = 0;
unsigned long httpTimeStamp = 0;
bool httpServerActive = false;
unsigned long serverTimeout = 3000; // 3 seconds

ESP8266WebServer httpServer (36911);
Timer t;
DS1624 ds1624(0x00);

//////////////////////////////////////////////////////////////////////////////
// Setup routine
void setup()
{
  // Debug
  Serial.begin(9600);
  
  // Setup pin buffers
  pinMode(CONNECTEDLED, OUTPUT);
  pinMode(RELAY, OUTPUT);
  pinMode(NIGHTLED, OUTPUT);
  pinMode(LIVINGLED, OUTPUT);
  pinMode(FIRELED, OUTPUT);
  pinMode(MODEBUTTON, INPUT_PULLUP);

  // Start with relay off
  digitalWrite(RELAY, LOW);

  // Mode led
  ModeLedShow();

  // Configure wifi node
  bool stationConfigured = WiFi.config(
    IPAddress(192, 168, 1, 248), // local_ip
    IPAddress(192, 168, 1, 1),   // gateway
    IPAddress(255, 255, 255, 0)  // subnet
  );

  if(!stationConfigured)
  {
    Serial.println("Error, WiFi.config() returned false.");
  }
  
  // Connect to wifi
  bool stationConnected = WiFi.begin(ssid, password);

  if(!stationConnected)
  {
    Serial.println("Error, WiFi.config() returned false.");
  }

  // Set automatic reconnection
  bool autoreconnect = WiFi.setAutoReconnect(true);

  if(!autoreconnect)
  {
    Serial.println("Error, WiFi.setAutoReconnect() returned false.");
  }

  // Handle http request for root page
  httpServer.on("/", handleRoot);

  // Handle http request for "fire" page
  httpServer.on("/fire", [](){
    if(currentMode == 'F')
    {
      String message = "already set by ";
      message += ElapsedMillis(timeStamp, millis()) / 1000;
      message += " seconds";
      httpServer.send (200, "text/plain", message);
    }
    else
    {
      httpServer.send (200, "text/plain", "ok");
      SetFireMode();
    }

    // Stop server for some seconds
    StopServer();
  });

  // Handle http request for "living" page
  httpServer.on("/living", []() {
    httpServer.send (200, "text/plain", "ok");
    SetLivingMode();

    // Stop server for some seconds
    StopServer();
  });

  // Handle http request for "night" page
  httpServer.on("/night", []() {
    httpServer.send (200, "text/plain", "ok");
    SetNightMode();

    // Stop server for some seconds
    StopServer();
  });

  // Handle http request for "off" page
  httpServer.on("/off", []() {
    httpServer.send (200, "text/plain", "ok");
    SetOffMode();

    // Stop server for some seconds
    StopServer();
  });

  // Handle http request for error 404
  httpServer.onNotFound(handleNotFound);

  // Start http server
  ActivateServer();

  // Read temperature for the first time
  ReadTemperature();

  // Read and save temperature every 10 seconds
  t.every(10000, ReadTemperature);
}

//////////////////////////////////////////////////////////////////////////////
// Main
void loop()
{
  // Check connection
  bool wifiConnected = WifiConnected();

  // Set led (inverted logic)
  digitalWrite(CONNECTEDLED, wifiConnected ? LOW : HIGH);

  // Switch modes if button pressed
  CheckManualCommand();

  // Handle http requests
  HandleHttpRequests();

  // Here perform a check wether the "Fire" mode is to be ended
  CheckIfTerminateFire();

  // Handle current mode
  HandleCurrentMode();

  // Handle timer
  t.update();
}

//////////////////////////////////////////////////////////////////////////////
// Check connection
bool WifiConnected()
{
  return WiFi.status() == WL_CONNECTED;
}

//////////////////////////////////////////////////////////////////////////////
// HTTP server - not found
void handleNotFound()
{
  httpServer.send(404, "text/plain", "");

  // Stop server for some seconds
  StopServer();
}

//////////////////////////////////////////////////////////////////////////////
// HTTP server - root page
void handleRoot()
{  
  // Print temperature
  String message = "T=";
  message += currentTemperature;

  // Print relay status
  message += "\nR=";
  relayStatus ? message += "1\n" : message += "0\n";

  // Print current mode
  message += "M=";
  message += currentMode;
  message += "\n";
  
  httpServer.send(200, "text/plain", message);

  // Stop server for some seconds
  StopServer();
}

//////////////////////////////////////////////////////////////////////////////
// Power on the correct mode led
void ModeLedShow()
{
  digitalWrite(NIGHTLED, LOW);
  digitalWrite(LIVINGLED, LOW);
  digitalWrite(FIRELED, LOW);

  switch(currentMode)
  {
    case 'N':
      digitalWrite(NIGHTLED, HIGH);
      break;
    case 'L':
      digitalWrite(LIVINGLED, HIGH);
      break;
    case 'F':
      digitalWrite(FIRELED, HIGH);
      break;
    default:
      break;
  }
}

//////////////////////////////////////////////////////////////////////////////
// If button pressed, cycle through thermostat modes 
void CheckManualCommand()
{  
  // If button is pressed, it is possible to cycle modes.
  // Releasing the button allows to select a new mode.
  // Button is connected between MODEBUTTON pin and ground; MODEBUTTON pin 
  // presents the internal pullup resistor enabled
  char newMode = currentMode;
  while(digitalRead(MODEBUTTON) == LOW)
  {
    // If button is pressed, mode led blinks
    Blink1Sec(newMode);
    
    // If button is released, exit from cycling modes
    if(digitalRead(MODEBUTTON) == HIGH)
    {
      break;
    }

    // If button stil pressed, change selected mode
    switch(newMode)
    {
      case 'N':
      default:
        newMode = 'L';
        break;
      case 'L':
        newMode = 'F';
        break;
      case 'F':
        newMode = 'O';
        break;
      case 'O':
        newMode = 'N';
        break;
    }
  }

  // If button was released in an instant when led blinks were of a different
  // color from original one, the thermostat will apply a new mode.  
  if(newMode != currentMode)
  {
    switch(newMode)
    {
      case 'N':
      default:
        SetNightMode();
        break;
      case 'L':
        SetLivingMode();
        break;
      case 'O':
        SetOffMode();
        break;
      case 'F':
        SetFireMode();
        break;
    }
  }
}

//////////////////////////////////////////////////////////////////////////////
// Sets Living mode 
void SetLivingMode()
{
  currentMode = 'L';
  ModeLedShow();
}

//////////////////////////////////////////////////////////////////////////////
// Sets Night mode 
void SetNightMode()
{
  currentMode = 'N';
  ModeLedShow();
}

//////////////////////////////////////////////////////////////////////////////
// Sets Off mode 
void SetOffMode()
{
  currentMode = 'O';
  ModeLedShow();
}

//////////////////////////////////////////////////////////////////////////////
// Sets Fire mode 
void SetFireMode()
{
  // If already in Fire mode, do nothing
  if(currentMode == 'F')
  {
    return;
  }

  // Save preceding mode
  lastMode = currentMode;

  // Set flags
  currentMode = 'F';
  ModeLedShow();
  
  // Save timestamp
  timeStamp = millis();
}

//////////////////////////////////////////////////////////////////////////////
// Blink the led corresponding with specified mode for one second
void Blink1Sec(char newMode)
{
  for(int i = 0; i < 5; i++)
  {
    digitalWrite(NIGHTLED, LOW);
    digitalWrite(LIVINGLED, LOW);
    digitalWrite(FIRELED, LOW);
    delay(100);
    switch(newMode)
    {
      case 'N':
        digitalWrite(NIGHTLED, HIGH);
        break;
      case 'L':
        digitalWrite(LIVINGLED, HIGH);
        break;
      case 'F':
        digitalWrite(FIRELED, HIGH);
        break;
      default:
        break;
    }
    delay(100);
  }
}

//////////////////////////////////////////////////////////////////////////////
// Reads the current temperature (in Celsius)
void ReadTemperature()
{
  bool valid = false;
  float temperature = 0.0f;

  // First try
  ds1624.GetTemperature(temperature, valid);
  
  if(valid)
  {
    currentTemperature = temperature;
    return;
  }
  
  // Second and last try
  ds1624.GetTemperature(temperature, valid);
  
  if(valid)
  {
    currentTemperature = temperature;
  }
}

//////////////////////////////////////////////////////////////////////////////
// Check timestamp to possibly terminate "Fire" mode
void CheckIfTerminateFire()
{
  if(ElapsedMillis(timeStamp, millis()) > fireTimeout)
  {
    FireTimeout();
  }
}

//////////////////////////////////////////////////////////////////////////////
// Elapsed milliseconds
unsigned long ElapsedMillis(unsigned long t0, unsigned long t1)
{
  // Check overflow
  if(t0 > t1)
  {
    return 4294967295UL - t0 + t1;
  }
  else
  {
    return t1 - t0;
  }
}

//////////////////////////////////////////////////////////////////////////////
// Timeout occurred. Switch to last mode
void FireTimeout()
{
  switch(lastMode)
  {
  case 'N':
  default:
    SetNightMode();
    break;
  case 'L':
    SetLivingMode();
    break;
  case 'O':
    SetOffMode();
    break;
  }
}

//////////////////////////////////////////////////////////////////////////////
// Handle temperature for current thermostat mode
void HandleCurrentMode()
{
  switch(currentMode)
  {
  case 'N':
    if(currentTemperature < (nightTemp - 0.5f))
    {
      relayStatus = true;
    }
    else if(currentTemperature > nightTemp)
    {
      relayStatus = false;
    }
    break;
  case 'L':
    if(currentTemperature < (livingTemp - 0.5f))
    {
      relayStatus = true;
    }
    else if(currentTemperature > livingTemp)
    {
      relayStatus = false;
    }
    break;
  case 'F':
    relayStatus = true;
    break;
  default:
    relayStatus = false;
    break;
  }

  digitalWrite(RELAY, relayStatus ? HIGH : LOW);
}

//////////////////////////////////////////////////////////////////////////////
// Handle http requests
void HandleHttpRequests()
{
  // When a http request is taken into account, server is shut down for
  // some seconds. If such timeout is expired, restart http server
  if(!httpServerActive)
  {
    if(ElapsedMillis(httpTimeStamp, millis()) > serverTimeout)
    {
      ActivateServer();
    }
  }

  // If server active, take care of http requests
  if(httpServerActive)
  {
    httpServer.handleClient();
  }
}

//////////////////////////////////////////////////////////////////////////////
// Activate http server and set proper flags
void ActivateServer()
{
  if(!httpServerActive)
  {
    httpServer.begin();
    httpServerActive = true;
  }
}

//////////////////////////////////////////////////////////////////////////////
// Stops http server and set proper flags
void StopServer()
{
  if(httpServerActive)
  {
    httpServer.stop();
    httpServerActive = false;
    httpTimeStamp = millis();
  }
}

Comments