Industrial manufacturing
Industrial Internet of Things | Industrial materials | Equipment Maintenance and Repair | Industrial programming |
home  MfgRobots >> Industrial manufacturing >  >> Manufacturing Technology >> Manufacturing process

Advanced Multi-Zone Heating Control System for Smart Homes

Components and supplies

Advanced Multi-Zone Heating Control System for Smart Homes
Arduino UNO
Use a Mega if you need to controll more then 5 zones (incl the extra zone for the rest of your house without floor heating)
×1
Keyes 8 channel 5Volt relay board
You need a board with 4 relays to control 2 floor zones (1 pump, 1 CV, 2 Valves), One extra relay is needed for every extra floor zone
×1
Honeywell MT8-230-NC Small Linear Thermoelectric Actuator (230v AC)
This is just one of many possible actuators available; You need 1 actuator (valve) per floor Unit group
×1

About this project

I builded this device because my kitchen was either to warm or to cold with a single thermostat in my living. Commercial Multi-Zone Heating Controllers (like EvoHome) are very expensive. This program captures the intelligence of these expensive systems, hosted on a simple Arduino Uno board. This fully resolved my issue.

Highlights/Features:

  • You only need to configure the pinning and number of zones
  • A simple Arduino Uno can control up to 5 Floor Unit zones
  • With an Arduino Mega the number of Zones is nearly unlimmited

The provided Program Controls:

  • The Floor unit pump
  • Aggregates all your zones as just one thermostat to the Central Heater
  • Valves used to open/close zones
  • A Watch Dog timer to ensure rock solid operation

Allows individual Heating per Zone:

  • Per zone a Thermostat to sense the request for heating
  • Per zone a Relay to control one or more valves to open/close the Floor unit groups of that zone
  • A room with multiple floor unit groups can be considered as one heating Zone (Wire the valves parallel to the Zone Relay)
  • This is not only more convenient, but saves energy as well as rooms don't become too warm anymore

