CategoriesBuilds, Projects & Solutions

Finalizing Our Smart Fan Controller: From Temperature Offset to Real-Time Data Logging

So, here we are, putting the final(-ish) touches on our smart fan controller project. What started as a simple temperature-based fan switch has evolved into something much more sophisticated. Now, we’re talking temperature offsets, data logging, real-time charts, and even NTP time synchronization.

If you missed the original posts where this project started, make sure you check them out:

Let’s dive into these new features and see what’s under the hood.

Temperature Offset Adjustment

First up, temperature offset. I noticed how our sensor’s reading didn’t quite match that fancy external display we’ve got. Well, now you can calibrate your readings with a simple offset adjustment. It’s like giving your sensor a pair of glasses – it finally sees things clearly! In our case, I don’t know the accuracy of the external display on the rack, but this still becomes a useful feature as you could calibrate on the fly with a known accurate sensor as well.

float TEMP_OFFSET = -0.5; // Temperature offset to match external display

void handleSetTempOffset() {
  String tempOffset = server.arg("tempOffset");
  TEMP_OFFSET = tempOffset.toFloat();
  server.send(200, "text/plain", "OK");
}

Data Logging and Chart Display

Next, we’ve got data logging and chart displays – because who doesn’t love a good graph? With this update, your temperature data is logged over time, the logs rotate out, and you can visualize it on the web interface.

void storeData(float temp) {
  const int maxEntries = 300; // Maximum number of entries to keep
  String newData = String(timeClient.getEpochTime()) + "," + String(temp, 1);

  // Read existing data
  File file = SPIFFS.open("/data.txt", "r");
  String data = "";
  if (file) {
    while (file.available()) {
      data += (char)file.read();
    }
    file.close();
  }

  // Split the data into entries based on the semicolon delimiter
  int numEntries = 0;
  int pos = 0;
  while ((pos = data.indexOf(';', pos)) != -1) {
    numEntries++;
    pos++;
  }

  // If we have too many entries, remove the oldest ones
  if (numEntries >= maxEntries) {
    int trimPos = data.indexOf(';') + 1;
    data = data.substring(trimPos);
  }

  // Append the new entry
  if (data.length() > 0) {
    data += ";";
  }
  data += newData;

  // Write the updated data back to the file
  file = SPIFFS.open("/data.txt", "w");
  if (file) {
    file.print(data);
    file.close();
  } else {
    logDebug("Failed to open data file for writing");
  }
}

void handleChartData() {
  if (!SPIFFS.exists("/data.txt")) {
    logDebug("File not found: /data.txt");
    server.send(404, "text/plain", "File Not Found");
    return;
  }

  File file = SPIFFS.open("/data.txt", "r");
  String data = "";
  if (file) {
    while (file.available()) {
      data += (char)file.read();
    }
    file.close();
  } else {
    logDebug("Failed to open data file: /data.txt");
    server.send(500, "text/plain", "Failed to open data file");
    return;
  }

  // Send the data to the client
  server.send(200, "text/plain", data);
}

NTP Time Synchronization

We also brought in NTP time synchronization. Now, all those data points can be accurately timestamped, ensuring you know exactly when things heated up. No more guessing when that temperature spike happened.

// NTP settings
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "1.ca.pool.ntp.org", 0, 60000); // UTC time, update every 60 seconds

void setup() {
  ...
  // Initialize NTP
  timeClient.begin();

  // Synchronize time with NTP server
  while(!timeClient.update()) {
    timeClient.forceUpdate();
  }

  // Log the initial time synchronization
  String currentTime = timeClient.getFormattedTime();
  logDebug("Time synchronized: " + currentTime);
  ...
}

void loop() {
  ...
  static unsigned long lastNTPUpdateTime = 0;
  const unsigned long NTP_UPDATE_INTERVAL = 3600000; // Update every 60 minutes (3600000 ms)

  // Update NTP time periodically
  if (millis() - lastNTPUpdateTime >= NTP_UPDATE_INTERVAL) {
    timeClient.update();
    lastNTPUpdateTime = millis();
  }
  ...
}

Enhanced Web Interface

We’ve also given our web interface a much-needed facelift. It’s got Bootstrap for that sleek, responsive look and now includes real-time updates, manual controls, and even the ability to set thresholds right from your browser.

