Components and supplies
Arduino UNO
Honeywell MT8-230-NC Small Linear Thermoelectric Actuator (230v AC)
Keyes 8 channel 5Volt relay board
Project description
Code
Devices.h
c_cpp
1// Helper classes for IO devices 2extern void printTimeStamp(); // defined in main ino file 3 4// IODevice: base class for all IO devices; needs specialization 5class IODevice { 6 //vars 7 protected: 8 bool _IsOn; 9 int _Pin; 10 String _Name; 11 12 //constructor 13 public: 14 IODevice(int pin, String name) { 15 _IsOn = false; 16 _Pin = pin; 17 _Name= name; 18 } 19 //methods 20 virtual bool IsOn() = 0; // abstract 21 virtual bool IsOff() { // default for all 22 return !IsOn(); 23 } 24 25 void DebugPrint() { 26 printTimeStamp(); 27 Serial.print(": "); 28 Print(); 29 } 30 void Print() { 31 Serial.print(_Name); 32 Serial.print(" on pin("); 33 Serial.print(_Pin); 34 if (_IsOn) 35 Serial.println(") = On"); 36 else 37 Serial.println(") = Off"); 38 } 39}; 40 41// Thermostat: reads an digital input adding some dender surpression 42class Thermostat : public IODevice 43{ 44 //vars 45 private: 46 int _Counter; // used to prevent reading intermitted switching (dender) 47 48 //constructor 49 public: 50 Thermostat(int pin, String name) : IODevice(pin, name) { 51 _Counter = 0; 52 pinMode(_Pin, INPUT_PULLUP); 53 } 54 55 //methods 56 virtual bool IsOn() { 57 if (digitalRead(_Pin) == HIGH && _IsOn == true) // open contact while on 58 { 59 if( _Counter++ > 5) // only act after 5 times the same read out 60 { 61 _IsOn = false; 62 DebugPrint(); 63 _Counter = 0; 64 } 65 } 66 else if (digitalRead(_Pin) == LOW && _IsOn == false) // closed contact while off 67 { 68 if( _Counter++ > 5) // only act after 5 times the same read out 69 { 70 _IsOn = true; 71 DebugPrint(); 72 _Counter = 0; 73 } 74 } 75 else 76 { 77 _Counter = 0; 78 } 79 return _IsOn; 80 } 81}; 82 83// Manipulator: the most basic working device on an digital output 84class Manipulator : public IODevice 85{ 86 //vars 87 private: 88 89 //constructor 90 public: 91 Manipulator(int pin, String name) : IODevice(pin, name) { 92 pinMode(_Pin, OUTPUT); 93 digitalWrite(_Pin, HIGH); 94 } 95 //methods 96 void On() { 97 if (_IsOn == false) 98 { 99 _IsOn = true; 100 digitalWrite(_Pin, LOW); 101 onSwitch(); 102 } 103 } 104 105 void Off() { 106 if (_IsOn == true) 107 { 108 _IsOn = false; 109 digitalWrite(_Pin, HIGH); 110 onSwitch(); 111 } 112 } 113 114 virtual void onSwitch() { // trigger for child claases; change in on/off state 115 DebugPrint(); 116 } 117 118 virtual bool IsOn() { 119 return _IsOn; 120 } 121}; 122 123// Valve: controlles themostatic valves on a digital output. 124// These valves react slowly (3-5 minutes) so this class adds this transition awareness 125// loop() must call Update() to keep track if the valve is fully open or closed 126class Valve : public Manipulator 127{ 128 private: 129 long transitionCount; 130 131 //constructor 132 public: 133 Valve(int pin, String name) : Manipulator(pin, name) { 134 transitionCount = 0; 135 } 136 137 bool ValveIsOpen() { 138 return (IsOn() && (transitionCount>=VALVE_TIME)); // at least 5 minutes in on state 139 } 140 141 // Execute once per pass in the sketch loop() !!! 142 void Update() { 143 if (IsOn()) { 144 if (transitionCount < VALVE_TIME) 145 transitionCount++; 146 } 147 else { 148 if (transitionCount > 0) 149 transitionCount--; 150 } 151 } 152}; 153 154 155// Pump: a pump need to be activated several times a week to keep them going. 156// loop() must call Update() to keep track when a maintenance activation is needed 157class Pump : public Manipulator 158{ 159 // valves react slowly (3-5 minutes) so this class adds this transition awareness 160 private: 161 long counter; 162 bool doMaintenance; 163 164 //constructor 165 public: 166 Pump(int pin, String name) : Manipulator(pin, name) { 167 counter = 0; 168 doMaintenance = false; 169 } 170 171 bool doMaintenanceRun() { 172 return doMaintenance; 173 } 174 175 virtual void onSwitch() { // change in on/off state 176 Manipulator::onSwitch(); 177 counter = 0; 178 } 179 180 // run this method every pass in loop() 181 void Update() { 182 if (IsOn()) { 183 if (counter < PUMP_ACTIVATION_TIME) { 184 counter++; 185 } else if (doMaintenance) { 186 printTimeStamp(); 187 Serial.println(": Pump Maintenance cleared"); 188 doMaintenance = false; 189 } 190 } 191 else { 192 if (counter < PUMP_MAINTENANCE_TIME) { 193 counter++; 194 } else if (doMaintenance==false) { 195 printTimeStamp(); 196 Serial.println(": Pump Maintenance needed"); 197 doMaintenance = true; 198 } 199 } 200 } 201}; 202 203// LED; besides on/off it offers a method to alternate the LED (1Hz) 204// just call Alternate() from the loop() to activate alternation 205class LED : public Manipulator 206{ 207 private: 208 long counter; 209 210 //constructor 211 public: 212 LED(int pin, String name) : Manipulator(pin, name) { 213 counter = 0; 214 } 215 216 virtual void onSwitch() { // change in on/off state 217 // surpress printing debug output for LEDs 218 } 219 220 void Alternate() { 221 #ifdef FAST_MODE 222 if (counter++ > 250) 223 #else 224 if (counter++ > 5) 225 #endif 226 { // toggle LED 227 counter=0; 228 if (IsOn()) 229 Off(); 230 else 231 On(); 232 } 233 } 234}; 235 236 237
ProjectCV.ino
c_cpp
1/* 2 * Floor Unit heating controller for multiple Rooms/Zones v1.0 3 * 4 * Copyright: the GNU General Public License version 3 (GPL-3.0) by Eric Kreuwels, 2017 5 * Credits: Peter Kreuwels for defining all use-cases that needed to be considered 6 * 7 * Although this setup already runs for over a full year for my ground floor, I'm not Liable for any error in the code 8 * It can be used as a good basis for your own needs, and should be tested before using 9 * 10 * Highlights/Features: 11 * - You only need to configure the pinning and number of zones 12 * - A simple Arduino Uno can control up to 5 Floor Unit zones 13 * - With an Arduino Mega the number of Zones is nearly unlimmited 14 * - The provided Program controls: 15 * - the Floor unit pump 16 * - Aggregates all your zones as just one thermostat to the CV heater 17 * - Valves to open/close zones 18 * - Allows individual Heating per Zone; 19 * - Per zone a Thermostat to sense the request for heating 20 * - Per zone a Relay to control one or more valves to open/close the Floor unit groups of that zone 21 * - A room with multiple floor unit groups can be considered as one heating Zone (Wire the valves parallel to the Zone Relay) 22 * - This is not only more convenient, but saves energy as well as rooms don't become too warm anymore 23 * - Controls the Floor Unit Pump 24 * - It basically only runs the pump when needed for heating. This already saves you 100-200 Euro electricity per year, 25 * compared to run the same pump 24/7 (80 Watt is 2kW Hour a day = 0.50 Euro per day) 26 * - Activates the floor unit pump at least once every 36 hours, for 8 minutes if there wasn't any heating request (Summer) 27 * - Prevents to Run the pump without opening the valves first; Taking into account these valves need 3-5 minutes 28 * - Optionally you can control the remainder of your house (rooms without floor heating) as well 29 * - Here you typically will have thermostat knobs on your radiators; so only the rooms that are cold will heat up 30 * - Just add a thermostat in the room(s) you want to control. Wire these thermostats in parallel to the No_Zone input 31 * - Final notes: 32 * - Not all zones need to be controlled; only the zones that become either to warm or stay too cold with 33 * manual adjusted knobs on the floor unit 34 */ 35 36#include <avr/wdt.h> // for Watchdog 37 38// WARNING: FAST_MODE is for testing/evaluation/debug purposes (loop runs 50x faster) 39// Be carefull using FAST_MODE with a real floor unit pump as it can get damaged with closed valves 40// Valves need minimal 3 minutes to open. In FAST_MODE the program doesn't wait long enough before starting the pump 41 42// #define FAST_MODE // 50 times faster execution; consider to disconnect your real CV/Pump! 43 44// In Normal operation to loop runs 10 times per second; so 10 counts/second (600 represents ca 1 minute) 45#define VALVE_TIME 3000L // 5 minutes to open/close a valve (on the safe site; takes typically 3 to 5 minutes) 46#ifdef FAST_MODE 47#define PUMP_MAINTENANCE_TIME 108000L // For evaluation, activates Floor Unit pump maintenance run once per 4 minutes (time stamp 3 hours) 48#else 49#define PUMP_MAINTENANCE_TIME 1300000L // Activates Floor Unit pump maintenance run once per 36 hours. Needed to keep pump working 50#endif 51#define PUMP_ACTIVATION_TIME 5000L // Activates the pump for ca 8 minutes (10 seconds in test mode) 52#define COOLDOWN_TIME 18000L // When heating is done, continue water circulation for another 30 minutes (40 seconds in test mode) 53 // This enables further dissipation the heat into the floor (typically takes 15 to 30 minutes) 54 55#include "./Devices.h" // valves, pumps, thermostat classes (use the constants defines above) 56struct Zone { 57 String name; 58 Valve valve; 59 Thermostat thermostat; 60}; 61 62//////////////////////////////////////////////////// 63// CONFIGURATION BLOCK 64 65// Configure/reorder your pinning as you like (This my wiring on an Arduino Uno); 66// Note: pins 1 and 2 are still free to add an extra zone 67#define HEATER_PIN 4 // output to a Relay that is wired with the Thermostat input of your heating system 68#define FU_PUMP_PIN 5 // output to a Relay that switches the Floor Unit Pump 69#define LIVING_VALVE 7 // Zone 1: output to a Relay that controls the Valve(s) 70#define KITCHEN_VALVE 6 // Zone 2: output to a Relay that controls the Valve(s) 71#define DINING_VALVE 3 // Zone 3: output to a Relay that controls the Valve(s) 72 73#define LIVING_THERMO 8 // Zone 1; input wired to the thermostat in the living 74#define KITCHEN_THERMO 9 // Zone 2; input wired to the thermostat in the kitchen 75#define DINING_THERMO 11 // Zone 3; input wired to the thermostat in the dining 76#define NO_ZONE_THERMO 10 // Optionally: thermostats in rooms without floor heating 77 78#define HEATING_LED 12 // On when heating, Alternates during cooldown, is Off in idle mode 79#define INDICATION_LED 13 // Alternates the on board LED to indicate board runs; can be easily removed to free an extra IO pin!! 80 81// Configure the Floor Unit Zones/rooms. Each zone/room owns a name, valve and thermostat: 82#define NR_ZONES 3 83Zone Zones[NR_ZONES] = { {"Living Room", Valve(LIVING_VALVE, "Living Valve"), Thermostat(LIVING_THERMO, "Living Thermostat")}, 84 {"Kitchen Area", Valve(KITCHEN_VALVE,"Kitchen Valve"), Thermostat(KITCHEN_THERMO,"Kitchen Thermostat")}, 85 {"Dining Room", Valve(DINING_VALVE, "Dining Valve"), Thermostat(DINING_THERMO, "Dining Thermostat")}}; 86 87// END CONFIGURATION BLOCK 88////////////////////////////////////////////////// 89 90 91// Some fixed devices: 92LED iLED(INDICATION_LED, "Indicator LED"); // can be removed if you run out of IO's 93LED hLED(HEATING_LED, "Heating LED"); 94Manipulator CV(HEATER_PIN, "CV Heater"); 95Pump FUPump(FU_PUMP_PIN, "Floor Unit Pump"); 96Thermostat ZonelessThermo(NO_ZONE_THERMO, "Zoneless Thermostat"); // For the rest of the house, no related to the floor unit zone 97 98void printConfiguration() { 99 Serial.println("------ Board Configuration: ---------"); 100 iLED.Print(); 101 hLED.Print(); 102 CV.Print(); 103 FUPump.Print(); 104 ZonelessThermo.Print(); 105 for(int i=0; i<NR_ZONES; i++) { 106 Serial.print("Zone["); Serial.print(i+1); 107 Serial.print("]: "); Serial.println(Zones[i].name); 108 Serial.print(" - "); Zones[i].valve.Print(); 109 Serial.print(" - "); Zones[i].thermostat.Print(); 110 } 111 Serial.println("-------------------------------------"); 112} 113 114// state machine, with both transition and state handling actions 115class State 116{ 117 public: 118 enum states { 119 idle, 120 on, 121 cooldown 122 }; 123 124 private: 125 //vars 126 states _State; 127 unsigned long cooldownCount; 128 129 public: 130 //constructor 131 StateMAchine() { 132 _State = idle; 133 } 134 135 // Getter 136 states const& operator()() const { 137 return _State; 138 } 139 140 // Setter 141 void operator()(states const& newState) { 142 printTimeStamp(); 143 Serial.print(": State change from ["); 144 Print(); 145 _State = newState; 146 Serial.print("] to ["); 147 Print(); 148 Serial.println("]"); 149 // deal with transition actions to the new state 150 switch(_State) 151 { 152 case idle: 153 hLED.Off(); 154 CV.Off(); // stop heating 155 FUPump.Off(); 156 allValvesOff(); 157 break; 158 case on: 159 hLED.On(); 160 CV.On(); // start heating, but Floor unit pump has to wait till at least one zone is open 161 break; 162 case cooldown: 163 CV.Off(); // stop heating 164 allValvesOn(); // open all zones for cooldown; pump has to wait for this 165 break; 166 default: 167 Serial.println("WARNING Unhandled State transition"); 168 break; 169 } 170 } 171 172 // Do the state handling; to be called repatively by the loop() 173 void doProcessingActions() { 174 switch(_State) { 175 case on: 176 onProcessing(); // As long heating is requested open/close zones matching the heating requests 177 break; 178 case cooldown: 179 coolDownProcessing(); // take 30 minutes to dissipate remaing Heat into the floor 180 break; 181 case idle: 182 idleProcessing(); // operate pumps/valves once per day 183 break; 184 default: 185 Serial.println("ERROR Unhandled State"); 186 break; 187 } 188 } 189 190 void setCoolDownNeeded() { 191 cooldownCount = COOLDOWN_TIME; 192 } 193 bool whileCoolDownNeeded() { // down counts the time 194 if (cooldownCount > 0) { 195 cooldownCount--; 196 } 197 return checkCoolDownNeeded(); 198 } 199 bool checkCoolDownNeeded() { 200 return (cooldownCount> 0); 201 } 202 203 void Print() { 204 switch(_State) 205 { 206 case idle: 207 Serial.print("idle"); 208 break; 209 case on: 210 Serial.print("on"); 211 break; 212 case cooldown: 213 Serial.print("cooldown"); 214 break; 215 } 216 } 217}; 218 219// The global state machine 220State CVState; 221 222 223void setup() 224{ 225 // initializations 226 Serial.begin(115200); 227 printTimeStamp(); 228 Serial.print(": "); 229#ifdef FAST_MODE 230 Serial.println("CV Zone Controller started in TestMode!\ 231" 232 " - Board time runs ca 50 times faster\ 233" 234 " - Pump maintenance cycle runs ever 3 hours instead once per 36 hours"); 235#else 236 Serial.println("CV Zone Controller started. Time stamps (dd:hh:mm:ss)"); 237#endif 238 Serial.println(" - Time stamps format (dd:hh:mm:ss)"); 239 printConfiguration(); 240 wdt_enable(WDTO_1S); // Watchdog: reset board after one second, if no "pat the dog" received 241} 242 243void loop() 244{ 245 246#ifdef FAST_MODE 247 delay(2); // 50 times faster so minutes become roughly seconds for debugging purpose; so every count for cooldown or idle is 0.002 second 248#else 249 delay(100); // Normal operation: loops approx 10 times per second; so every count for cooldown or idle is 0.1 second 250#endif 251 252 // Use Indication LED to show board is alive 253 iLED.Alternate(); 254 255 // once per loop() the pump and valves need to opdate hteir administatrion 256 FUPump.Update(); 257 for (int i=0; i<NR_ZONES; i++) { 258 // Update valve administration for transition times to open/close 259 Zones[i].valve.Update(); 260 } 261 262 // Reset the WatchDog timer (pat the dog) 263 wdt_reset(); 264 265 // Do the state handling 266 CVState.doProcessingActions(); 267} 268 269//////////////////////////////////////////////////////////////////// 270// Processing Methods for each CV State 271///////////////////////////////////////// 272 273void onProcessing() { 274 if (ProcessThermostats()) { // returns true if at least one of the thermostats is on (switch closed) => stay in this state 275 if (FloorPumpingAllowed()) { 276 FUPump.On(); 277 } 278 else { 279 FUPump.Off(); 280 } 281 } 282 else if ( CVState.checkCoolDownNeeded() ) { // Continue in cooldown state to keep pump running for a while 283 CVState(State::cooldown); 284 } 285 else { // skip cooldown for floor unit, go back to idle 286 CVState(State::idle); 287 } 288} 289 290 291void coolDownProcessing() { 292 hLED.Alternate(); 293 if (HeatingRequested()) { // returns true when one of the thermostats is closed 294 CVState(State::on); 295 } 296 else { 297 if ( CVState.whileCoolDownNeeded() ) { 298 if (FloorPumpingAllowed()) { 299 FUPump.On(); 300 } else { 301 FUPump.Off(); 302 } 303 } 304 else { 305 CVState(State::idle); 306 } 307 } 308} 309 310void idleProcessing() 311{ 312 if (HeatingRequested()) { // returns true when one of the thermostats is closed 313 CVState(State::on); 314 } 315 else 316 { 317 // During idle period this check will activate the Floor Unit Pump for 8 minutes per 36 hours to keep them operatable 318 if ( FUPump.doMaintenanceRun()) { 319 if (FUPump.IsOff()) { 320 if ( allValvesOpen() == false ) { // start opening just once 321 printTimeStamp(); Serial.println(": Start daily cycle for Floor Unit Pump; open valves: "); 322 allValvesOn(); 323 } 324 if (FloorPumpingAllowed()) { 325 // this takes ca 5 minutes after activating the valves (6 seconds in test mode) 326 printTimeStamp(); Serial.println(": Start daily cycle for Floor Unit Pump; start pump "); 327 FUPump.On(); 328 } 329 330 } 331 } 332 else if (FUPump.IsOn()) { // no Maintenance needed. So stop pump if still running 333 printTimeStamp(); Serial.println(": Stop daily cycle for Floor Unit Pump; stop pump and close valves"); 334 FUPump.Off(); 335 allValvesOff(); 336 } 337 } 338} 339 340//////////////////////////////////////////////////////////////////// 341// Helper Methods used by the State handlers 342///////////////////////////////////////// 343 344void allValvesOff() { 345 for (int i=0; i<NR_ZONES; i++) { 346 Zones[i].valve.Off(); 347 } 348} 349 350void allValvesOn() { 351 for (int i=0; i<NR_ZONES; i++) { 352 Zones[i].valve.On(); 353 } 354} 355 356bool allValvesOpen() { 357 for (int i=0; i<NR_ZONES; i++) { 358 if ( Zones[i].valve.IsOff() ) { 359 return false; 360 } 361 } 362 return true; 363} 364 365 366bool FloorPumpingAllowed() 367{ 368 // returns true if at least one zone is open, taking Valve transition into account 369 for (int i=0; i<NR_ZONES; i++) { 370 if (Zones[i].valve.ValveIsOpen() ) { 371 return true; 372 } 373 } 374 return false; // all valves are closed 375} 376 377bool ProcessThermostats() // returns true when one of the thermostats is closed 378{ // (De-)Activates Floor zones 379 bool heating=false; 380 bool requested[NR_ZONES]; 381 382 if ( ZonelessThermo.IsOn() ) { 383 heating = true; 384 } 385 for (int i=0; i<NR_ZONES; i++) { 386 // record heating requests only once to avoid race conditions due changes in between 387 requested[i] = Zones[i].thermostat.IsOn(); 388 if ( requested[i] ) { 389 heating = true; 390 CVState.setCoolDownNeeded(); // remember if there was a request for heating a floor unit zone 391 } 392 } 393 for (int i=0; i<NR_ZONES; i++) { 394 if ( requested[i] ) { 395 // Selectively open valves for zones that request heating 396 Zones[i].valve.On(); 397 } 398 else if (heating) { 399 // Selectively close valves for zones that don't require heating anymore 400 // Only close them if heating is still required because in cooldown all zones need to be open 401 Zones[i].valve.Off(); 402 } 403 } 404 return heating; 405} 406 407bool HeatingRequested() 408{ 409 if ( ZonelessThermo.IsOn() ) { 410 return true; 411 } 412 for (int i=0; i<NR_ZONES; i++) { 413 if (Zones[i].thermostat.IsOn() ) { 414 return true; 415 } 416 } 417 return false; // all Thermostats are open (no heating needed) 418} 419 420void printTimeStamp() { 421 #ifdef FAST_MODE 422 // 50 times faster; represent the time it would be in normal mode 423 unsigned long seconds = millis()/(unsigned long)20; 424 #else 425 // Normal operation 426 unsigned long seconds = millis()/(unsigned long)1000; 427 #endif 428 unsigned long minutes, hours, days; 429 minutes = seconds / 60L; 430 seconds %= 60L; 431 hours = minutes / 60L; 432 minutes %= 60L; 433 days = hours / 24L; 434 hours %= 24L; 435 char time[30]; 436 sprintf(time, "%02d:%02d:%02d:%02d", (int)days, (int)hours, (int)minutes, (int)seconds); 437 Serial.print(time); 438} 439 440 441
ProjectCV.ino
c_cpp
1/* 2 * Floor Unit heating controller for multiple Rooms/Zones v1.0 3 * 4 * Copyright: the GNU General Public License version 3 (GPL-3.0) by Eric Kreuwels, 2017 5 * Credits: Peter Kreuwels for defining all use-cases that needed to be considered 6 * 7 * Although this setup already runs for over a full year for my ground floor, I'm not Liable for any error in the code 8 * It can be used as a good basis for your own needs, and should be tested before using 9 * 10 * Highlights/Features: 11 * - You only need to configure the pinning and number of zones 12 * - A simple Arduino Uno can control up to 5 Floor Unit zones 13 * - With an Arduino Mega the number of Zones is nearly unlimmited 14 * - The provided Program controls: 15 * - the Floor unit pump 16 * - Aggregates all your zones as just one thermostat to the CV heater 17 * - Valves to open/close zones 18 * - Allows individual Heating per Zone; 19 * - Per zone a Thermostat to sense the request for heating 20 * - Per zone a Relay to control one or more valves to open/close the Floor unit groups of that zone 21 * - A room with multiple floor unit groups can be considered as one heating Zone (Wire the valves parallel to the Zone Relay) 22 * - This is not only more convenient, but saves energy as well as rooms don't become too warm anymore 23 * - Controls the Floor Unit Pump 24 * - It basically only runs the pump when needed for heating. This already saves you 100-200 Euro electricity per year, 25 * compared to run the same pump 24/7 (80 Watt is 2kW Hour a day = 0.50 Euro per day) 26 * - Activates the floor unit pump at least once every 36 hours, for 8 minutes if there wasn't any heating request (Summer) 27 * - Prevents to Run the pump without opening the valves first; Taking into account these valves need 3-5 minutes 28 * - Optionally you can control the remainder of your house (rooms without floor heating) as well 29 * - Here you typically will have thermostat knobs on your radiators; so only the rooms that are cold will heat up 30 * - Just add a thermostat in the room(s) you want to control. Wire these thermostats in parallel to the No_Zone input 31 * - Final notes: 32 * - Not all zones need to be controlled; only the zones that become either to warm or stay too cold with 33 * manual adjusted knobs on the floor unit 34 */ 35 36#include <avr/wdt.h> // for Watchdog 37 38// WARNING: FAST_MODE is for testing/evaluation/debug purposes (loop runs 50x faster) 39// Be carefull using FAST_MODE with a real floor unit pump as it can get damaged with closed valves 40// Valves need minimal 3 minutes to open. In FAST_MODE the program doesn't wait long enough before starting the pump 41 42// #define FAST_MODE // 50 times faster execution; consider to disconnect your real CV/Pump! 43 44// In Normal operation to loop runs 10 times per second; so 10 counts/second (600 represents ca 1 minute) 45#define VALVE_TIME 3000L // 5 minutes to open/close a valve (on the safe site; takes typically 3 to 5 minutes) 46#ifdef FAST_MODE 47#define PUMP_MAINTENANCE_TIME 108000L // For evaluation, activates Floor Unit pump maintenance run once per 4 minutes (time stamp 3 hours) 48#else 49#define PUMP_MAINTENANCE_TIME 1300000L // Activates Floor Unit pump maintenance run once per 36 hours. Needed to keep pump working 50#endif 51#define PUMP_ACTIVATION_TIME 5000L // Activates the pump for ca 8 minutes (10 seconds in test mode) 52#define COOLDOWN_TIME 18000L // When heating is done, continue water circulation for another 30 minutes (40 seconds in test mode) 53 // This enables further dissipation the heat into the floor (typically takes 15 to 30 minutes) 54 55#include "./Devices.h" // valves, pumps, thermostat classes (use the constants defines above) 56struct Zone { 57 String name; 58 Valve valve; 59 Thermostat thermostat; 60}; 61 62//////////////////////////////////////////////////// 63// CONFIGURATION BLOCK 64 65// Configure/reorder your pinning as you like (This my wiring on an Arduino Uno); 66// Note: pins 1 and 2 are still free to add an extra zone 67#define HEATER_PIN 4 // output to a Relay that is wired with the Thermostat input of your heating system 68#define FU_PUMP_PIN 5 // output to a Relay that switches the Floor Unit Pump 69#define LIVING_VALVE 7 // Zone 1: output to a Relay that controls the Valve(s) 70#define KITCHEN_VALVE 6 // Zone 2: output to a Relay that controls the Valve(s) 71#define DINING_VALVE 3 // Zone 3: output to a Relay that controls the Valve(s) 72 73#define LIVING_THERMO 8 // Zone 1; input wired to the thermostat in the living 74#define KITCHEN_THERMO 9 // Zone 2; input wired to the thermostat in the kitchen 75#define DINING_THERMO 11 // Zone 3; input wired to the thermostat in the dining 76#define NO_ZONE_THERMO 10 // Optionally: thermostats in rooms without floor heating 77 78#define HEATING_LED 12 // On when heating, Alternates during cooldown, is Off in idle mode 79#define INDICATION_LED 13 // Alternates the on board LED to indicate board runs; can be easily removed to free an extra IO pin!! 80 81// Configure the Floor Unit Zones/rooms. Each zone/room owns a name, valve and thermostat: 82#define NR_ZONES 3 83Zone Zones[NR_ZONES] = { {"Living Room", Valve(LIVING_VALVE, "Living Valve"), Thermostat(LIVING_THERMO, "Living Thermostat")}, 84 {"Kitchen Area", Valve(KITCHEN_VALVE,"Kitchen Valve"), Thermostat(KITCHEN_THERMO,"Kitchen Thermostat")}, 85 {"Dining Room", Valve(DINING_VALVE, "Dining Valve"), Thermostat(DINING_THERMO, "Dining Thermostat")}}; 86 87// END CONFIGURATION BLOCK 88////////////////////////////////////////////////// 89 90 91// Some fixed devices: 92LED iLED(INDICATION_LED, "Indicator LED"); // can be removed if you run out of IO's 93LED hLED(HEATING_LED, "Heating LED"); 94Manipulator CV(HEATER_PIN, "CV Heater"); 95Pump FUPump(FU_PUMP_PIN, "Floor Unit Pump"); 96Thermostat ZonelessThermo(NO_ZONE_THERMO, "Zoneless Thermostat"); // For the rest of the house, no related to the floor unit zone 97 98void printConfiguration() { 99 Serial.println("------ Board Configuration: ---------"); 100 iLED.Print(); 101 hLED.Print(); 102 CV.Print(); 103 FUPump.Print(); 104 ZonelessThermo.Print(); 105 for(int i=0; i<NR_ZONES; i++) { 106 Serial.print("Zone["); Serial.print(i+1); 107 Serial.print("]: "); Serial.println(Zones[i].name); 108 Serial.print(" - "); Zones[i].valve.Print(); 109 Serial.print(" - "); Zones[i].thermostat.Print(); 110 } 111 Serial.println("-------------------------------------"); 112} 113 114// state machine, with both transition and state handling actions 115class State 116{ 117 public: 118 enum states { 119 idle, 120 on, 121 cooldown 122 }; 123 124 private: 125 //vars 126 states _State; 127 unsigned long cooldownCount; 128 129 public: 130 //constructor 131 StateMAchine() { 132 _State = idle; 133 } 134 135 // Getter 136 states const& operator()() const { 137 return _State; 138 } 139 140 // Setter 141 void operator()(states const& newState) { 142 printTimeStamp(); 143 Serial.print(": State change from ["); 144 Print(); 145 _State = newState; 146 Serial.print("] to ["); 147 Print(); 148 Serial.println("]"); 149 // deal with transition actions to the new state 150 switch(_State) 151 { 152 case idle: 153 hLED.Off(); 154 CV.Off(); // stop heating 155 FUPump.Off(); 156 allValvesOff(); 157 break; 158 case on: 159 hLED.On(); 160 CV.On(); // start heating, but Floor unit pump has to wait till at least one zone is open 161 break; 162 case cooldown: 163 CV.Off(); // stop heating 164 allValvesOn(); // open all zones for cooldown; pump has to wait for this 165 break; 166 default: 167 Serial.println("WARNING Unhandled State transition"); 168 break; 169 } 170 } 171 172 // Do the state handling; to be called repatively by the loop() 173 void doProcessingActions() { 174 switch(_State) { 175 case on: 176 onProcessing(); // As long heating is requested open/close zones matching the heating requests 177 break; 178 case cooldown: 179 coolDownProcessing(); // take 30 minutes to dissipate remaing Heat into the floor 180 break; 181 case idle: 182 idleProcessing(); // operate pumps/valves once per day 183 break; 184 default: 185 Serial.println("ERROR Unhandled State"); 186 break; 187 } 188 } 189 190 void setCoolDownNeeded() { 191 cooldownCount = COOLDOWN_TIME; 192 } 193 bool whileCoolDownNeeded() { // down counts the time 194 if (cooldownCount > 0) { 195 cooldownCount--; 196 } 197 return checkCoolDownNeeded(); 198 } 199 bool checkCoolDownNeeded() { 200 return (cooldownCount> 0); 201 } 202 203 void Print() { 204 switch(_State) 205 { 206 case idle: 207 Serial.print("idle"); 208 break; 209 case on: 210 Serial.print("on"); 211 break; 212 case cooldown: 213 Serial.print("cooldown"); 214 break; 215 } 216 } 217}; 218 219// The global state machine 220State CVState; 221 222 223void setup() 224{ 225 // initializations 226 Serial.begin(115200); 227 printTimeStamp(); 228 Serial.print(": "); 229#ifdef FAST_MODE 230 Serial.println("CV Zone Controller started in TestMode!\ 231" 232 " - Board time runs ca 50 times faster\ 233" 234 " - Pump maintenance cycle runs ever 3 hours instead once per 36 hours"); 235#else 236 Serial.println("CV Zone Controller started. Time stamps (dd:hh:mm:ss)"); 237#endif 238 Serial.println(" - Time stamps format (dd:hh:mm:ss)"); 239 printConfiguration(); 240 wdt_enable(WDTO_1S); // Watchdog: reset board after one second, if no "pat the dog" received 241} 242 243void loop() 244{ 245 246#ifdef FAST_MODE 247 delay(2); // 50 times faster so minutes become roughly seconds for debugging purpose; so every count for cooldown or idle is 0.002 second 248#else 249 delay(100); // Normal operation: loops approx 10 times per second; so every count for cooldown or idle is 0.1 second 250#endif 251 252 // Use Indication LED to show board is alive 253 iLED.Alternate(); 254 255 // once per loop() the pump and valves need to opdate hteir administatrion 256 FUPump.Update(); 257 for (int i=0; i<NR_ZONES; i++) { 258 // Update valve administration for transition times to open/close 259 Zones[i].valve.Update(); 260 } 261 262 // Reset the WatchDog timer (pat the dog) 263 wdt_reset(); 264 265 // Do the state handling 266 CVState.doProcessingActions(); 267} 268 269//////////////////////////////////////////////////////////////////// 270// Processing Methods for each CV State 271///////////////////////////////////////// 272 273void onProcessing() { 274 if (ProcessThermostats()) { // returns true if at least one of the thermostats is on (switch closed) => stay in this state 275 if (FloorPumpingAllowed()) { 276 FUPump.On(); 277 } 278 else { 279 FUPump.Off(); 280 } 281 } 282 else if ( CVState.checkCoolDownNeeded() ) { // Continue in cooldown state to keep pump running for a while 283 CVState(State::cooldown); 284 } 285 else { // skip cooldown for floor unit, go back to idle 286 CVState(State::idle); 287 } 288} 289 290 291void coolDownProcessing() { 292 hLED.Alternate(); 293 if (HeatingRequested()) { // returns true when one of the thermostats is closed 294 CVState(State::on); 295 } 296 else { 297 if ( CVState.whileCoolDownNeeded() ) { 298 if (FloorPumpingAllowed()) { 299 FUPump.On(); 300 } else { 301 FUPump.Off(); 302 } 303 } 304 else { 305 CVState(State::idle); 306 } 307 } 308} 309 310void idleProcessing() 311{ 312 if (HeatingRequested()) { // returns true when one of the thermostats is closed 313 CVState(State::on); 314 } 315 else 316 { 317 // During idle period this check will activate the Floor Unit Pump for 8 minutes per 36 hours to keep them operatable 318 if ( FUPump.doMaintenanceRun()) { 319 if (FUPump.IsOff()) { 320 if ( allValvesOpen() == false ) { // start opening just once 321 printTimeStamp(); Serial.println(": Start daily cycle for Floor Unit Pump; open valves: "); 322 allValvesOn(); 323 } 324 if (FloorPumpingAllowed()) { 325 // this takes ca 5 minutes after activating the valves (6 seconds in test mode) 326 printTimeStamp(); Serial.println(": Start daily cycle for Floor Unit Pump; start pump "); 327 FUPump.On(); 328 } 329 330 } 331 } 332 else if (FUPump.IsOn()) { // no Maintenance needed. So stop pump if still running 333 printTimeStamp(); Serial.println(": Stop daily cycle for Floor Unit Pump; stop pump and close valves"); 334 FUPump.Off(); 335 allValvesOff(); 336 } 337 } 338} 339 340//////////////////////////////////////////////////////////////////// 341// Helper Methods used by the State handlers 342///////////////////////////////////////// 343 344void allValvesOff() { 345 for (int i=0; i<NR_ZONES; i++) { 346 Zones[i].valve.Off(); 347 } 348} 349 350void allValvesOn() { 351 for (int i=0; i<NR_ZONES; i++) { 352 Zones[i].valve.On(); 353 } 354} 355 356bool allValvesOpen() { 357 for (int i=0; i<NR_ZONES; i++) { 358 if ( Zones[i].valve.IsOff() ) { 359 return false; 360 } 361 } 362 return true; 363} 364 365 366bool FloorPumpingAllowed() 367{ 368 // returns true if at least one zone is open, taking Valve transition into account 369 for (int i=0; i<NR_ZONES; i++) { 370 if (Zones[i].valve.ValveIsOpen() ) { 371 return true; 372 } 373 } 374 return false; // all valves are closed 375} 376 377bool ProcessThermostats() // returns true when one of the thermostats is closed 378{ // (De-)Activates Floor zones 379 bool heating=false; 380 bool requested[NR_ZONES]; 381 382 if ( ZonelessThermo.IsOn() ) { 383 heating = true; 384 } 385 for (int i=0; i<NR_ZONES; i++) { 386 // record heating requests only once to avoid race conditions due changes in between 387 requested[i] = Zones[i].thermostat.IsOn(); 388 if ( requested[i] ) { 389 heating = true; 390 CVState.setCoolDownNeeded(); // remember if there was a request for heating a floor unit zone 391 } 392 } 393 for (int i=0; i<NR_ZONES; i++) { 394 if ( requested[i] ) { 395 // Selectively open valves for zones that request heating 396 Zones[i].valve.On(); 397 } 398 else if (heating) { 399 // Selectively close valves for zones that don't require heating anymore 400 // Only close them if heating is still required because in cooldown all zones need to be open 401 Zones[i].valve.Off(); 402 } 403 } 404 return heating; 405} 406 407bool HeatingRequested() 408{ 409 if ( ZonelessThermo.IsOn() ) { 410 return true; 411 } 412 for (int i=0; i<NR_ZONES; i++) { 413 if (Zones[i].thermostat.IsOn() ) { 414 return true; 415 } 416 } 417 return false; // all Thermostats are open (no heating needed) 418} 419 420void printTimeStamp() { 421 #ifdef FAST_MODE 422 // 50 times faster; represent the time it would be in normal mode 423 unsigned long seconds = millis()/(unsigned long)20; 424 #else 425 // Normal operation 426 unsigned long seconds = millis()/(unsigned long)1000; 427 #endif 428 unsigned long minutes, hours, days; 429 minutes = seconds / 60L; 430 seconds %= 60L; 431 hours = minutes / 60L; 432 minutes %= 60L; 433 days = hours / 24L; 434 hours %= 24L; 435 char time[30]; 436 sprintf(time, "%02d:%02d:%02d:%02d", (int)days, (int)hours, (int)minutes, (int)seconds); 437 Serial.print(time); 438} 439 440 441
Devices.h
c_cpp
1// Helper classes for IO devices 2extern void printTimeStamp(); // defined 3 in main ino file 4 5// IODevice: base class for all IO devices; needs specialization 6class 7 IODevice { 8 //vars 9 protected: 10 bool _IsOn; 11 int _Pin; 12 String 13 _Name; 14 15 //constructor 16 public: 17 IODevice(int pin, String name) 18 { 19 _IsOn = false; 20 _Pin = pin; 21 _Name= name; 22 } 23 //methods 24 25 virtual bool IsOn() = 0; // abstract 26 virtual bool IsOff() { // default 27 for all 28 return !IsOn(); 29 } 30 31 void DebugPrint() { 32 printTimeStamp(); 33 34 Serial.print(": "); 35 Print(); 36 } 37 void Print() { 38 Serial.print(_Name); 39 40 Serial.print(" on pin("); 41 Serial.print(_Pin); 42 if (_IsOn) 43 44 Serial.println(") = On"); 45 else 46 Serial.println(") = Off"); 47 48 } 49}; 50 51// Thermostat: reads an digital input adding some dender surpression 52 53class Thermostat : public IODevice 54{ 55 //vars 56 private: 57 int 58 _Counter; // used to prevent reading intermitted switching (dender) 59 60 //constructor 61 62 public: 63 Thermostat(int pin, String name) : IODevice(pin, name) { 64 _Counter 65 = 0; 66 pinMode(_Pin, INPUT_PULLUP); 67 } 68 69 //methods 70 virtual 71 bool IsOn() { 72 if (digitalRead(_Pin) == HIGH && _IsOn == true) // open 73 contact while on 74 { 75 if( _Counter++ > 5) // only act after 5 times 76 the same read out 77 { 78 _IsOn = false; 79 DebugPrint(); 80 81 _Counter = 0; 82 } 83 } 84 else if (digitalRead(_Pin) == 85 LOW && _IsOn == false) // closed contact while off 86 { 87 if( _Counter++ 88 > 5) // only act after 5 times the same read out 89 { 90 _IsOn = 91 true; 92 DebugPrint(); 93 _Counter = 0; 94 } 95 } 96 97 else 98 { 99 _Counter = 0; 100 } 101 return _IsOn; 102 } 103}; 104 105// 106 Manipulator: the most basic working device on an digital output 107class Manipulator 108 : public IODevice 109{ 110 //vars 111 private: 112 113 //constructor 114 public: 115 116 Manipulator(int pin, String name) : IODevice(pin, name) { 117 pinMode(_Pin, 118 OUTPUT); 119 digitalWrite(_Pin, HIGH); 120 } 121 //methods 122 void On() 123 { 124 if (_IsOn == false) 125 { 126 _IsOn = true; 127 digitalWrite(_Pin, 128 LOW); 129 onSwitch(); 130 } 131 } 132 133 void Off() { 134 if (_IsOn 135 == true) 136 { 137 _IsOn = false; 138 digitalWrite(_Pin, HIGH); 139 140 onSwitch(); 141 } 142 } 143 144 virtual void onSwitch() { // trigger 145 for child claases; change in on/off state 146 DebugPrint(); 147 } 148 149 150 virtual bool IsOn() { 151 return _IsOn; 152 } 153}; 154 155// Valve: controlles 156 themostatic valves on a digital output. 157// These valves react slowly (3-5 minutes) 158 so this class adds this transition awareness 159// loop() must call Update() to 160 keep track if the valve is fully open or closed 161class Valve : public Manipulator 162{ 163 164 private: 165 long transitionCount; 166 167 //constructor 168 public: 169 170 Valve(int pin, String name) : Manipulator(pin, name) { 171 transitionCount 172 = 0; 173 } 174 175 bool ValveIsOpen() { 176 return (IsOn() && (transitionCount>=VALVE_TIME)); 177 // at least 5 minutes in on state 178 } 179 180 // Execute once per pass in 181 the sketch loop() !!! 182 void Update() { 183 if (IsOn()) { 184 if 185 (transitionCount < VALVE_TIME) 186 transitionCount++; 187 } 188 else 189 { 190 if (transitionCount > 0) 191 transitionCount--; 192 } 193 194 } 195}; 196 197 198// Pump: a pump need to be activated several times a week to 199 keep them going. 200// loop() must call Update() to keep track when a maintenance 201 activation is needed 202class Pump : public Manipulator 203{ 204 // valves react 205 slowly (3-5 minutes) so this class adds this transition awareness 206 private: 207 208 long counter; 209 bool doMaintenance; 210 211 //constructor 212 public: 213 214 Pump(int pin, String name) : Manipulator(pin, name) { 215 counter = 0; 216 217 doMaintenance = false; 218 } 219 220 bool doMaintenanceRun() { 221 return 222 doMaintenance; 223 } 224 225 virtual void onSwitch() { // change in on/off state 226 227 Manipulator::onSwitch(); 228 counter = 0; 229 } 230 231 // run this 232 method every pass in loop() 233 void Update() { 234 if (IsOn()) { 235 if 236 (counter < PUMP_ACTIVATION_TIME) { 237 counter++; 238 } else if (doMaintenance) 239 { 240 printTimeStamp(); 241 Serial.println(": Pump Maintenance cleared"); 242 243 doMaintenance = false; 244 } 245 } 246 else { 247 if (counter 248 < PUMP_MAINTENANCE_TIME) { 249 counter++; 250 } else if (doMaintenance==false) 251 { 252 printTimeStamp(); 253 Serial.println(": Pump Maintenance needed"); 254 255 doMaintenance = true; 256 } 257 } 258 } 259}; 260 261// LED; besides 262 on/off it offers a method to alternate the LED (1Hz) 263// just call Alternate() 264 from the loop() to activate alternation 265class LED : public Manipulator 266{ 267 268 private: 269 long counter; 270 271 //constructor 272 public: 273 LED(int 274 pin, String name) : Manipulator(pin, name) { 275 counter = 0; 276 } 277 278 279 virtual void onSwitch() { // change in on/off state 280 // surpress printing 281 debug output for LEDs 282 } 283 284 void Alternate() { 285 #ifdef FAST_MODE 286 287 if (counter++ > 250) 288 #else 289 if (counter++ > 5) 290 #endif 291 292 { // toggle LED 293 counter=0; 294 if (IsOn()) 295 Off(); 296 297 else 298 On(); 299 } 300 } 301}; 302 303 304
Downloadable files
The device mounted
Inspirational
The device mounted
Multiple Controllers
Example of wiring multiple 'cascaded' controllers. One controller per Floor Unit
Multiple Controllers
Example of the Logging
Some 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.
Example of the Logging
Schematics
Detailed wiring of periferals (Pump, Valves, Thermostats, LED's)
Schematics
Multiple Controllers
Example of wiring multiple 'cascaded' controllers. One controller per Floor Unit
Multiple Controllers
Example of the Logging
Some 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.
Example of the Logging
The device mounted
Inspirational
The device mounted
Comments
Only logged in users can leave comments