Controls the Floor Unit Pump:

  • It basically only runs the pump when needed for heating. This already saves you 100-200 Euro electricity per year (compared to run the same pump 24/7 (80 Watt is 2kW Hour a day = 0.50 Euro per day)
  • Activates the floor unit pump at least once every 36 hours, for 8 minutes if there wasn't any heating request (Summer)
  • Prevents to Run the pump without opening the valves first; Taking into account these valves need 3-5 minutes

Optionally you can control the remainder of your house (rooms without floor heating) as well:

  • Here you typically will have thermostat knobs on your radiators; so only the rooms that are cold will heat up
  • Just add a thermostat in the room(s) you want to control. Wire these thermostats in parallel to the No_Zone input

Final notes:

Not all zones need to be controlled; only the zones that become either to warm or stay too cold (otherwise use the manual adjustable knobs on the floor unit)

I explicitly decided not to connect the device to the internet:

  • It would increase the risk of mal-functioning (has to be rock solid)
  • You can use smart thermostats to control your house. This controller offers nothing extra to adapt remotely

Code

  • ProjectCV.ino
  • Devices.h
ProjectCV.inoC/C++
/* 
 *  Floor Unit heating controller for multiple Rooms/Zones v1.0
 * 
 *  Copyright: the GNU General Public License version 3 (GPL-3.0) by Eric Kreuwels, 2017
 *  Credits: Peter Kreuwels for defining all use-cases that needed to be considered
 *   
 *  Although this setup already runs for over a full year for my ground floor, I'm not Liable for any error in the code
 *  It can be used as a good basis for your own needs, and should be tested before using
 *   
 *  Highlights/Features:
 *   - You only need to configure the pinning and number of zones 
 *     - A simple Arduino Uno can control up to 5 Floor Unit zones
 *     - With an Arduino Mega the number of Zones is nearly unlimmited
 *   - The provided Program controls: 
 *       - the Floor unit pump
 *       - Aggregates all your zones as just one thermostat to the CV heater
 *       - Valves to open/close zones
 *   - Allows individual Heating per Zone; 
 *     - Per zone a Thermostat to sense the request for heating
 *     - Per zone a Relay to control one or more valves to open/close the Floor unit groups of that zone
 *     - A room with multiple floor unit groups can be considered as one heating Zone (Wire the valves parallel to the Zone Relay)
 *     - This is not only more convenient, but saves energy as well as rooms don't become too warm anymore
 *   - Controls the Floor Unit Pump
 *     - It basically only runs the pump when needed for heating. This already saves you 100-200 Euro electricity per year, 
 *       compared to run the same pump 24/7 (80 Watt is 2kW Hour a day = 0.50 Euro per day)
 *     - Activates the floor unit pump at least once every 36 hours, for 8 minutes if there wasn't any heating request (Summer)
 *     - Prevents to Run the pump without opening the valves first; Taking into account these valves need 3-5 minutes
 *   - Optionally you can control the remainder of your house (rooms without floor heating) as well
 *     - Here you typically will have thermostat knobs on your radiators; so only the rooms that are cold will heat up
 *     - Just add a thermostat in the room(s) you want to control. Wire these thermostats in parallel to the No_Zone input
 *   - Final notes:
 *     - Not all zones need to be controlled; only the zones that become either to warm or stay too cold with 
 *       manual adjusted knobs on the floor unit
 */

#include <avr/wdt.h> // for Watchdog

// WARNING: FAST_MODE is for testing/evaluation/debug purposes (loop runs 50x faster)
// Be carefull using FAST_MODE with a real floor unit pump as it can get damaged with closed valves
// Valves need minimal 3 minutes to open. In FAST_MODE the program doesn't wait long enough before starting the pump

// #define FAST_MODE // 50 times faster execution; consider to disconnect your real CV/Pump!

// In Normal operation to loop runs 10 times per second; so 10 counts/second (600 represents ca 1 minute)
#define VALVE_TIME             3000L    // 5 minutes to open/close a valve (on the safe site; takes typically 3 to 5 minutes)
#ifdef FAST_MODE
#define PUMP_MAINTENANCE_TIME  108000L // For evaluation, activates Floor Unit pump maintenance run once per 4 minutes (time stamp 3 hours)
#else
#define PUMP_MAINTENANCE_TIME  1300000L // Activates Floor Unit pump maintenance run once per 36 hours. Needed to keep pump working
#endif
#define PUMP_ACTIVATION_TIME   5000L    // Activates the pump for ca 8 minutes (10 seconds in test mode)
#define COOLDOWN_TIME          18000L   // When heating is done, continue water circulation for another 30 minutes (40 seconds in test mode)
                                        // This enables further dissipation the heat into the floor (typically takes 15 to 30 minutes)

#include "./Devices.h" // valves, pumps, thermostat classes (use the constants defines above)
struct Zone {
  String name;
  Valve valve;
  Thermostat thermostat;
};

////////////////////////////////////////////////////
//   CONFIGURATION BLOCK                             

// Configure/reorder your pinning as you like (This my wiring on an Arduino Uno); 
// Note: pins 1 and 2 are still free to add an extra zone
#define HEATER_PIN     4  // output to a Relay that is wired with the Thermostat input of your heating system
#define FU_PUMP_PIN    5  // output to a Relay that switches the Floor Unit Pump
#define LIVING_VALVE   7  // Zone 1: output to a Relay that controls the Valve(s)
#define KITCHEN_VALVE  6  // Zone 2: output to a Relay that controls the Valve(s)
#define DINING_VALVE   3  // Zone 3: output to a Relay that controls the Valve(s)

#define LIVING_THERMO  8  // Zone 1; input wired to the thermostat in the living
#define KITCHEN_THERMO 9  // Zone 2; input wired to the thermostat in the kitchen
#define DINING_THERMO  11 // Zone 3; input wired to the thermostat in the dining
#define NO_ZONE_THERMO 10 // Optionally: thermostats in rooms without floor heating

#define HEATING_LED    12 // On when heating, Alternates during cooldown, is Off in idle mode
#define INDICATION_LED 13 // Alternates the on board LED to indicate board runs; can be easily removed to free an extra IO pin!!

// Configure the Floor Unit Zones/rooms. Each zone/room owns a name, valve and thermostat:
#define NR_ZONES 3
Zone Zones[NR_ZONES] = { {"Living Room",  Valve(LIVING_VALVE, "Living Valve"),  Thermostat(LIVING_THERMO, "Living Thermostat")},
                         {"Kitchen Area", Valve(KITCHEN_VALVE,"Kitchen Valve"), Thermostat(KITCHEN_THERMO,"Kitchen Thermostat")},
                         {"Dining Room",  Valve(DINING_VALVE, "Dining Valve"),  Thermostat(DINING_THERMO, "Dining Thermostat")}};

// END CONFIGURATION BLOCK                                           
//////////////////////////////////////////////////


// Some fixed devices:
LED           iLED(INDICATION_LED, "Indicator LED"); // can be removed if you run out of IO's
LED           hLED(HEATING_LED, "Heating LED");
Manipulator   CV(HEATER_PIN, "CV Heater");
Pump          FUPump(FU_PUMP_PIN, "Floor Unit Pump");
Thermostat    ZonelessThermo(NO_ZONE_THERMO, "Zoneless Thermostat"); // For the rest of the house, no related to the floor unit zone

void printConfiguration() {
   Serial.println("------ Board Configuration: ---------");
   iLED.Print();
   hLED.Print();
   CV.Print();
   FUPump.Print();
   ZonelessThermo.Print();
   for(int i=0; i<NR_ZONES; i++) {
      Serial.print("Zone["); Serial.print(i+1); 
      Serial.print("]: "); Serial.println(Zones[i].name);
      Serial.print(" - "); Zones[i].valve.Print();
      Serial.print(" - "); Zones[i].thermostat.Print();
   }
   Serial.println("-------------------------------------");
}

// state machine, with both transition and state handling actions
class State
{
  public:
  enum states {
    idle,
    on,
    cooldown
  };

  private:
  //vars
  states _State;
  unsigned long cooldownCount;

  public: 
  //constructor
  StateMAchine()   {
    _State = idle;
  }
  
  // Getter
  states const& operator()() const {
      return _State;    
  }

  // Setter
  void operator()(states const& newState) {
      printTimeStamp();
      Serial.print(": State change from [");
      Print();
      _State = newState;
      Serial.print("] to [");
      Print();
      Serial.println("]");
      // deal with transition actions to the new state
      switch(_State)
      {
       case idle:
          hLED.Off();
          CV.Off();  // stop heating
          FUPump.Off(); 
          allValvesOff();
          break;
       case on:
          hLED.On();
          CV.On(); // start heating, but Floor unit pump has to wait till at least one zone is open
          break;
       case cooldown:
          CV.Off();  // stop heating
          allValvesOn(); // open all zones for cooldown; pump has to wait for this
          break;  
       default: 
         Serial.println("WARNING Unhandled State transition");
         break;
      }
  }

  // Do the state handling; to be called repatively by the loop()  
  void doProcessingActions() {
    switch(_State) {
       case on:
        onProcessing(); // As long heating is requested open/close zones matching the heating requests
        break;    
      case cooldown:
        coolDownProcessing(); // take 30 minutes to dissipate remaing Heat into the floor
        break;
      case idle:
        idleProcessing(); // operate pumps/valves once per day
        break;
      default: 
        Serial.println("ERROR Unhandled State");
        break;
    }
  }

  void setCoolDownNeeded() {
    cooldownCount = COOLDOWN_TIME;
  }
  bool whileCoolDownNeeded() { // down counts the time
     if (cooldownCount > 0) {
        cooldownCount--;
     }
     return checkCoolDownNeeded();
  }
  bool checkCoolDownNeeded() {
     return (cooldownCount> 0);
  }
  
  void Print()   {
    switch(_State)
    {
     case idle:
        Serial.print("idle");
        break;
     case on:
        Serial.print("on");
        break;
     case cooldown:
         Serial.print("cooldown");
        break;  
    }
  }
};

// The global state machine
State    CVState;


void setup() 
{
  // initializations
  Serial.begin(115200);
  printTimeStamp();
  Serial.print(": ");
#ifdef FAST_MODE
  Serial.println("CV Zone Controller started in TestMode!\n"
                 " - Board time runs ca 50 times faster\n"
                 " - Pump maintenance cycle runs ever 3 hours instead once per 36 hours");
#else
  Serial.println("CV Zone Controller started. Time stamps (dd:hh:mm:ss)");
#endif
  Serial.println(" - Time stamps format (dd:hh:mm:ss)");
  printConfiguration();
  wdt_enable(WDTO_1S);  // Watchdog: reset board after one second, if no "pat the dog" received
}

void loop() 
{

#ifdef FAST_MODE
  delay(2); // 50 times faster so minutes become roughly seconds for debugging purpose; so every count for cooldown or idle is 0.002 second
#else
  delay(100); // Normal operation: loops approx 10 times per second; so every count for cooldown or idle is 0.1 second
#endif

  // Use Indication LED to show board is alive
  iLED.Alternate();

  // once per loop() the pump and valves need to opdate hteir administatrion
  FUPump.Update();
  for (int i=0; i<NR_ZONES; i++) {
    // Update valve administration for transition times to open/close
    Zones[i].valve.Update();
  }
  
  // Reset the WatchDog timer (pat the dog)
  wdt_reset();  

  // Do the state handling
  CVState.doProcessingActions();
}

////////////////////////////////////////////////////////////////////
// Processing Methods for each CV State
/////////////////////////////////////////

void  onProcessing() {
  if (ProcessThermostats())  { // returns true if at least one of the thermostats is on (switch closed) => stay in this state
    if (FloorPumpingAllowed())   {
       FUPump.On();
    }
    else {
       FUPump.Off(); 
    }
  }
  else if ( CVState.checkCoolDownNeeded() ) {  // Continue in cooldown state to keep pump running for a while
      CVState(State::cooldown); 
  }
  else  {  // skip cooldown for floor unit, go back to idle
      CVState(State::idle); 
  }
}


void coolDownProcessing() {
  hLED.Alternate();
  if (HeatingRequested()) {   // returns true when one of the thermostats is closed
    CVState(State::on);
  }
  else  {
    if ( CVState.whileCoolDownNeeded() ) {
      if (FloorPumpingAllowed()) {
         FUPump.On();
      } else {
         FUPump.Off();  
      }        
    }
    else {
      CVState(State::idle);
    }
  }
}

void idleProcessing() 
{
  if (HeatingRequested())  {    // returns true when one of the thermostats is closed
    CVState(State::on);
  }
  else
  {
    // During idle period this check will activate the Floor Unit Pump for 8 minutes per 36 hours to keep them operatable
    if ( FUPump.doMaintenanceRun()) {
      if (FUPump.IsOff()) {
        if ( allValvesOpen() == false ) { // start opening just once
          printTimeStamp(); Serial.println(": Start daily cycle for Floor Unit Pump; open valves: ");
          allValvesOn();
        }
        if (FloorPumpingAllowed()) { 
          // this takes ca 5 minutes after activating the valves (6 seconds in test mode)
          printTimeStamp(); Serial.println(": Start daily cycle for Floor Unit Pump; start pump ");
          FUPump.On(); 
        }
        
      }
    } 
    else if (FUPump.IsOn()) {  // no Maintenance needed. So stop pump if still running
        printTimeStamp(); Serial.println(": Stop daily cycle for Floor Unit Pump; stop pump and close valves");
        FUPump.Off();
        allValvesOff();
    }
  }
}

////////////////////////////////////////////////////////////////////
// Helper Methods used by the State handlers
/////////////////////////////////////////

void allValvesOff() {
  for (int i=0; i<NR_ZONES; i++) {
    Zones[i].valve.Off();
  }    
}

void allValvesOn() {
  for (int i=0; i<NR_ZONES; i++) {
    Zones[i].valve.On();
  }    
}

bool allValvesOpen() {
  for (int i=0; i<NR_ZONES; i++) {
    if ( Zones[i].valve.IsOff() ) {
       return false;
    }
  }
  return true;
}


bool FloorPumpingAllowed() 
{
  // returns true if at least one zone is open, taking Valve transition into account
  for (int i=0; i<NR_ZONES; i++) {
    if (Zones[i].valve.ValveIsOpen() ) {
      return true;
    }
  }
  return false; // all valves are closed
}

bool ProcessThermostats() // returns true when one of the thermostats is closed
{                         // (De-)Activates Floor zones
   bool heating=false;
   bool requested[NR_ZONES]; 
    
   if ( ZonelessThermo.IsOn() ) {
     heating = true;
   }
   for (int i=0; i<NR_ZONES; i++) {
     // record heating requests only once to avoid race conditions due changes in between
     requested[i] = Zones[i].thermostat.IsOn();
     if ( requested[i] ) {
       heating = true;
       CVState.setCoolDownNeeded(); // remember if there was a request for heating a floor unit zone
     }
   }
   for (int i=0; i<NR_ZONES; i++) {
     if ( requested[i] ) {
       // Selectively open valves for zones that request heating 
       Zones[i].valve.On();
     }
     else if (heating) {
       // Selectively close valves for zones that don't require heating anymore
       // Only close them if heating is still required because in cooldown all zones need to be open
       Zones[i].valve.Off();
     }
   }
   return heating;
}

bool HeatingRequested()
{
  if ( ZonelessThermo.IsOn() ) {
    return true;
  }
  for (int i=0; i<NR_ZONES; i++) {
    if (Zones[i].thermostat.IsOn() ) {
      return true;
    }
  }
  return false; // all Thermostats are open (no heating needed)
}

void printTimeStamp() {
  #ifdef FAST_MODE
    // 50 times faster; represent the time it would be in normal mode
     unsigned long seconds = millis()/(unsigned long)20;
  #else
    // Normal operation
     unsigned long seconds = millis()/(unsigned long)1000;
  #endif
    unsigned long minutes, hours, days;
    minutes = seconds / 60L;
    seconds %= 60L; 
    hours = minutes / 60L;
    minutes %= 60L;
    days = hours / 24L;
    hours %= 24L;
    char time[30]; 
    sprintf(time, "%02d:%02d:%02d:%02d", (int)days, (int)hours, (int)minutes, (int)seconds);
    Serial.print(time);
}
Devices.hC/C++
// Helper classes for IO devices
extern void printTimeStamp(); // defined in main ino file

// IODevice: base class for all IO devices; needs specialization
class IODevice { 
  //vars
  protected:
  bool _IsOn;
  int _Pin;
  String _Name;
  
  //constructor
  public:
  IODevice(int pin, String name)   {
    _IsOn = false;
    _Pin = pin;
    _Name= name;
  }
  //methods
  virtual bool IsOn() = 0; // abstract
  virtual bool IsOff() {   // default for all
    return !IsOn();
  }

  void DebugPrint()   {
    printTimeStamp();
    Serial.print(": ");
    Print();
  }
  void Print() {
    Serial.print(_Name);
    Serial.print(" on pin(");
    Serial.print(_Pin);
    if (_IsOn)
      Serial.println(") = On");
    else
      Serial.println(") = Off");
  }
};

// Thermostat: reads an digital input adding some dender surpression 
class Thermostat : public IODevice  
{
  //vars
  private:
  int _Counter; // used to prevent reading intermitted switching (dender)
  
  //constructor
  public:
  Thermostat(int pin, String name) : IODevice(pin, name)   {
    _Counter = 0;
    pinMode(_Pin, INPUT_PULLUP); 
  }

  //methods  
  virtual bool IsOn()   {
    if (digitalRead(_Pin) == HIGH  && _IsOn == true) // open contact while on
    {
      if( _Counter++ > 5) // only act after  5 times the same read out
      {
         _IsOn = false;
         DebugPrint();
         _Counter = 0;
      }
    }
    else if (digitalRead(_Pin) == LOW  && _IsOn == false) // closed contact while off
    {
      if( _Counter++ > 5) // only act after  5 times the same read out
      {
         _IsOn = true;
         DebugPrint();
         _Counter = 0;
      }
    }
    else 
    {
       _Counter = 0;
    }
    return _IsOn;
  }
};

// Manipulator: the most basic working device on an digital output  
class Manipulator : public IODevice
{
  //vars
  private:

  //constructor
  public:
  Manipulator(int pin, String name)  : IODevice(pin, name)   {
    pinMode(_Pin, OUTPUT);   
    digitalWrite(_Pin, HIGH);
  }
  //methods
  void On()    {
    if (_IsOn == false)
    {
      _IsOn = true;
      digitalWrite(_Pin, LOW);
      onSwitch();
    }
  }

  void Off()   {
    if (_IsOn == true)
    {
      _IsOn = false;
      digitalWrite(_Pin, HIGH);
      onSwitch();
    }
  }

  virtual void onSwitch() {  // trigger for child claases; change in on/off state
     DebugPrint();  
  }

  virtual bool IsOn()   {
    return _IsOn;
  }
};

// Valve: controlles themostatic valves on a digital output. 
// These valves react slowly (3-5 minutes) so this class adds this transition awareness
// loop() must call Update() to keep track if the valve is fully open or closed
class Valve : public Manipulator
{
  private:
  long transitionCount;

    //constructor
  public:
  Valve(int pin, String name) : Manipulator(pin, name)   {
    transitionCount = 0;
  }

  bool ValveIsOpen()   {
    return (IsOn() && (transitionCount>=VALVE_TIME)); // at least 5 minutes in on state
  }
  
  // Execute once per pass in the sketch loop() !!!
  void Update()   { 
    if (IsOn())     {
      if (transitionCount < VALVE_TIME)
        transitionCount++;
    }
    else     {
      if (transitionCount > 0)
        transitionCount--;
    }
  }
};


// Pump: a pump need to be activated several times a week to keep them going. 
// loop() must call Update() to keep track when a maintenance activation is needed
class Pump : public Manipulator
{
  // valves react slowly (3-5 minutes) so this class adds this transition awareness
  private:
  long counter;
  bool doMaintenance;

    //constructor
  public:
  Pump(int pin, String name) : Manipulator(pin, name)   {
    counter = 0;
    doMaintenance = false;
  }

  bool doMaintenanceRun()   {
    return doMaintenance;
  }

  virtual void onSwitch() {  // change in on/off state
     Manipulator::onSwitch();  
     counter = 0;
  }

  // run this method every pass in loop() 
  void Update()   {
    if (IsOn()) {
      if (counter < PUMP_ACTIVATION_TIME) {
        counter++;
      } else if (doMaintenance) {
        printTimeStamp();
        Serial.println(": Pump Maintenance cleared");
        doMaintenance = false;
      }
    }
    else {
      if (counter < PUMP_MAINTENANCE_TIME) {
        counter++;
      }  else if (doMaintenance==false) {
        printTimeStamp();
        Serial.println(": Pump Maintenance needed");
        doMaintenance = true;
      }
    }
  }
};

// LED; besides on/off it offers a method to alternate the LED (1Hz)
// just call Alternate() from the loop() to activate alternation
class LED : public Manipulator
{
  private:
  long counter;

    //constructor
  public:
  LED(int pin, String name) : Manipulator(pin, name)   {
    counter = 0;
  }

  virtual void onSwitch() {  // change in on/off state
    // surpress printing debug output for LEDs
  }

  void Alternate() {
  #ifdef FAST_MODE
    if (counter++ > 250)
  #else
    if (counter++ > 5)
  #endif  
    {  // toggle LED 
      counter=0;
      if (IsOn())
        Off();
      else
        On();
    }
  }
};

Schematics

Detailed wiring of periferals (Pump, Valves, Thermostats, LED's)Advanced Multi-Zone Heating Control System for Smart HomesExample of wiring multiple 'cascaded' controllers. One controller per Floor UnitAdvanced Multi-Zone Heating Control System for Smart HomesSome real logging of the Serial Monitor to understand the fuctionallity. The timestamps show e.g. a delay of 5 minutes between opening valves and actually starting the floor unit pump.logexample_fTczkAa0tf.txtInspirationalAdvanced Multi-Zone Heating Control System for Smart Homes

Manufacturing process

  1. Solar Heating Systems: From Passive to Active Solutions for Sustainable Home Heating
  2. Monitoring Your Central Heating Boiler Using a Raspberry Pi – Hardware, Software, and Setup Guide
  3. DIY Arduino USB Gaming Controller – Build Your Own High-Performance Gamepad
  4. Smart AV Cabinet Fan Controller – Arduino Nano, DHT11, and Relay for Optimal Cooling
  5. Arduino DMX-512 Tester Controller – Full Parts Kit for Reliable Lighting Control
  6. Arduino-Driven GrowBox Controller – Open-Source Firmware & Hardware Guide
  7. Smart Central Heating Boiler Control Box – Efficient Energy Management
  8. Arduino‑Powered Smart Irrigation Controller – Auto‑Watering with Weather & Light Sensors
  9. Comprehensive Pool Controller Kit – Raspberry Pi 2 + Arduino, Relay Boards, Temperature Sensors & Accessories
  10. Reheating Furnace Steel Heating: Enhancing Hot Rolling Efficiency