EMUSNOC¡

hardware transitie

Lokale klimaatregeling

23-11-2025 Harmen Zijp

De WAR is gevestigd in een complex van gebouwen uit 1916, 1936 en 1980, waarin uiteenlopende constructietechnieken zijn gebruikt, en per ruimte verschillende isolatiemaatregelen zijn genomen. Bovendien worden de ruimtes zeer divers gebruikt: o.m. opslag zonder comfort-eisen, werkplaatsen waar mensen bewegen, kantoren waar mensen stilzitten, woningen, groepsruimtes.

Dit stuk beschrijft een systeem om in het gebouw van De WAR per ruimte het binnenklimaat te kunnen meten en regelen.

1. Binnenklimaat

Het klimaat in een gebouw wordt voornamelijk bepaald door luchtvochtigheid, luchtverversing en temperatuur.

Luchtvochtigheid

Een van de belangrijkste factoren voor het in goede staat houden van een gebouw is de vochthuishouding. Vocht dat voor langere tijd ophoopt zorgt ervoor dat hout gaat rotten of schimmelen en dat ijzer gaat roesten. Wanneer dat gebeurt op een plek waar je niet bij kunt kan sluipend een groot probleem ontstaan. Denk daarbij aan (onder)vloeren, kozijnen of dakbeschot. Vocht gaat condenseren wanneer de relatieve luchtvochtigheid 100% nadert.

Schimmel in een houten ondervloer onder tapijttegels als gevolg van een langdurige lekkage

Los van schade aan het gebouw zorgt een (te sterk) wisselend binnenklimaat voor klemmende deuren, opbollende vloeren en daarmee onnodig ongemak en slijtage.

Voor mensen bepaalt de relatieve luchtvochtigheid een deel van het comfort dat ze ervaren. Te droge lucht (< 30% RH) kan zorgen voor een droge huid en irritatie aan ogen, neus en keel. Te vochtige lucht (> 70% RH) kan zorgen voor benauwdheid en irritatie aan de luchtwegen. Een te hoog vochtgehalte kan er tenslotte voor zorgen dat voor mensen ongezonde schimmels zich kunnen verspreiden. (bron: RIVM)

Luchtverversing

Luchtverversing is een manier om balans te brengen tussen de luchtvochtigheid binnenshuis en buiten. Maar minstens zo belangrijk is de concentratie koolstofdioxide (CO₂) die ermee kan worden gereguleerd. Een te hoge concentratie CO₂ (> 1200 ppm) kan leiden tot vermoeidheid, hoofdpijn en concentratieverlies.

Er blijkt bovendien een sterke correlatie te zijn tussen CO₂-concentratie en de overdraagbaarheid door de lucht van infectieziekten in een ruimte waar zich meer mensen bevinden. Bij voorkeur blijft het CO₂-gehalte onder 800 ppm.

Temperatuur

Tenslotte maakt de temperatuur veel uit voor het comfort dat mensen ervaren in een ruimte. Over wat een aangename of gezonde temperatuur is om in te verblijven lopen de inzichten uiteen. Het verschilt bovendien tussen zomer en winter en van land tot land, met een bandbreedte tussen 15⁰C en 30⁰C.

De temperatuur die mensen ervaren is afhankelijk van de volgende factoren (bron: Wikipedia Thermal comfort):

Samengevat is de temperatuur die een ruimte voor mensen moet hebben afhankelijk van hoeveel ze bewegen en wat voor kleding ze dragen. Warmte kan worden toegevoegd door de lucht te verwarmen (met airco, blower of CV) of door warmtestraling (met een infraroodpaneel). De temperatuur verlagen kan alleen door de lucht af te koelen (met airco).

Voor het gebouw zelf maakt temperatuur niet veel uit, behalve wanneer het gaat vriezen. Doordat water uitzet als het bevriest kan vorst schade aanrichten, bijvoorbeeld door leidingen die springen of opgehoopt water in kieren dat ervoor zorgt dat die kieren groter worden.

2. Doelen

Binnen De WAR worden de verschillende ruimtes zeer uiteenlopend gebruikt. Er zijn grote verschillen tussen de soorten ruimte (groot/klein), het aantal mensen en de activiteit die ze er ondernemen. Daardoor lijkt het niet nodig, en zelfs verkwistend, om één klimaat voor het gehele gebouw te realiseren.

Een doel van dit project is om per ruimte te kunnen meten aan het binnenklimaat en dit op een zo duurzaam mogelijke manier te kunnen verbeteren als dat nodig is.