void handleRoot() {
  float temp = dht.readTemperature() + TEMP_OFFSET;
  float humidity = dht.readHumidity();
  String fanStatus = fanOn ? "ON" : "OFF";
  String html = "<html><head><meta charset=\"UTF-8\">";
  html += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">";
  html += "<link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css\">";
  html += "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js\"></script>";
  html += "<style>";
  html += "body { font-family: Arial, sans-serif; }";
  html += "h1 { color: #333; }";
  html += ".btn { border-radius: 0; }"; /* Make buttons square */
  html += ".container { max-width: 1000px; margin-top: 20px; }";
  html += ".form-group { margin-bottom: 10px; }";
  html += "canvas { display: block; margin: 0 auto; }";
  html += "#logContent { background-color: #f8f9fa; padding: 10px; border: 1px solid #ddd; white-space: pre-wrap; height: 200px; overflow-y: scroll; }";
  html += ".inline-input { width: 95px; display: inline-block; }"; /* Adjusted width */
  html += ".inline-label { display: inline-block; margin-right: 30px; }"; /* Adjusted margin */
  html += "</style>";
  html += "<script>";
  html += "function updateData() {";
  html += "  fetch('/update').then(response => response.json()).then(data => {";
  html += "    document.getElementById('temp').innerText = data.temperature + ' °C';";
  html += "    document.getElementById('humidity').innerText = data.humidity + ' %';";
  html += "    document.getElementById('fanStatus').innerText = data.fanStatus;";
  html += "  });";
  html += "  fetchLog();";
  html += "  fetchChartData();";
  html += "}";
  html += "function controlFan(command) {";
  html += "  fetch('/control?command=' + command).then(() => updateData());";
  html += "}";
  html += "function setControls() {";
  html += "  const onThreshold = document.getElementById('onThreshold').value;";
  html += "  const offThreshold = document.getElementById('offThreshold').value;";
  html += "  const tempOffset = document.getElementById('tempOffset').value;";
  html += "  fetch(`/setThresholds?onThreshold=${onThreshold}&offThreshold=${offThreshold}`).then(() => {";
  html += "    fetch(`/setTempOffset?tempOffset=${tempOffset}`).then(() => updateData());";
  html += "  });";
  html += "}";

  html += "function fetchChartData() {";
  html += "  fetch('/chart').then(response => response.text()).then(data => {";
  html += "    const entries = data.split(';');"; // Split data by semicolon
  html += "    const labels = [];"; // For timestamps
  html += "    const tempData = [];"; // For temperatures";
  html += "    for (let i = 0; i < entries.length; i++) {";
  html += "      const parts = entries[i].split(',');"; // Split each entry into timestamp and temperature
  html += "      const timestamp = new Date(parseInt(parts[0]) * 1000);"; // Convert epoch to milliseconds and then to Date object
  html += "      labels.push(timestamp);"; // Use Date object as label";
  html += "      tempData.push(parseFloat(parts[1]));"; // Temperature
  html += "    }";
  html += "    new Chart(document.getElementById('chart'), {";
  html += "      type: 'line',";
  html += "      data: { labels: labels, datasets: [{ label: 'Temperature (°C)', data: tempData }] },";
  html += "      options: {";
  html += "        animation: false,"; // Disable animations";
  html += "        scales: {";
  html += "          xAxes: [{ type: 'time', time: { unit: 'minute', tooltipFormat: 'll HH:mm:ss', displayFormats: { minute: 'HH:mm' }}}],";
  html += "          yAxes: [{ type: 'linear', position: 'left' }]";
  html += "        }";
  html += "      }";
  html += "    });";
  html += "  }).catch(error => console.error('Error fetching chart data:', error));";
  html += "}";

  html += "function fetchLog() {";
  html += "  fetch('/log.txt').then(response => response.text()).then(data => {";
  html += "    document.getElementById('logContent').innerText = data;";
  html += "  });";
  html += "}";
  html += "function clearLog() {";
  html += "  fetch('/clearLog').then(() => {";
  html += "    document.getElementById('logContent').innerText = '';";
  html += "  });";
  html += "}";
  html += "setInterval(updateData, 2000);";
  html += "</script>";
  html += "</head><body onload='updateData();'>";
  html += "<div class='container'>";
  html += "<h1 class='text-center'>Fan Controller</h1>";
  html += "<canvas id='chart' width='300' height='150'></canvas>";
  html += "<div class='form-group text-center'>";
  html += "<span class='inline-label'>Current Temperature: <span id='temp'>" + String(temp) + " °C</span></span>";
  html += "<span class='inline-label'>Current Humidity: <span id='humidity'>" + String(humidity) + " %</span></span>";
  html += "<span class='inline-label'>Fan Status: <span id='fanStatus'>" + fanStatus + "</span></span>";
  html += "</div>";
  html += "<div class='form-group text-center'>";
  html += "<label class='inline-label'>On Threshold: <input type='number' id='onThreshold' value='" + String(ON_THRESHOLD) + "' class='form-control inline-input'> °C</label>";
  html += "<label class='inline-label'>Off Threshold: <input type='number' id='offThreshold' value='" + String(OFF_THRESHOLD) + "' class='form-control inline-input'> °C</label>";
  html += "<label class='inline-label'>Temperature Offset: <input type='number' id='tempOffset' value='" + String(TEMP_OFFSET) + "' class='form-control inline-input'> °C</label>";
  html += "</div>";
  html += "<button onclick='setControls()' class='btn btn-primary btn-block'>Set Thresholds and Offset</button>";
  html += "<div class='d-flex justify-content-between mt-3'>";
  html += "<button onclick='controlFan(\"ON\")' class='btn btn-success flex-fill mr-1'>Turn Fan ON</button>";
  html += "<button onclick='controlFan(\"OFF\")' class='btn btn-danger flex-fill ml-1'>Turn Fan OFF</button>";
  html += "</div>";
  html += "<pre id='logContent' class='mt-3'></pre>";
  html += "<button onclick='clearLog()' class='btn btn-warning btn-block mt-3'>Clear Log</button>";
  html += "</div>";
  html += "</body></html>";

  server.send(200, "text/html", html);
}

