Geavanceerd Multi-Zone Verwarmingsregelsysteem voor Slimme Huizen
Onderdelen en Benodigdheden
| Onderdeel | Aantal | Opmerkingen |
|---|---|---|
| Arduino UNO | 1 | Gebruik een Mega als je meer dan 5 zones moet besturen (inclusief de extra zone voor de rest van je huis zonder vloerverwarming) |
| Keyes 8-kanaals 5V relaybord | 1 | Je hebt een bord met 4 relais nodig om 2 vloerzones te besturen (1 pomp, 1 CV, 2 kleppen). Voor elke extra vloerzone is één extra relais nodig. |
| Honeywell MT8-230-NC Kleine Lineaire Thermoelectrische Aandrijving (230V AC) | 1 | Dit is slechts één van de veel mogelijke aandrijvingen; je hebt 1 aandrijving (klep) per vloereenheidgroep nodig. |
Over dit Project
Ik bouwde dit apparaat omdat mijn keuken of te warm of te koud was met één thermostaat in de woonkamer. Commerciële multi-zone verwarmingscontrollers (zoals EvoHome) zijn zeer duur. Dit programma vangt de intelligentie van die dure systemen op, gehost op een simpele Arduino Uno. Dit loste mijn probleem volledig op.
Hoofdkenmerken
- Je hoeft alleen de pintoewijzing en het aantal zones te configureren
- Een simpele Arduino Uno kan tot 5 vloereenheid-zones besturen
- Met een Arduino Mega is het aantal zones bijna onbeperkt
Het Programma Bestuurt
- De vloereenheidspomp
- Aggregeert al je zones als één enkele thermostaat naar de CV-ketel
- Kleppen om zones te openen/sluiten
- Een watchdog-timer voor robuuste werking
Individuele Verwarming per Zone
- Per zone een thermostaat om de verwarmingsvraag te detecteren
- Per zone een relais om één of meer kleppen te besturen die de vloereenheidgroepen van die zone openen/sluiten
- Een ruimte met meerdere vloereenheidgroepen kan als één verwarmingszone worden beschouwd (kleppen parallel aan de zonerelais aansluiten)
- Dit is niet alleen comfortabeler, maar bespaart ook energie omdat kamers niet te warm worden
Besturing van de Vloereenheidspomp
- De pomp draait alleen wanneer verwarming nodig is. Dit bespaart al 100-200 euro stroom per jaar (vergelijk met 24/7 draaien: 80 Watt = 2 kWh/dag = €0,50/dag)
- Activeert de vloereenheidspomp ten minste één keer per 36 uur, gedurende 8 minuten, als er geen verwarmingsvraag was (zomer)
- Voorkomt dat de pomp draait zonder de kleppen eerst te openen; rekening houdend met de 3-5 minuten openingsduur van de kleppen
Optioneel: Besturing van de Rest van het Huis
- Hier heb je meestal thermostaatknoppen op je radiators; alleen de koude kamers verwarmen dus
- Voeg gewoon een thermostaat toe in de kamer(s) die je wilt besturen. Sluit deze thermostaten parallel aan op de No_Zone-ingang
Eindnotities
Niet alle zones hoeven bestuurd te worden; alleen de zones die of te warm worden of te koud blijven (anders de handmatig instelbare knoppen op de vloereenheid gebruiken).
Ik heb bewust gekozen om het apparaat niet met internet te verbinden:
- Dat zou het risico op storingen vergroten (het moet robuust zijn)
- Je kunt slimme thermostaten gebruiken om je huis te besturen. Deze controller biedt geen extra waarde voor externe bediening
Code
De code bestaat uit twee bestanden: ProjectCV.ino en Devices.h. Hieronder volgt de volledige code met uitgebreide commentaren.
ProjectCV.ino
/*
* Vloereenheid verwarmingscontroller voor meerdere Kamers/Zones v1.0
* Copyright: GNU General Public License v3 (GPL-3.0) door Eric Kreuwels, 2017
* Credits: Peter Kreuwels voor het definiëren van alle use-cases
*
* Hoewel deze opstelling al meer dan een jaar draait voor mijn begane grond,
* ben ik niet aansprakelijk voor fouten in de code. Het kan als goede basis
* dienen voor je eigen behoeften en moet getest worden voordat je het gebruikt.
*/
#include <avr/wdt.h> // voor Watchdog
// WAARSCHUWING: FAST_MODE is voor testen/evaluatie/debug (lus draait 50x sneller)
// Wees voorzichtig met FAST_MODE bij een echte vloereenheidspomp - die kan beschadigen
// met gesloten kleppen. Kleppen hebben min. 3 minuten nodig om te openen.
// In FAST_MODE wacht het programma niet lang genoeg voordat de pomp start.
// #define FAST_MODE
// Normale operatie: lus draait ~10x per seconde; dus 10 tellingen/seconde (600 ≈ 1 minuut)
#define VALVE_TIME 3000L // 5 minuten om klep te openen/sluiten (veilige marge; typisch 3-5 min)
#ifdef FAST_MODE
#define PUMP_MAINTENANCE_TIME 108000L // Test: onderhoudslus elke 4 minuten (tijdstempel 3 uur)
#else
#define PUMP_MAINTENANCE_TIME 1300000L // Normaal: onderhoudslus elke 36 uur
#endif
#define PUMP_ACTIVATION_TIME 5000L // Pomp actief voor ~8 minuten (10 sec in testmodus)
#define COOLDOWN_TIME 18000L // Na verwarming: nog 30 min circuleren (40 sec test)
#include "./Devices.h" // kleppen, pompen, thermostaten klassen
struct Zone {
String name;
Valve valve;
Thermostat thermostat;
};
////////////////////////////////////////////////////
// CONFIGURATIE BLOK
// Configureer/herordne je pintoewijzing zoals je wilt (dit is mijn bedrading op Arduino Uno);
// Let op: pinnen 1 en 2 zijn nog vrij voor een extra zone
#define HEATER_PIN 4 // uitgang naar relais, aangesloten op thermostaatingang CV-ketel
#define FU_PUMP_PIN 5 // uitgang naar relais dat vloereenheidspomp schakelt
#define LIVING_VALVE 7 // Zone 1: uitgang naar relais voor klep(p-en)
#define KITCHEN_VALVE 6 // Zone 2
#define DINING_VALVE 3 // Zone 3
#define LIVING_THERMO 8 // Zone 1: ingang aangesloten op thermostaat in woonkamer
#define KITCHEN_THERMO 9 // Zone 2: keuken
#define DINING_THERMO 11 // Zone 3: eetkamer
#define NO_ZONE_THERMO 10 // Optioneel: thermostaten in kamers zonder vloerverwarming
#define HEATING_LED 12 // Aan bij verwarming, knippert tijdens cooldown, uit in idle
#define INDICATION_LED 13 // Knippert om aan te tonen dat bord draait; kan verwijderd worden voor extra IO-pin
// Configureer de vloereenheid zones/kamers. Elke zone bezit een naam, klep en thermostaat:
#define NR_ZONES 3
Zone Zones[NR_ZONES] = {
{"Woonkamer", Valve(LIVING_VALVE, "Woonkamer Klep"), Thermostat(LIVING_THERMO, "Woonkamer Thermostaat")},
{"Keuken", Valve(KITCHEN_VALVE, "Keuken Klep"), Thermostat(KITCHEN_THERMO, "Keuken Thermostaat")},
{"Eetkamer", Valve(DINING_VALVE, "Eetkamer Klep"), Thermostat(DINING_THERMO, "Eetkamer Thermostaat")}
};
// EINDE CONFIGURATIE BLOK
//////////////////////////////////////////////////
// Vaste apparaten:
LED iLED(INDICATION_LED, "Indicator LED"); // kan verwijderd worden bij IO-tekort
LED hLED(HEATING_LED, "Verwarming LED");
Manipulator CV(HEATER_PIN, "CV Ketel");
Pump FUPump(FU_PUMP_PIN, "Vloereenheidspomp");
Thermostat ZonelessThermo(NO_ZONE_THERMO, "Zoneloze Thermostaat"); // Voor rest van huis, niet gerelateerd aan vloereenheid zones
void printConfiguration() {
Serial.println("------ Bord Configuratie: ---------");
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 met zowel transitie- als state-handling acties
class State {
public:
enum states { idle, on, cooldown };
private:
states _State;
unsigned long cooldownCount;
public:
State() { _State = idle; } // constructor
const states& operator()() const { return _State; } // Getter
// Setter met transitie-acties
void operator()(states const& newState) {
printTimeStamp();
Serial.print(": State verandering van ["); Print();
_State = newState;
Serial.print("] naar ["); Print(); Serial.println("]");
switch(_State) {
case idle:
hLED.Off(); CV.Off(); FUPump.Off(); allValvesOff(); break;
case on:
hLED.On(); CV.On(); // verwarming start, maar pomp wacht tot min. 1 zone open is
break;
case cooldown:
CV.Off(); allValvesOn(); // alle zones open voor cooldown; pomp wacht hierop
break;
default: Serial.println("WAARSCHUWING Onverwerkte State transitie"); break;
}
}
// State handling; aanroepen herhaaldelijk vanuit loop()
void doProcessingActions() {
switch(_State) {
case on: onProcessing(); break;
case cooldown: coolDownProcessing(); break;
case idle: idleProcessing(); break;
default: Serial.println("FOUT Onverwerkte State"); break;
}
}
void setCoolDownNeeded() { cooldownCount = COOLDOWN_TIME; }
bool whileCoolDownNeeded() { 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;
}
}
};
State CVState; // globale state machine
void setup() {
Serial.begin(115200);
printTimeStamp(); Serial.print(": ");
#ifdef FAST_MODE
Serial.println("CV Zone Controller gestart in TestModus!\\n - Bordtijd loopt ~50x sneller\\n - Pomp onderhoudscyclus elke 3 uur ipv 1x per 36 uur");
#else
Serial.println("CV Zone Controller gestart. Tijdstempels (dd:uu:mm:ss)");
#endif
Serial.println(" - Tijdstempel formaat (dd:uu:mm:ss)");
printConfiguration();
wdt_enable(WDTO_1S); // Watchdog: reset na 1 sec als geen "pat the dog"
}
void loop() {
#ifdef FAST_MODE
delay(2); // 50x sneller voor debug
#else
delay(100); // Normaal: ~10x/sec, elke telling = 0.1 sec
#endif
iLED.Alternate(); // toon dat bord leeft
// Update pomp en kleppen administratie
FUPump.Update();
for (int i=0; i<NR_ZONES; i++) Zones[i].valve.Update();
wdt_reset(); // Watchdog resetten
CVState.doProcessingActions();
}
//////////////////////////////////////////////////////////////////
// Verwerkingsmethodes per CV State
/////////////////////////////////////////////
void onProcessing() {
if (ProcessThermostats()) { // true als min. 1 thermostaat aanstaat (contact dicht) => blijf in deze state
if (FloorPumpingAllowed()) FUPump.On(); else FUPump.Off();
} else if (CVState.checkCoolDownNeeded()) {
CVState(State::cooldown); // doorgaan in cooldown om pomp te laten draaien
} else {
CVState(State::idle); // cooldown overslaan, terug naar idle
}
}
void coolDownProcessing() {
hLED.Alternate();
if (HeatingRequested()) { // true als een thermostaat contact dicht is
CVState(State::on);
} else {
if (CVState.whileCoolDownNeeded()) {
if (FloorPumpingAllowed()) FUPump.On(); else FUPump.Off();
} else {
CVState(State::idle);
}
}
}
void idleProcessing() {
if (HeatingRequested()) {
CVState(State::on);
} else {
// Tijdens idle: activeer vloereenheidspomp 8 min per 36 uur om bruikbaar te houden
if (FUPump.doMaintenanceRun()) {
if (FUPump.IsOff()) {
if (!allValvesOpen()) { // kleppen openen één keer
printTimeStamp(); Serial.println(": Start dagelijkse cyclus vloereenheidspomp; open kleppen: ");
allValvesOn();
}
if (FloorPumpingAllowed()) { // na ~5 min (6 sec test) pomp starten
printTimeStamp(); Serial.println(": Start dagelijkse cyclus vloereenheidspomp; start pomp");
FUPump.On();
}
}
} else if (FUPump.IsOn()) { // geen onderhoud nodig, stop pomp als die draait
printTimeStamp(); Serial.println(": Stop dagelijkse cyclus vloereenheidspomp; stop pomp en sluit kleppen");
FUPump.Off(); allValvesOff();
}
}
}
//////////////////////////////////////////////////////////////////
// Hulpmethodes voor 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() {
for (int i=0; i<NR_ZONES; i++) if (Zones[i].valve.ValveIsOpen()) return true;
return false; // alle kleppen dicht
}
bool ProcessThermostats() {
bool heating = false;
bool requested[NR_ZONES];
if (ZonelessThermo.IsOn()) heating = true;
for (int i=0; i<NR_ZONES; i++) {
requested[i] = Zones[i].thermostat.IsOn();
if (requested[i]) {
heating = true;
CVState.setCoolDownNeeded(); // onthouden dat er vraag was voor vloereenheid zone
}
}
for (int i=0; i<NR_ZONES; i++) {
if (requested[i]) {
Zones[i].valve.On(); // selectief openen voor zones die verwarming vragen
} else if (heating) {
Zones[i].valve.Off(); // selectief sluiten voor zones die geen verwarming meer vragen
}
}
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; // alle thermostaten open (geen verwarming nodig)
}
void printTimeStamp() {
#ifdef FAST_MODE
unsigned long seconds = millis() / 20UL; // 50x sneller
#else
unsigned long seconds = millis() / 1000UL; // Normaal
#endif
unsigned long minutes = seconds / 60; seconds %= 60;
unsigned long hours = minutes / 60; minutes %= 60;
unsigned long days = hours / 24; hours %= 24;
char time[30];
sprintf(time, "%02d:%02d:%02d:%02d", (int)days, (int)hours, (int)minutes, (int)seconds);
Serial.print(time);
}
Devices.h
// Hulpmiddel klassen voor IO apparaten
extern void printTimeStamp(); // gedefinieerd in hoofd ino bestand
// IODevice: basisklasse voor alle IO apparaten; vereist specialisatie
class IODevice {
protected:
bool _IsOn; int _Pin; String _Name;
public:
IODevice(int pin, String name) { _IsOn = false; _Pin = pin; _Name = name; }
virtual bool IsOn() = 0; // abstract
virtual bool IsOff() { return !IsOn(); }
void DebugPrint() { printTimeStamp(); Serial.print(": "); Print(); }
void Print() {
Serial.print(_Name); Serial.print(" op pin("); Serial.print(_Pin);
if (_IsOn) Serial.println(") = Aan"); else Serial.println(") = Uit");
}
};
// Thermostat: leest digitale ingang met dender onderdrukking
class Thermostat : public IODevice {
private: int _Counter;
public:
Thermostat(int pin, String name) : IODevice(pin, name) { _Counter = 0; pinMode(_Pin, INPUT_PULLUP); }
virtual bool IsOn() {
if (digitalRead(_Pin) == HIGH && _IsOn) { // open contact terwijl aan
if (_Counter++ > 5) { _IsOn = false; DebugPrint(); _Counter = 0; }
} else if (digitalRead(_Pin) == LOW && !_IsOn) { // gesloten contact terwijl uit
if (_Counter++ > 5) { _IsOn = true; DebugPrint(); _Counter = 0; }
} else _Counter = 0;
return _IsOn;
}
};
// Manipulator: basaal werkend apparaat op digitale uitgang
class Manipulator : public IODevice {
public:
Manipulator(int pin, String name) : IODevice(pin, name) { pinMode(_Pin, OUTPUT); digitalWrite(_Pin, HIGH); }
void On() { if (!_IsOn) { _IsOn = true; digitalWrite(_Pin, LOW); onSwitch(); } }
void Off() { if (_IsOn) { _IsOn = false; digitalWrite(_Pin, HIGH); onSwitch(); } }
virtual void onSwitch() { DebugPrint(); } // trigger voor kindklassen
virtual bool IsOn() { return _IsOn; }
};
// Klep: bestuurt thermostatische kleppen op digitale uitgang
// Deze kleppen reageren langzaam (3-5 min) dus deze klasse voegt transitiebewustzijn toe
// loop() MOET Update() aanroepen om bij te houden of klep volopen/volgesloten is
class Valve : public Manipulator {
private: long transitionCount;
public:
Valve(int pin, String name) : Manipulator(pin, name) { transitionCount = 0; }
bool ValveIsOpen() { return (IsOn() && (transitionCount >= VALVE_TIME)); } // min 5 min in aan-toestand
void Update() {
if (IsOn()) { if (transitionCount < VALVE_TIME) transitionCount++; }
else { if (transitionCount > 0) transitionCount--; }
}
};
// Pomp: pomp moet meerdere keren per week geactiveerd worden om werkend te blijven
// loop() MOET Update() aanroepen om bij te houden wanneer onderhoudsactivering nodig is
class Pump : public Manipulator {
private: long counter; bool doMaintenance;
public:
Pump(int pin, String name) : Manipulator(pin, name) { counter = 0; doMaintenance = false; }
bool doMaintenanceRun() { return doMaintenance; }
virtual void onSwitch() { Manipulator::onSwitch(); counter = 0; }
void Update() {
if (IsOn()) {
if (counter < PUMP_ACTIVATION_TIME) counter++;
else if (doMaintenance) { printTimeStamp(); Serial.println(": Pomp onderhoud gewist"); doMaintenance = false; }
} else {
if (counter < PUMP_MAINTENANCE_TIME) counter++;
else if (!doMaintenance) { printTimeStamp(); Serial.println(": Pomp onderhoud nodig"); doMaintenance = true; }
}
}
};
// LED; naast aan/uit biedt methode om te alterneren (1Hz)
// roep Alternate() vanuit loop() om alternatie te activeren
class LED : public Manipulator {
private: long counter;
public:
LED(int pin, String name) : Manipulator(pin, name) { counter = 0; }
virtual void onSwitch() { /* debug output onderdrukken voor LEDs */ }
void Alternate() {
#ifdef FAST_MODE
if (counter++ > 250)
#else
if (counter++ > 5)
#endif
{ counter = 0; if (IsOn()) Off(); else On(); }
}
};
Schema's
Gedetailleerde bedrading van peripheralapparaten (Pomp, Kleppen, Thermostaten, LEDs)