De relatieve vochtigheid is afhankelijk van luchttemperatuur en ventilatie, en is van invloed op de gevoelstemperatuur, die weer verder kan worden beïnvloed met stralingswarmte (infraroodpanelen) en convectiewarmte (airco of blowers).

Ventileren en verwarmen kosten een hoeveelheid energie die ook weer afhankelijk is van de grootte van de ruimte.

Bij verwarming is een belangrijk verschil dat met luchtverwarming het hele volume van een ruimte moet worden verwarmd, en met stralingswarmte alleen daar hoeft te worden verwarmd waar zich mensen bevinden.

Bij luchtverwarming is een warmtepomp (zoals ook door een airco wordt gebruikt) veel energiezuiniger dan bijvoorbeeld een CV-installatie of blower.

In alle gevallen heeft elektrische verwarming (infraroodpaneel, airco of blower) de voorkeur boven een gasgestookte CV-installatie:

Voor grote ruimtes met weinig mensen is het dus energiezuiniger (en goedkoper) om stralingswarmte te gebruiken. In kleine ruimtes met veel mensen maakt het niet uit hoe je verwarmt.

Als luchtverwarming wordt gebruikt is het beter om dat met een airco (of andere warmtepomp) te doen dan met een blower.

3. Meten

Vocht in de lucht

Lucht bevat waterdamp. De hoeveelheid waterdamp die lucht kan bevatten is afhankelijk van de temperatuur.

Hoe warmer de lucht, hoe meer water er per m³ in opgenomen kan worden. Bron: Joost de Vree

Concreet: bij 10⁰C kan lucht 9,4 gram water per m³ bevatten, bij 20⁰C kan lucht 17,3 gram water per m³ bevatten. De lucht heet dan verzadigd te zijn.

Wordt er meer vocht in de lucht gebracht dan zal deze condenseren. Gebeurt dit buiten dan ontstaan mist en dauw, binnen wordt een ruimte klam: oppervlakken worden nat, textiel en andere vezelige of poreuze (capillair actieve) stoffen nemen vocht op.

Relatief en absoluut

Een belangrijk onderscheid is dat tussen absolute en relatieve luchtvochtigheid. De absolute luchtvochtigheid geeft aan hoeveel gram water er per m³ in de lucht zit. De relatieve luchtvochtigheid geeft aan voor hoeveel procent de lucht verzadigd is. Dit hangt af van de temperatuur. Hoe hoger de temperatuur hoe lager de relatieve luchtvochtigheid.

In het bovenstaande voorbeeld: verzadigde lucht bij 10⁰C heeft een absolute vochtigheid van 9,4 gram water per m³ en een relatieve vochtigheid van 100%. Wanneer we deze opwarmen tot 20⁰C is de absolute vochtigheid nog steeds 9,4 gram water per m³ maar is de relatieve vochtigheid nog maar 54% (9,4/17,3).

Dauwpunt

Ook in warme lucht kan water condenseren, op een koud oppervlak wanneer de relatieve luchtvochtigheid hoog is. Bekend voorbeeld is brillenglas dat beslaat wanneer de bril van buiten naar een warmere vochtige binnenruimte wordt genomen. Een ander voorbeeld zijn de beslagen ramen in keuken of douche wanneer daar veel vocht in de lucht is gebracht en de ramen de koudste plek in de ruimte zijn. Wanneer de temperatuur van het glas lager is dan de dauwpuntstemperatuur van de ruimte zal daar water condenseren.

Het dauwpunt is de temperatuur waarbij waterdamp begint te condenseren door afkoeling van de lucht, zonder dat vocht wordt toegevoerd of afgevoerd. Zodra de lucht de dauwpuntstemperatuur bereikt, is de lucht verzadigd met waterdamp.

Het dauwpunt Td in een gegeven ruimte is afhankelijk van temperatuur T en relatieve vochtigheid RH en kan worden uitgerekend met de volgende formule (bron: Wikipedia dauwpunt):

$$ T_d = {b \cdot \gamma \over a - \gamma } $$

waarbij

$$ a = 17,27 $$

$$ b = 237,7 ⁰C $$

$$ \gamma = {a \cdot T \over b+T} + ln {RH \over 100} $$

Sensoren

Er bestaan goedkope sensoren waarmee temperatuur en relatieve luchtvochtigheid kan worden gemeten, en betaalbare sensoren voor CO₂. Geschikte sensoren voor binnengebruik zijn de Si7021 en de BME280.