Manual Fan Control & LED Indicator

Sometimes, you just need to take control. That’s why we added manual fan control through the web interface. Plus, we synced the built-in LED with the fan status.

void handleFanControl() {
  String command = server.arg("command");
  if (command == "ON") {
    digitalWrite(RELAY_PIN, LOW); // Turn on the fan
    digitalWrite(BUILTIN_LED, LOW); // Sync LED state with fan
    fanOn = true;
  } else if (command == "OFF") {
    digitalWrite(RELAY_PIN, HIGH); // Turn off the fan
    digitalWrite(BUILTIN_LED, HIGH); // Sync LED state with fan
    fanOn = false;
  }
  server.send(200, "text/plain", "OK");
}

Additional Changes

We didn’t stop there. A few other tweaks make this system even better:

Built-in LED Control: The ESP8266’s LED now lights up to show the fan status.

Clear Log and Data Feature: Need a fresh start? You can clear logs and data files directly from the interface.

Improved Error Handling: We beefed up the error handling so your system can keep chugging along, even if something goes awry.

Humidity Display: We’ve also added real-time humidity monitoring.

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <DHT.h>
#include <ArduinoOTA.h>
#include <FS.h>
#include <NTPClient.h>

#define DHTPIN 2       // GPIO2 corresponds to D4 on the ESP8266
#define DHTTYPE DHT11  // Define the sensor type
#define RELAY_PIN 5    // GPIO5 corresponds to D1 on the ESP8266
#define BUILTIN_LED 2  // GPIO2 corresponds to the built-in LED on the ESP8266

const char* ssid = "YOUR SSID";
const char* password = "YOUR WIFI PASSWORD";

// NTP settings
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "1.ca.pool.ntp.org", 0, 60000);  // UTC time, update every 60 seconds

DHT dht(DHTPIN, DHTTYPE);
ESP8266WebServer server(80);

bool fanOn = false;
float ON_THRESHOLD = 30.0;   // Temperature to turn the fan on
float OFF_THRESHOLD = 28.0;  // Temperature to turn the fan off
float TEMP_OFFSET = -0.5;    // Temperature offset to match external display
float lastLoggedTemp = 0;