Voorbeeld van bedrading van meerdere 'gecasandeerde' controllers. Eén controller per vloereenheid

Enkele echte logging van de Serial Monitor om de functionaliteit te begrijpen. De tijdstempels tonen bv. een vertraging van 5 minuten tussen het openen van kleppen en het daadwerkelijk starten van de vloereenheidspomp.
Productieproces
- Zonneverwarmingssystemen: Van Passief tot Actieve Oplossingen voor Duurzame Huisverwarming
- Uw centrale verwarmingsketel monitoren met een Raspberry Pi – Hardware, software en installatiegids
- Web-gestuurd DMX-verlichtingssysteem – Arduino Master Controller
- Geautomatiseerde Gaming Assistant: Slimme taak-automatisering voor gamers
- DIY Arduino USB Gaming Controller – Bouw Je Eigen High-Performance Gamepad
- Unopad: Naadloze Arduino MIDI-controller voor Ableton Live
- Arduino-gebaseerde Vacuum Fluorescent Display (VFD) Controller Kit
- Centrale Verwarming Slim Maken met Arduino en OpenTherm
- Alexa-ingeschakelde Arduino Thuiscontroller: Slimme Automatisering Made Simple
- PWM Vermogenscontroller: Bouw een Pulsbreedte Gemoduleerde Lampstuur
- Essentiële Gids Voor Het Onderhouden Van Uw Vloerscrubber Voor Topprestaties