Wood Central Heating Monitoring

Context

At the grandma's house, where I currently live, I use a wood-burning central heating unit to stay warm during the winter months. This central heating unit uses a fan to provide fresh air to the fire, which turns off when the water (that's running through the radiators in the house) reaches the operating temperature (90 degrees Celsius). That's great, otherwise my pipes would explode.

What's controlling the fan, you may ask? A little knob on the side of the central heating unit that tells the fan (using a bimetalic strip) how cold the water has to be for it to start doing something.

That's great, but there is a small problem: when the fire dies down eventually, there is no mechanism in place to automatically tell the fan to not turn back on once the water temperature drops below the threshold set earlier by the knob… so you have to always go back to the central heating unit once the fire has started to turn the knob, in essence shutting down the fan for good.

I wanted to fix that :)

Hardware

  • ESP32-S3-devkitc-1

  • 2 * DS18B20 temperature sensors

ds18b20.jpg

  • SG-05010 servomotor

servomotor-sg5010.jpg

  • a breadboard
  • a battery pack with 4 AAA batteries (6V)

Software

There is a lot of logic that controls the icons that are displayed in the interface, I'll stick them below in the code snippets because talking about them is really really boring, I just thought of all the possible cases and coded them in.

Code Snippets

Initial checks when ESP32 starts (maybe the user resets it manually):

if (initialTempC >= 30.0) {
    // If temperature is already high, set servo to triggered position and fan OFF
    lastServoPos = SERVO_TRIGGER_POS;
    fanState = false;
    fireState = "off";
    Serial.println("Initial state: Temperature above 30°C - Setting servo to 30° and fan OFF");
    
    // Actually move the servo to 30° position
    myServo.attach(SERVO_PIN, 700, 2300);
    myServo.write(SERVO_TRIGGER_POS);
    delay(700); // Give servo time to move to position
    myServo.detach();
    Serial.println("Servo physically positioned at 30° and detached");
  } else {
    // If temperature is normal at startup, initialize to 0° position
    lastServoPos = SERVO_RESET_POS;
    fanState = true;
    fireState = "starting";
    Serial.println("Initial state: Temperature below 30°C - Setting servo to 0° and fan ON");
    
    // Actually move the servo to 0° position
    myServo.attach(SERVO_PIN, 700, 2300);
    myServo.write(SERVO_RESET_POS);
    delay(700); // Give servo time to move to position
    myServo.detach();
    Serial.println("Servo physically positioned at 0° and detached");
    
    // Clean database at startup
    Serial.println("Performing initial database cleanup...");
    cleanupDatabase();
  }

Standard checks:

if (Firebase.ready() && signupOK && (millis() - commandCheckPrevMillis > commandCheckDelay || commandCheckPrevMillis == 0)) {
    commandCheckPrevMillis = millis();
    
    // Check for reset button commands
    if (Firebase.RTDB.getInt(&fbdo, "/commands/resetServo")) {
      int resetCommand = fbdo.intData();
      Serial.print("Reset command received: ");
      Serial.println(resetCommand);
      
      if (resetCommand > 0) {
        Serial.println("Reset button pressed - checking if temperature allows reset...");
        
        // Read latest temperature values when reset button is pressed
        sensors.requestTemperatures();
        smokeSensors.requestTemperatures();
        tempC = sensors.getTempCByIndex(0);
        smokeTempC = smokeSensors.getTempCByIndex(0);
        
        Serial.print("Water Temperature: ");
        Serial.print(tempC);
        Serial.println(" °C");
        Serial.print("Smoke Temperature: ");
        Serial.print(smokeTempC);
        Serial.println(" °C");
        
        // Check if temperature allows reset
        if (tempC < 30.0) {
          // If temperature is normal, set fan ON
          targetPos = SERVO_RESET_POS;
          fanState = true;
          fireState = "starting";
          Serial.println("Reset accepted: Temperature is below 30°C - Moving servo to 0°");
          // Clean the entire database
          Serial.println("Clearing all historical data...");
          cleanupDatabase();
        } else {
          // If temperature is high, don't allow reset - keep servo at 30° and fan OFF
          targetPos = SERVO_TRIGGER_POS;
          fanState = false; 
          fireState = "off";
          Serial.println("Reset rejected: Temperature is above 30°C - Servo remains at 30°");
          
          // Even when reset is rejected, update the current data in Firebase immediately
          updateLatestFirebaseData(tempC, smokeTempC, targetPos);
        }
        
        // Verify that the temperature and servo position follow the rules:
        // - If temp ≥ 30°C, servo MUST be at 30° (safety critical)
        // - If reset was accepted, servo MUST be at 0° (just pressed)
        // - If reset was rejected, servo MUST be at 30° (just verified)
        Serial.println("RESET VERIFICATION: Checking temperature and servo consistency");
        if (tempC >= 30.0 && targetPos != SERVO_TRIGGER_POS) {
          // This is a safety-critical error and needs immediate correction
          Serial.println("CRITICAL ERROR: Temperature ≥30°C but servo not at 30°!");
          Serial.println("SAFETY ACTION: Overriding to ensure servo at 30° position!");
          targetPos = SERVO_TRIGGER_POS;
          fanState = false;
          
          // Physically move servo to safe position 
          myServo.attach(SERVO_PIN, 700, 2300);
          myServo.write(SERVO_TRIGGER_POS);
          lastServoPos = SERVO_TRIGGER_POS;
          delay(700);
          myServo.detach();
        } else if (tempC < 30.0 && resetCommand > 0 && targetPos != SERVO_RESET_POS) {
          // Only fix position if reset was explicitly requested and temperature allows it
          Serial.println("ERROR: Reset requested with temp <30°C but servo not at 0°");
          Serial.println("Fixing: Moving servo to 0° position");
          targetPos = SERVO_RESET_POS;
          fanState = true;
          
          // Physically move servo
          myServo.attach(SERVO_PIN, 700, 2300);
          myServo.write(SERVO_RESET_POS);
          lastServoPos = SERVO_RESET_POS;
          delay(700);
          myServo.detach();
        } else {
          Serial.println("VERIFICATION PASSED: Temperature and servo position are consistent");
        }
        
        // Acknowledge the command by setting it to 0
        Firebase.RTDB.setInt(&fbdo, "/commands/resetServo", 0);
      }
    }
  }
  
  // Always check smoke temperature - this takes precedence over Start Fire
  if (smokeTempC > 0) {  // Only update if we've read the sensors during this loop
    if (smokeTempC >= 28.0) {
      // If smoke temp is high, always show fire icon regardless of fan state
      fireState = "on";
    } else if (fanState) {
      // If smoke temp is low and fan is ON, show bolt
      fireState = "starting";
    } else {
      // If smoke temp is low and fan is OFF, show extinguisher
      fireState = "off";
    }
  }
  
  // --- Fan/fire logic based on temperatures ---
  if (tempC > 0) {  // Only update if we've read the sensors during this loop
    if (tempC >= 30.0) {
      // CRITICAL: High temperature needs immediate servo action!
      if (lastServoPos != SERVO_TRIGGER_POS) {
        // Servo is not in correct position for this high temperature!
        targetPos = SERVO_TRIGGER_POS; 
        fanState = false;  // Always turn fan OFF when temperature is high
        Serial.println("HIGH TEMP SAFETY: Temperature >= 30°C with servo not at 30° - Moving servo NOW!");
      } else {
        // Servo is already in correct position but emphasize high temp state
        Serial.println("Temperature still >= 30°C: Maintaining servo at 30° and fan OFF");
      }
    } else if (tempC < 30.0) {
      // When temperature drops below 30°C, DO NOT automatically move servo to 0°
      // Only update fan state based on current servo position
      fanState = (lastServoPos == SERVO_RESET_POS);
      if (lastServoPos == SERVO_TRIGGER_POS) {
        Serial.println("Temperature now < 30°C but servo will remain at 30° until reset button is pressed");
      }
    }
  }
  
  // Only move the servo if the target position is different from the current position
  if (targetPos != lastServoPos) {
    Serial.print("Servo position change needed: current = ");
    Serial.print(lastServoPos);
    Serial.print("°, target = ");
    Serial.print(targetPos);
    Serial.println("°");
    
    // Attach servo, move it, then detach to prevent jitter and reduce power consumption
    myServo.attach(SERVO_PIN, 700, 2300);
    myServo.write(targetPos);
    
    // Update logical state
    lastServoPos = targetPos;
    
    // Update fan state based on servo position
    // SERVO_RESET_POS (0°) = fan ON, SERVO_TRIGGER_POS (30°) = fan OFF
    bool previousFanState = fanState;
    fanState = (targetPos == SERVO_RESET_POS);
    
    if (previousFanState != fanState) {
      Serial.print("Fan state updated to: ");
      Serial.println(fanState ? "ON" : "OFF");
    }
    
    // Wait for servo to complete movement
    delay(500);
    
    // Detach servo to prevent jitter and reduce power consumption
    myServo.detach();
    Serial.println("Servo detached after movement");
    
    // Short delay after servo movement for stability
    delay(500);
  }
  
  // Small delay for loop stability - this doesn't affect command responsiveness
  delay(200);
  
  // Check if it's time to send sensor data (every 1 second)
  if (signupOK && Firebase.ready() && (millis() - lastSend > sensorDataDelay)) {
    lastSend = millis();
    
    // Read sensors every time we check (every 1s)
    sensors.requestTemperatures();
    smokeSensors.requestTemperatures();
    tempC = sensors.getTempCByIndex(0);
    smokeTempC = smokeSensors.getTempCByIndex(0);
    
    Serial.print("Water Temperature: ");
    Serial.print(tempC);
    Serial.println(" °C");
    Serial.print("Smoke Temperature: ");
    Serial.print(smokeTempC);
    Serial.println(" °C");
    
    // IMPORTANT: Only move servo to 30° when temperature rises above 30°C
    // DO NOT move servo back to 0° automatically - this only happens with reset button
    if (tempC >= 30.0 && lastServoPos != SERVO_TRIGGER_POS) {
      // Critical temperature detected, servo needs to move!
      Serial.println("CRITICAL: Temperature ≥30°C but servo not at 30° - Moving servo now!");
      targetPos = SERVO_TRIGGER_POS;
      fanState = false;
      
      // Move servo immediately - don't wait for next loop
      Serial.println("Moving servo to 30° position due to high temperature");
      myServo.attach(SERVO_PIN, 700, 2300);
      myServo.write(targetPos);
      lastServoPos = targetPos;
      delay(500); // Wait for servo to complete movement
      myServo.detach();
      Serial.println("Servo moved to 30° and detached");
    }
    // We don't auto-reset to 0° when temperature drops - only reset button can do this
    
    // Now update Firebase with the latest data (including potentially updated servo position)
    updateLatestFirebaseData(tempC, smokeTempC, lastServoPos);
  }

I really hate just throwing code in here and calling it a day but I'm really tired and also otherwise the page would be empty.

Firebase

The entire application is hosted in firebase using a realtime database that contains:

  • history of sensor readings
  • latest reading
  • status of “Start Fire” button

The entire communication between the ESP32 and the Firebase application is done using the realtime database:

Challenges

How the hell can someone open a store that sells something and have no knowledge about it? There is one store in Pitesti that sells electronics and Arduino stuff, and they have no idea what they are selling. Literally. I went there asking for parts and some advice, and they asked me for the codes of stuff on their website. They even gave me the wrong thing because I wasn't paying attention. HOW? (It's also my fault, I suppose, but still)

Another challenge was getting the interface to be responsive:

And also all of the other stuff that can go wrong when using this hellish spawn of a platform called ESP32/Arduino, I really hope someone pays for the suffering they have upon the world.

References

Idk, stuff online I guess?

iothings/proiecte/2025sric/centralheatingmonitoring.txt · Last modified: 2025/05/28 23:49 by george.tudurean
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0