In elke ruimte komt idealiter een meetstation te hangen dat temperatuur en relatieve vochtigheid van de lucht meet, met een extra temperatuursensor op het koudste oppervlak. Dat is namelijk de plek waar als eerste vocht zal condenseren. Op zolder is dit onder het dakbeschot, in de lager gelegen ruimtes waarschijnlijk een raam of een koudebrug in de constructie. Een geschikte externe temperatuursensor is de DS18B20.

In ruimtes waar vaker een groter publiek komt wordt een CO₂-sensor toegevoegd. Een sensor die CO₂, temperatuur èn luchtvochtigheid meet is de SCD4x.

4. Regelen

Om van een teveel aan vocht in de lucht af te komen zijn er drie directe technieken en één indirecte.

De gevoelstemperatuur in een ruimte kan worden beïnvloed met luchtverwarming (ook wel convectiewarmte genoemd) of stralingswarmte.

CO₂-gehalte kan worden beïnvloed met ventilatie.

Energieverbruik

Hoeveel energie kosten de verschillende technieken?

[[TODO: Opzoeken kengetallen en eenheden]]

5. Model

[[TODO: In model brengen van meet- en regellus]]

Input:

Regelmechnismen

Sensors:

Actuators:

Variables: Use = {storage | workshop | comfort} RHmin = 40% RHmax = 65% TminStorage = 5⁰C TminWorkshop = 10⁰ TminComfort = 15⁰C Tmax = 30⁰C

Key insights:

Rules:

  1. if (CO₂ > CO₂max) or (RHin > RHmax && AH(RHout, Tout) < AH(RHin, Tin)) or (RHin < RHmin && AH(RHout, Tout) > AH(RHin, Tin)) or (Use != storage && Tin > Tmax && Tout < Tin) of (Tin < Tout && Tin < (Tmax+Tcomfort)/2 && AH(RHout, Tout) < AH(RHmax, Tin)) then VENTILATE

  2. if (RHin < RHmin && AH(RHout, Tout) < AH(RHin, Tin)) then HUMIDIFY

  3. if (RHin > RHmax && AH(RHout, Tout) > AH(RHin, Tin)) then DEHUMIDIFY

  4. if (Use == storage && Tin < Tstorage) < only when Tout < Tin or also together with VENTILATE ??? or (Use == workshop && Tin < Tworkshop) or (Use == comfort && Tin < Tcomfort) then AIRCOHEAT

  5. DIRECT HEATING manual operation for extra comfort

6. Techniek

Er is een experimenteerprint ontworpen op basis van de XIAO ESP32C3 processor, met de volgende kenmerken:

Schema

PCB

Arduino code

// Go to File > Preferences and add to Additional Board Manager URLs: "https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json"
// Go to Tools > Boards Manager... and install "esp32"
// Go to Tools > Board > ESP32 Arduino and select XIAO_ESP32C3
// Go to Tools > Manage Libraries... and install "U8g2", "SparkFun SCD4x Arduino Library", "Adafruit SHT31 Library" and "Adafruit BME280 Library"
// Fill ssid and password parameters for WiFi connection

#include <WiFi.h>
#include <WebServer.h>
#include <Arduino.h>
#include <U8x8lib.h>
#include <Wire.h>
#include "SparkFun_SCD4x_Arduino_Library.h"
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <Adafruit_SHT31.h>

#define DEBUG true
#define I2C_OLED 0x3C
#define I2C_SCD  0x62
#define I2C_BME  0x76
#define I2C_SHT  0x44

// Display parameter
U8X8_SSD1306_128X64_NONAME_HW_I2C Oled(U8X8_PIN_NONE);

// Sensor parameters
SCD4x scd40;
Adafruit_BME280 bme;
//for use of sht3x, uncomment next line
//Adafruit_SHT31 sht; 
float Tin = 0.0;
float RH = 0.0;
float Tout = 0.0;
float CO2 = 0.0;

// Actuator parameters
#define heaterPin D3
#define ventilationPin D8
#define dryerPin D9
#define evaporatorPin D10

bool heaterState = false;
bool ventilationState = false;
bool dryerState = false;
bool evaporatorState = false;

// Controller parameters
int volume;
int people;
int comfort;
bool runmode = true;
int menuActive = 0;
unsigned long int lastDisplayTime;
unsigned long int lastSensorTime;

// Encoder parameters
int counter = 0;
int encoder;
volatile unsigned int lastEncoder;

// Webserver parameters
const char* ssid = "######";
const char* password = "######";
WebServer server(80);