void logDebug(String message) {
  //Serial.println(message); // Output to serial console
  File logFile = SPIFFS.open("/log.txt", "a");
  if (logFile) {
    logFile.println(message);
    logFile.close();
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(RELAY_PIN, OUTPUT);
  pinMode(BUILTIN_LED, OUTPUT);
  digitalWrite(RELAY_PIN, HIGH);    // Ensure the fan is initially off
  digitalWrite(BUILTIN_LED, HIGH);  // Ensure the LED is off
  dht.begin();

  if (!SPIFFS.begin()) {
    Serial.println("Failed to mount file system");
    return;
  }

  // Create initial data file if it doesn't exist
  if (!SPIFFS.exists("/data.txt")) {
    File dataFile = SPIFFS.open("/data.txt", "w");
    if (!dataFile) {
      logDebug("Failed to create data file");
    } else {
      dataFile.close();
      logDebug("Data file created successfully");
    }
  }

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    logDebug("Connecting to WiFi...");
  }

  logDebug("Connected to WiFi");

  // Initialize NTP
  timeClient.begin();

  // Synchronize time with NTP server
  while (!timeClient.update()) {
    timeClient.forceUpdate();
  }

  // Log the initial time synchronization
  String currentTime = timeClient.getFormattedTime();
  logDebug("Time synchronized: " + currentTime);

  server.on("/", handleRoot);
  server.on("/update", handleUpdate);
  server.on("/control", handleFanControl);
  server.on("/chart", handleChartData);
  server.on("/setThresholds", handleSetThresholds);
  server.on("/setTempOffset", handleSetTempOffset);
  server.on("/log.txt", handleLogFile);
  server.on("/data.txt", handleChartData);
  server.on("/clearLog", handleClearLog);
  server.begin();
  logDebug("HTTP server started");

  logDebug("IP Address: " + WiFi.localIP().toString());

  // Start OTA
  ArduinoOTA.onStart([]() {
    String type;
    if (ArduinoOTA.getCommand() == U_FLASH) {
      type = "sketch";
    } else {  // U_SPIFFS
      type = "filesystem";
    }
    Serial.println("Start updating " + type);
  });
  ArduinoOTA.onEnd([]() {
    Serial.println("\nEnd");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.println("Progress: " + String((progress / (total / 100))) + "%");
  });
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.println("Error[" + String(error) + "]: " + (error == OTA_AUTH_ERROR ? "Auth Failed" : error == OTA_BEGIN_ERROR   ? "Begin Failed"
                                                                                               : error == OTA_CONNECT_ERROR ? "Connect Failed"
                                                                                               : error == OTA_RECEIVE_ERROR ? "Receive Failed"
                                                                                                                            : "End Failed"));
  });
  ArduinoOTA.begin();
}

void loop() {
  server.handleClient();
  ArduinoOTA.handle();  // Handle OTA updates

  static unsigned long lastReadTime = 0;
  static unsigned long lastNTPUpdateTime = 0;
  const unsigned long NTP_UPDATE_INTERVAL = 3600000;  // Update every 60 minutes (3600000 ms)

  // Update NTP time at triggered intervals
  if (millis() - lastNTPUpdateTime >= NTP_UPDATE_INTERVAL) {
    timeClient.update();  // Update the time
    lastNTPUpdateTime = millis();
  }

  if (millis() - lastReadTime >= 2000) {  // Read every 2 seconds
    float temp = dht.readTemperature() + TEMP_OFFSET;
    float humidity = dht.readHumidity();

    if (!isnan(temp) && !isnan(humidity)) {
      if (abs(temp - lastLoggedTemp) >= 1.0) {
        logDebug("Temperature: " + String(temp) + " °C, Humidity: " + String(humidity) + " %");
        lastLoggedTemp = temp;
      }

      storeData(temp);

      if (!fanOn && temp > ON_THRESHOLD) {
        digitalWrite(RELAY_PIN, LOW);    // Turn on the fan
        digitalWrite(BUILTIN_LED, LOW);  // Turn off the built-in LED (indicating fan is on)
        fanOn = true;
        logDebug("Fan turned ON automatically");
      } else if (fanOn && temp < OFF_THRESHOLD) {
        digitalWrite(RELAY_PIN, HIGH);    // Turn off the fan
        digitalWrite(BUILTIN_LED, HIGH);  // Turn on the built-in LED (indicating fan is off)
        fanOn = false;
        logDebug("Fan turned OFF automatically");
      }
    } else {
      logDebug("Failed to read from DHT sensor!");
    }

    lastReadTime = millis();
  }
}

void handleRoot() {
  float temp = dht.readTemperature() + TEMP_OFFSET;
  float humidity = dht.readHumidity();
  String fanStatus = fanOn ? "ON" : "OFF";
  String html = "<html><head><meta charset=\"UTF-8\">";
  html += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">";
  html += "<link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css\">";
  html += "<script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js\"></script>";
  html += "<style>";
  html += "body { font-family: Arial, sans-serif; }";
  html += "h1 { color: #333; }";
  html += ".btn { border-radius: 0; }"; /* Make buttons square */
  html += ".container { max-width: 1000px; margin-top: 20px; }";
  html += ".form-group { margin-bottom: 10px; }";
  html += "canvas { display: block; margin: 0 auto; }";
  html += "#logContent { background-color: #f8f9fa; padding: 10px; border: 1px solid #ddd; white-space: pre-wrap; height: 200px; overflow-y: scroll; }";
  html += ".inline-input { width: 95px; display: inline-block; }";        /* Adjusted width */
  html += ".inline-label { display: inline-block; margin-right: 30px; }"; /* Adjusted margin */
  html += "</style>";
  html += "<script>";
  html += "function updateData() {";
  html += "  fetch('/update').then(response => response.json()).then(data => {";
  html += "    document.getElementById('temp').innerText = data.temperature + ' °C';";
  html += "    document.getElementById('humidity').innerText = data.humidity + ' %';";
  html += "    document.getElementById('fanStatus').innerText = data.fanStatus;";
  html += "  });";
  html += "  fetchLog();";
  html += "  fetchChartData();";
  html += "}";
  html += "function controlFan(command) {";
  html += "  fetch('/control?command=' + command).then(() => updateData());";
  html += "}";
  html += "function setControls() {";
  html += "  const onThreshold = document.getElementById('onThreshold').value;";
  html += "  const offThreshold = document.getElementById('offThreshold').value;";
  html += "  const tempOffset = document.getElementById('tempOffset').value;";
  html += "  fetch(`/setThresholds?onThreshold=${onThreshold}&offThreshold=${offThreshold}`).then(() => {";
  html += "    fetch(`/setTempOffset?tempOffset=${tempOffset}`).then(() => updateData());";
  html += "  });";
  html += "}";

  html += "function fetchChartData() {";
  html += "  fetch('/chart').then(response => response.text()).then(data => {";
  html += "    const entries = data.split(';');";  // Split data by semicolon
  html += "    const labels = [];";                // For timestamps
  html += "    const tempData = [];";              // For temperatures";
  html += "    for (let i = 0; i < entries.length; i++) {";
  html += "      const parts = entries[i].split(',');";                    // Split each entry into timestamp and temperature
  html += "      const timestamp = new Date(parseInt(parts[0]) * 1000);";  // Convert epoch to milliseconds and then to Date object
  html += "      labels.push(timestamp);";                                 // Use Date object as label";
  html += "      tempData.push(parseFloat(parts[1]));";                    // Temperature
  html += "    }";
  html += "    new Chart(document.getElementById('chart'), {";
  html += "      type: 'line',";
  html += "      data: { labels: labels, datasets: [{ label: 'Temperature (°C)', data: tempData }] },";
  html += "      options: {";
  html += "        animation: false,";  // Disable animations";
  html += "        scales: {";
  html += "          xAxes: [{ type: 'time', time: { unit: 'minute', tooltipFormat: 'll HH:mm:ss', displayFormats: { minute: 'HH:mm' }}}],";
  html += "          yAxes: [{ type: 'linear', position: 'left' }]";
  html += "        }";
  html += "      }";
  html += "    });";
  html += "  }).catch(error => console.error('Error fetching chart data:', error));";
  html += "}";

  html += "function fetchLog() {";
  html += "  fetch('/log.txt').then(response => response.text()).then(data => {";
  html += "    document.getElementById('logContent').innerText = data;";
  html += "  });";
  html += "}";
  html += "function clearLog() {";
  html += "  fetch('/clearLog').then(() => {";
  html += "    document.getElementById('logContent').innerText = '';";
  html += "  });";
  html += "}";
  html += "setInterval(updateData, 2000);";
  html += "</script>";
  html += "</head><body onload='updateData();'>";
  html += "<div class='container'>";
  html += "<h1 class='text-center'>Fan Controller</h1>";
  html += "<canvas id='chart' width='300' height='150'></canvas>";
  html += "<div class='form-group text-center'>";
  html += "<span class='inline-label'>Current Temperature: <span id='temp'>" + String(temp) + " °C</span></span>";
  html += "<span class='inline-label'>Current Humidity: <span id='humidity'>" + String(humidity) + " %</span></span>";
  html += "<span class='inline-label'>Fan Status: <span id='fanStatus'>" + fanStatus + "</span></span>";
  html += "</div>";
  html += "<div class='form-group text-center'>";
  html += "<label class='inline-label'>On Threshold: <input type='number' id='onThreshold' value='" + String(ON_THRESHOLD) + "' class='form-control inline-input'> °C</label>";
  html += "<label class='inline-label'>Off Threshold: <input type='number' id='offThreshold' value='" + String(OFF_THRESHOLD) + "' class='form-control inline-input'> °C</label>";
  html += "<label class='inline-label'>Temperature Offset: <input type='number' id='tempOffset' value='" + String(TEMP_OFFSET) + "' class='form-control inline-input'> °C</label>";
  html += "</div>";
  html += "<button onclick='setControls()' class='btn btn-primary btn-block'>Set Thresholds and Offset</button>";
  html += "<div class='d-flex justify-content-between mt-3'>";
  html += "<button onclick='controlFan(\"ON\")' class='btn btn-success flex-fill mr-1'>Turn Fan ON</button>";
  html += "<button onclick='controlFan(\"OFF\")' class='btn btn-danger flex-fill ml-1'>Turn Fan OFF</button>";
  html += "</div>";
  html += "<pre id='logContent' class='mt-3'></pre>";
  html += "<button onclick='clearLog()' class='btn btn-warning btn-block mt-3'>Clear Log</button>";
  html += "</div>";
  html += "</body></html>";

  server.send(200, "text/html", html);
}

void handleUpdate() {
  float temp = dht.readTemperature() + TEMP_OFFSET;
  float humidity = dht.readHumidity();
  String fanStatus = fanOn ? "ON" : "OFF";
  String json = "{";
  json += "\"temperature\": " + String(temp) + ",";
  json += "\"humidity\": " + String(humidity) + ",";
  json += "\"fanStatus\": \"" + fanStatus + "\"";
  json += "}";
  server.send(200, "application/json", json);
}

void handleFanControl() {
  String command = server.arg("command");
  logDebug("Fan control command received: " + command);
  if (command == "ON") {
    digitalWrite(RELAY_PIN, LOW);
    digitalWrite(BUILTIN_LED, LOW);  // Sync LED state with relay (fan on)
    fanOn = true;
    logDebug("Fan turned ON");
  } else if (command == "OFF") {
    digitalWrite(RELAY_PIN, HIGH);
    digitalWrite(BUILTIN_LED, HIGH);  // Sync LED state with relay (fan off)
    fanOn = false;
    logDebug("Fan turned OFF");
  }
  server.send(200, "text/plain", "OK");
}

void handleSetThresholds() {
  String onThreshold = server.arg("onThreshold");
  String offThreshold = server.arg("offThreshold");
  logDebug("Set thresholds command received: onThreshold=" + onThreshold + ", offThreshold=" + offThreshold);
  ON_THRESHOLD = onThreshold.toFloat();
  OFF_THRESHOLD = offThreshold.toFloat();

  // Immediately evaluate and update fan state based on new thresholds
  float temp = dht.readTemperature() + TEMP_OFFSET;
  if (!isnan(temp)) {
    if (fanOn && temp < OFF_THRESHOLD) {
      digitalWrite(RELAY_PIN, HIGH);    // Turn off the fan
      digitalWrite(BUILTIN_LED, HIGH);  // Sync LED state with relay (fan off)
      fanOn = false;
      logDebug("Fan turned OFF due to new threshold");
    } else if (!fanOn && temp > ON_THRESHOLD) {
      digitalWrite(RELAY_PIN, LOW);    // Turn on the fan
      digitalWrite(BUILTIN_LED, LOW);  // Sync LED state with relay (fan on)
      fanOn = true;
      logDebug("Fan turned ON due to new threshold");
    }
  }

  server.send(200, "text/plain", "OK");
}

void handleSetTempOffset() {
  String tempOffset = server.arg("tempOffset");
  logDebug("Set temperature offset command received: tempOffset=" + tempOffset);
  TEMP_OFFSET = tempOffset.toFloat();
  server.send(200, "text/plain", "OK");
}

void handleChartData() {
  if (!SPIFFS.exists("/data.txt")) {
    logDebug("File not found: /data.txt");
    server.send(404, "text/plain", "File Not Found");
    return;
  }

  File file = SPIFFS.open("/data.txt", "r");
  String data = "";
  if (file) {
    while (file.available()) {
      data += (char)file.read();
    }
    file.close();
  } else {
    logDebug("Failed to open data file: /data.txt");
    server.send(500, "text/plain", "Failed to open data file");
    return;
  }

  // Send the data to the client
  server.send(200, "text/plain", data);
}

void handleLogFile() {
  if (!SPIFFS.exists("/log.txt")) {
    server.send(200, "text/plain", "");
    return;
  }

  File logFile = SPIFFS.open("/log.txt", "r");
  if (!logFile) {
    server.send(500, "text/plain", "Failed to open log file");
    return;
  }

  String log = "";
  while (logFile.available()) {
    log += logFile.readStringUntil('\n');
  }
  logFile.close();
  server.send(200, "text/plain", log.length() > 0 ? log : " ");
}

void handleClearLog() {
  if (SPIFFS.exists("/log.txt")) {
    SPIFFS.remove("/log.txt");
  }
  if (SPIFFS.exists("/data.txt")) {
    SPIFFS.remove("/data.txt");
    // Recreate the data.txt file to prevent issues with non-existent file
    File dataFile = SPIFFS.open("/data.txt", "w");
    if (!dataFile) {
      logDebug("Failed to create data file");
    } else {
      //dataFile.println("0.0");
      dataFile.close();
      logDebug("Data file created successfully");
    }
  }
  logDebug("Log file cleared");
  server.send(200, "text/plain", "Log cleared");
}

void storeData(float temp) {
  const int maxEntries = 300;  // Maximum number of entries to keep
  String newData = String(timeClient.getEpochTime()) + "," + String(temp, 1);

  // Read existing data
  File file = SPIFFS.open("/data.txt", "r");
  String data = "";
  if (file) {
    while (file.available()) {
      data += (char)file.read();
    }
    file.close();
  }

  // Split the data into entries based on the semicolon delimiter
  int numEntries = 0;
  int pos = 0;
  while ((pos = data.indexOf(';', pos)) != -1) {
    numEntries++;
    pos++;
  }

  // If we have too many entries, remove the oldest ones
  if (numEntries >= maxEntries) {
    int trimPos = data.indexOf(';') + 1;
    data = data.substring(trimPos);
  }

  // Append the new entry
  if (data.length() > 0) {
    data += ";";
  }
  data += newData;

  // Write the updated data back to the file
  file = SPIFFS.open("/data.txt", "w");
  if (file) {
    file.print(data);
    file.close();
  } else {
    logDebug("Failed to open data file for writing");
  }
}

With these updates, our smart fan controller is now a complete package. It monitors, it logs, it visualizes, and it gives you full control – all in one neat little system. Whether you’re using it for a server rack, a greenhouse, or anywhere else, this controller is ready to handle it all.

Oh hi there 👋
It’s nice to meet you.

Sign up to receive awesome content in your inbox

We don’t spam! Read our privacy policy for more info.

One comment on “Finalizing Our Smart Fan Controller: From Temperature Offset to Real-Time Data Logging”

Leave a Reply

Your email address will not be published. Required fields are marked *