void setup(void) {
  // Setup actuator outputs
  pinMode(heaterPin, OUTPUT);
  digitalWrite(heaterPin, heaterState);
  pinMode(ventilationPin, OUTPUT);
  digitalWrite(ventilationPin, ventilationState);
  pinMode(dryerPin, OUTPUT);
  digitalWrite(dryerPin, dryerState);
  pinMode(evaporatorPin, OUTPUT);
  digitalWrite(evaporatorPin, evaporatorState);
  
  // Setup rotary encoder
  pinMode(D0, INPUT_PULLUP);
  pinMode(D1, INPUT_PULLUP);
  pinMode(D2, INPUT_PULLUP);
  lastEncoder = digitalRead(D0);
  attachInterrupt(D0, handleEncoder, CHANGE);
  
  // Setup display
  Oled.begin();
  Oled.setPowerSave(0);
  Oled.setFont(u8x8_font_pxplusibmcgathin_f);
  Oled.drawString(0, 0, "Ti");
  Oled.drawString(0, 1, "To");
  Oled.drawString(8, 0, "RH");
  Oled.drawString(8, 1, "CO2");
  Oled.drawString(1, 2, "Volm:");
  Oled.drawString(1, 3, "Peop:");
  Oled.drawString(1, 4, "Comf:");
  Oled.drawString(1, 5, "Mode:");
  Oled.drawString(13, 2, "Htr");
  Oled.drawString(13, 3, "Vnt");
  Oled.drawString(13, 4, "Dry");
  Oled.drawString(13, 5, "Evp");

  // Setup serial connection
  if(DEBUG) Serial.begin(9600);
  
  // Setup sensors
  Wire.begin();
  bme.begin(0x76);
  scd40.begin();
  //to use sht3x, uncomment nest line
  //sht.begin(0x44);

  // Connect to WiFi network
  if(DEBUG) {
    Serial.println("Connecting to ");
    Serial.println(ssid);
  }
  
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    if(DEBUG) Serial.print(".");
  }
  if(DEBUG) {
    Serial.println("");
    Serial.println("WiFi connected..!");
    Serial.print("Got IP: ");
    Serial.println(WiFi.localIP());
  }
  
  server.on("/", httpResponse);
  server.onNotFound(httpNotFound);
  server.begin();
  if(DEBUG) Serial.println("HTTP server started");
  
  lastDisplayTime = millis();
  lastSensorTime = millis();
}

void loop(void) {
  unsigned long int currentTime = millis();

  // Read sensors
  if(scd40.readMeasurement()) {
    Tin = scd40.getTemperature();
    RH = scd40.getHumidity();
    CO2 = scd40.getCO2();
  }
  Tout = bme.readTemperature();
  //to use sht31, use sht.readTemperature() and sht.readHumidity()

  // Handle user interface
  if (currentTime - lastDisplayTime > 100) {
    updateInterface();
    updateModel();
    
    writeDisplay();
    lastDisplayTime = currentTime;
    
    digitalWrite(heaterPin, heaterState);
    digitalWrite(ventilationPin, ventilationState);
    digitalWrite(dryerPin, dryerState);
    digitalWrite(evaporatorPin, evaporatorState);
  }

  // Handle server
  server.handleClient();
}

void handleEncoder() {
  encoder = digitalRead(D0);
  if (encoder != lastEncoder){     
    if (digitalRead(D1) != encoder) counter++;
    else counter--;
  } 
  lastEncoder = encoder;
}

void updateInterface() {
  if (digitalRead(D2)) {
    menuActive+= counter<0 ? -1 : counter>0 ? 1 : 0;
    int menuMax = runmode ? 3 : 7;
    if (menuActive<0) menuActive = menuMax;
    if (menuActive>menuMax) menuActive = 0;
  }
  else {
    switch(menuActive) {
      case 0: volume+= counter; volume = min(1200, max(0, volume)); break;
      case 1: people+= counter; people = min(120, max(0, people)); break;
      case 2: comfort+= counter; comfort = min(5, max(0, comfort)); break;
      case 3: runmode = abs(counter) ? !runmode : runmode; break;
      case 4: heaterState = abs(counter) ? !heaterState : heaterState; break;
      case 5: ventilationState = abs(counter) ? !ventilationState : ventilationState; break;
      case 6: dryerState = abs(counter) ? !dryerState : dryerState; break;
      case 7: evaporatorState = abs(counter) ? !evaporatorState : evaporatorState; break;
    }
  }
  counter = 0;
}

void updateModel() {
// TODO
}

void writeDisplay() {
  char s[6];
  Oled.setCursor(3, 0);  Oled.print(Tin, 1);
  Oled.setCursor(3, 1);  Oled.print(Tout, 1);
  Oled.setCursor(12, 0); Oled.print(RH, 1);
  sprintf(s, "%*d", 4, (int)CO2);
  Oled.setCursor(12, 1); Oled.print(s);
  
  sprintf(s, "%*d", 4, volume);
  Oled.setCursor(6, 2);  Oled.print(s);
  sprintf(s, "%*d", 4, people);
  Oled.setCursor(6, 3);  Oled.print(s);
  Oled.setCursor(6, 4);  Oled.print(comfort/2==2 ? "High" : comfort/2==1 ? " Low" : "  No");
  Oled.setCursor(6, 5);  Oled.print(runmode ? "Auto" : "Hand");
  
  Oled.setFont(u8x8_font_open_iconic_play_1x1);
  Oled.drawGlyph(12, 2, heaterState ? 'F' : ' ');
  Oled.drawGlyph(12, 3, ventilationState ? 'F' : ' ');
  Oled.drawGlyph(12, 4, dryerState ? 'F' : ' ');
  Oled.drawGlyph(12, 5, evaporatorState ? 'F' : ' ');
  Oled.setFont(u8x8_font_pxplusibmcgathin_f);
  
  Oled.setCursor(0, 7);  Oled.print(WiFi.localIP());
  
  for (int i=0; i<8; i++) Oled.drawString(11*(i/4), 2+i%4, i==menuActive ? ">" : " ");
}

void httpResponse() {
  String html = "<!DOCTYPE html>";
  html+= "<html>\n";
  html+= "<head>\n";
  html+= "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, user-scalable=no\">\n";
  html+= "<meta charset=\"UTF-8\" />\n";
  html+= "<meta http-equiv=\"refresh\" content=\"15\" />\n";
  html+= "<title>De WAR Klimaatregeling</title>\n";
  html+= "<style>th { text-align:right; }</style>\n";
  html+= "</head>\n";
  html+= "<body>\n";
  html+= "<h1>De WAR Klimaatregeling</h1>\n";
  
  html+= "<table>";
  html+= "<tr><th>Volume</th><td>";
  html+= (int)volume;
  html+= " m²</td></tr>";
  html+= "<tr><th>Aantal personen</th><td>";
  html+= (int)people;
  html+= "</td></tr>";
  html+= "<tr><th>Comfort</th><td>";
  if (comfort/2==0) html+= "Geen (opslag)";
  if (comfort/2==1) html+= "Laag (bewegen)";
  if (comfort/2==2) html+= "Hoog (stilzitten)";
  html+= "</td></tr>";
  html+= "<tr><th>Modus</th><td>";
  if (runmode) html+= "Automatisch";
  if (!runmode) html+= "Handmatig";
  html+= "</td></tr>";
  html+= "<tr><td style=\"text-align:right;\">S E N S O R E N</td><td></td></tr>";
  html+= "<tr><th>Truimte</th><td>";
  html+= (int)Tin;
  html+= " ⁰C</td></tr>";
  html+= "<tr><th>Tkoudebrug</th><td>";
  html+= (int)Tout;
  html+= " ⁰C</td></tr>";
  html+= "<tr><th>Luchtvochtigheid</th><td>";
  html+= (int)RH;
  html+= " % RH</td></tr>";
  html+= "<tr><th>CO₂</th><td>";
  html+= (int)CO2;
  html+= " ppm</td></tr>";
  html+= "<tr><td style=\"text-align:right;\">A C T U A T O R E N</td><td></td></tr>";
  html+= "<tr><th>Verwarming</th><td>";
  html+= heaterState ? "Aan" : "Uit";
  html+= "</td></tr>";
  html+= "<tr><th>Ventilatie</th><td>";
  html+= ventilationState ? "Aan" : "Uit";
  html+= "</td></tr>";
  html+= "<tr><th>Ontvochtiger</th><td>";
  html+= dryerState ? "Aan" : "Uit";
  html+= "</td></tr>";
  html+= "<tr><th>Bevochtiger</th><td>";
  html+= evaporatorState ? "Aan" : "Uit";
  html+= "</td></tr>";
  html+= "</table>";
  
  html+= "</body>\n";
  html+= "</html>\n";
  
  server.send(200, "text/html", html); 
}

void httpNotFound(){
  server.send(404, "text/plain", "Not found");
}

TO DO

To do: