#include <WiFi.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
// WiFi credentials
const char* ssid = "Wokwi-GUEST";
const char* password = ""; // Usually empty for Wokwi-GUEST
// MQTT broker
const char* mqtt_server = "broker.hivemq.com"; // Public broker, works well
const int mqtt_port = 1883; // Standard MQTT port
// MQTT topics
const char* tempHumTopic = "iot/sensor/temperature_humidity";
const char* weatherTopic = "iot/weather";
const char* fanControlTopic = "iot/fan/control";
const char* alertTopic = "iot/alert";
const char* thresholdControlTopic = "iot/threshold/set";
const char* sensorSelectTopic = "iot/threshold/sensor_select";
// OpenWeatherMap API details
const char* openWeatherMapApiKey = "f63003e055f21f1953b1186b7c7fc142"; // Your OpenWeatherMap API Key
const char* weatherCity = "Cape Town"; // City for weather lookup
const char* weatherUnits = "metric"; // or "imperial"
// DHT22 sensor
#define DHTPIN 15
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
// Pins
const int thermistorPin = 34; // Analog pin for thermistor
const int buzzerPin = 26; // Digital pin for buzzer
const int fanPin = 2; // Digital pin for fan
// Temperature threshold and sensor selection
float tempThreshold = 20.0; // Default threshold
String selectedSensorForThreshold = "dht"; // Default sensor ("dht" or "thermistor")
// Weather deviation threshold
const float weatherDeviationThreshold = 5.0; // Degrees Celsius difference to trigger an alert
// WiFi and MQTT clients
WiFiClient espClient;
PubSubClient client(espClient);
// Timers for non-blocking operations
unsigned long lastSensorReadMillis = 0;
const long sensorReadInterval = 10000; // Read sensors every 10 seconds
unsigned long lastWeatherFetchMillis = 0;
// *** IMPORTANT CHANGE FOR TESTING: Reduced weather fetch interval to 30 seconds ***
const long weatherFetchInterval = 30000; // Fetch weather every 30 seconds (30000 ms)
void setup() {
Serial.begin(115200); // Initialize serial communication for debugging
Serial.println("\n--- ESP32 Startup ---"); // Added startup message
setup_wifi(); // Connect to WiFi
client.setServer(mqtt_server, mqtt_port); // Configure MQTT broker
client.setCallback(callback); // Set the MQTT message callback function
dht.begin(); // Initialize DHT sensor
pinMode(thermistorPin, INPUT); // Set thermistor pin as input
pinMode(buzzerPin, OUTPUT); // Set buzzer pin as output
pinMode(fanPin, OUTPUT); // Set fan pin as output
Serial.println("ESP32 setup complete. Entering loop...");
}
void setup_wifi() {
delay(10);
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password); // Start WiFi connection
unsigned long wifiConnectStart = millis();
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
if (millis() - wifiConnectStart > 30000) { // Timeout after 30 seconds
Serial.println("\nWiFi connection timed out! Retrying...");
wifiConnectStart = millis(); // Reset timer and retry
WiFi.begin(ssid, password); // Try connecting again
}
}
Serial.println("\nWiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
void reconnect() {
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
// Create a unique client ID based on MAC address
String clientId = "ESP32Client-" + String(WiFi.macAddress());
// Attempt to connect to MQTT broker
if (client.connect(clientId.c_str())) { // No username/password needed for HiveMQ public broker
Serial.println("connected");
// Subscribe to control topics
client.subscribe(fanControlTopic);
Serial.print("Subscribed to: ");
Serial.println(fanControlTopic);
client.subscribe(thresholdControlTopic);
Serial.print("Subscribed to: ");
Serial.println(thresholdControlTopic);
client.subscribe(sensorSelectTopic);
Serial.print("Subscribed to: ");
Serial.println(sensorSelectTopic);
} else {
Serial.print("failed, rc=");
Serial.print(client.state()); // Print MQTT connection state code
Serial.println(" trying again in 5 seconds");
delay(5000); // Wait before retrying
}
}
}
// Callback function for incoming MQTT messages
void callback(char* topic, byte* payload, unsigned int length) {
String message;
for (unsigned int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.print("MQTT message arrived on topic: ");
Serial.print(topic);
Serial.print(". Message: ");
Serial.println(message);
if (String(topic) == fanControlTopic) {
int fanState = message.toInt();
digitalWrite(fanPin, fanState); // Control fan (HIGH/LOW)
Serial.print("Fan state set to: ");
Serial.println(fanState == HIGH ? "ON" : "OFF");
} else if (String(topic) == thresholdControlTopic) {
tempThreshold = message.toFloat(); // Update temperature threshold
Serial.print("Temperature threshold updated to: ");
Serial.print(tempThreshold, 1); // Print with 1 decimal place
Serial.println("°C");
} else if (String(topic) == sensorSelectTopic) {
selectedSensorForThreshold = message; // Update selected sensor for threshold
Serial.print("Selected sensor for threshold: ");
Serial.println(selectedSensorForThreshold);
}
}
// Function to send WhatsApp alerts via CallMeBot API
void sendWhatsAppAlert(String message) {
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
// Construct the CallMeBot API URL
String url = "https://api.callmebot.com/whatsapp.php?phone=27786480295&text=" + // UPDATED PHONE NUMBER
urlencode(message) + // URL-encode the message
"&apikey=9303714"; // UPDATED API KEY
http.begin(url);
int httpCode = http.GET(); // Send HTTP GET request
if (httpCode > 0) {
Serial.print("WhatsApp sent, HTTP code: ");
Serial.println(httpCode);
} else {
Serial.print("Failed to send WhatsApp. Error: ");
Serial.println(http.errorToString(httpCode)); // Print HTTP error
}
http.end(); // Close connection
} else {
Serial.println("WiFi not connected. Cannot send WhatsApp alert.");
}
}
// Helper function to URL-encode strings
String urlencode(String str) {
String encodedString = "";
char c;
char code0;
char code1;
for (int i = 0; i < str.length(); i++) {
c = str.charAt(i);
if (isalnum(c)) { // If alphanumeric, append directly
encodedString += c;
} else if (c == ' ') { // Replace space with '+'
encodedString += '+';
} else { // Encode other characters
code0 = (c >> 4) & 0xF;
code1 = c & 0xF;
encodedString += '%';
encodedString += char(code0 > 9 ? code0 - 10 + 'A' : code0 + '0');
encodedString += char(code1 > 9 ? code1 - 10 + 'A' : code1 + '0');
}
}
return encodedString;
}
// Function to read sensor data and publish to MQTT
void readSensors() {
Serial.println("\n--- Reading Local Sensor Data ---");
float dht_t = dht.readTemperature(); // Read temperature from DHT22
float dht_h = dht.readHumidity(); // Read humidity from DHT22
if (isnan(dht_h) || isnan(dht_t)) {
Serial.println("Warning: Failed to read from DHT sensor! Data might be NaN.");
// Continue attempting thermistor read
}
// Read thermistor data (analog)
int analogValue = analogRead(thermistorPin);
float voltage = analogValue * 3.3 / 4095.0; // ESP32 ADC is 12-bit (0-4095) for 3.3V
float resistance = (3.3 - voltage) * 10000.0 / voltage; // Assuming 10kOhm series resistor
// Steinhart-Hart equation constants for thermistor
const float B_PARAM = 3950.0;
const float R0 = 10000.0; // Resistance at 25°C
const float T0_KELVIN = 25.0 + 273.15; // Reference temperature in Kelvin
// Calculate thermistor temperature in Celsius
float thermistor_t = 1 / (log(resistance / R0) / B_PARAM + 1 / T0_KELVIN) - 273.15;
// Create JSON document for sensor readings
StaticJsonDocument<200> doc; // Adjust size if more data is added
doc["dht_temp_c"] = dht_t;
doc["dht_humidity"] = dht_h;
doc["thermistor_temp_c"] = thermistor_t;
String jsonPayload;
serializeJson(doc, jsonPayload); // Serialize JSON to string
client.publish(tempHumTopic, jsonPayload.c_str()); // Publish sensor data
Serial.println("Published Sensor Data: " + jsonPayload);
// Determine which temperature sensor to use for threshold comparison
float currentTempForThreshold = 0.0;
String sensorNameForThreshold = "";
if (selectedSensorForThreshold == "dht" && !isnan(dht_t)) {
currentTempForThreshold = dht_t;
sensorNameForThreshold = "DHT22";
} else if (selectedSensorForThreshold == "thermistor") { // Thermistor is always a valid float unless calculation goes wrong
currentTempForThreshold = thermistor_t;
sensorNameForThreshold = "Thermistor";
} else {
// Fallback if selected sensor failed or is invalid, try DHT first
Serial.println("Selected sensor reading invalid or failed. Attempting fallback...");
if (!isnan(dht_t)) {
currentTempForThreshold = dht_t;
sensorNameForThreshold = "DHT22 (Fallback)";
} else {
Serial.println("Error: No valid sensor data for threshold comparison available.");
return; // Exit if no valid sensor data
}
}
// Alert or OK logic based on selected sensor and threshold
if (currentTempForThreshold > tempThreshold) {
digitalWrite(buzzerPin, HIGH); // Turn on buzzer
digitalWrite(fanPin, HIGH); // Turn on fan
String alertMsg = "ALERT: Temperature threshold exceeded! (" + sensorNameForThreshold + ": " + String(currentTempForThreshold, 1) + "°C > " + String(tempThreshold, 1) + "°C)";
client.publish(alertTopic, alertMsg.c_str()); // Publish alert to MQTT
Serial.println(alertMsg);
String whatsappAlertMsg = "⚠️ Temp Alert (" + sensorNameForThreshold + ")\n" + String(currentTempForThreshold, 1) + "°C > " + String(tempThreshold, 1) + "°C";
sendWhatsAppAlert(whatsappAlertMsg); // Send WhatsApp alert
} else {
digitalWrite(buzzerPin, LOW); // Turn off buzzer
digitalWrite(fanPin, LOW); // Turn off fan
String okMessage = "STATUS: Temperature is normal. (" + sensorNameForThreshold + ": " + String(currentTempForThreshold, 1) + "°C <= " + String(tempThreshold, 1) + "°C)";
client.publish(alertTopic, okMessage.c_str()); // Publish status to MQTT
Serial.println(okMessage);
// Optionally send WhatsApp OK message. Be mindful of spamming.
// sendWhatsAppAlert(okMessage);
}
}
// Function to fetch weather data from OpenWeatherMap API and publish to MQTT
void fetchWeatherData() {
Serial.println("\n--- Fetching External Weather Data ---");
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
// Construct the OpenWeatherMap API URL
String openWeatherMapUrl = "http://api.openweathermap.org/data/2.5/weather?q=" +
urlencode(weatherCity) + // URL-encode city name
"&appid=" + openWeatherMapApiKey +
"&units=" + weatherUnits;
Serial.print("OpenWeatherMap URL: ");
Serial.println(openWeatherMapUrl);
http.begin(openWeatherMapUrl);
int httpCode = http.GET(); // Send HTTP GET request
if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
String payload = http.getString(); // Get response payload
Serial.print("OpenWeatherMap HTTP Code: ");
Serial.println(httpCode);
Serial.println("OpenWeatherMap Raw Response (truncated if very long):"); // Added truncation note
Serial.println(payload); // Print the raw JSON response
// Create JSON document for weather data parsing
// Use ArduinoJson Assistant for optimal size: https://arduinojson.org/v7/assistant/
// A size of 1024 is generally sufficient for standard weather responses.
StaticJsonDocument<1024> doc;
// Deserialize JSON payload
DeserializationError error = deserializeJson(doc, payload);
if (!error) {
// Extract weather data from the JSON
float api_temp = doc["main"]["temp"];
const char* city_name = doc["name"];
float api_humidity = doc["main"]["humidity"];
float wind_speed = doc["wind"]["speed"];
int clouds_all = doc["clouds"]["all"];
const char* weather_description = doc["weather"][0]["description"]; // More descriptive
float lon = doc["coord"]["lon"]; // Longitude for map
float lat = doc["coord"]["lat"]; // Latitude for map
// Create a separate JSON document for publishing to MQTT
StaticJsonDocument<256> weatherDoc; // Smaller, optimized for publishing
weatherDoc["city"] = city_name;
weatherDoc["temp"] = api_temp;
weatherDoc["humidity"] = api_humidity;
weatherDoc["wind_speed"] = wind_speed;
weatherDoc["clouds"] = clouds_all;
weatherDoc["weather"] = weather_description;
weatherDoc["lon"] = lon;
weatherDoc["lat"] = lat;
String weatherJson;
serializeJson(weatherDoc, weatherJson); // Serialize weather data to string
client.publish(weatherTopic, weatherJson.c_str()); // Publish weather data
Serial.println("Published Weather Data to MQTT: " + weatherJson);
Serial.println("Weather API Temp: " + String(api_temp, 1) + "°C");
// --- Compare local sensor temperature with weather API data ---
// Re-read sensors for the most up-to-date comparison
float dht_t = dht.readTemperature();
float thermistor_t = 0.0;
int analogValue = analogRead(thermistorPin);
float voltage = analogValue * 3.3 / 4095.0;
float resistance = (3.3 - voltage) * 10000.0 / voltage;
const float B_PARAM = 3950.0;
const float R0 = 10000.0;
const float T0_KELVIN = 25.0 + 273.15;
thermistor_t = 1 / (log(resistance / R0) / B_PARAM + 1 / T0_KELVIN) - 273.15;
// Determine which local sensor temperature is closer to the API temperature for deviation check
float sensor_temp_for_comparison = NAN; // Initialize as Not-A-Number
String sensor_name_for_comparison = "N/A";
bool dht_valid = !isnan(dht_t);
bool thermistor_valid = true; // Assuming thermistor always provides a valid number
if (dht_valid && thermistor_valid) {
// Both are valid, pick the one closer to API temp
if (fabs(thermistor_t - api_temp) < fabs(dht_t - api_temp)) {
sensor_temp_for_comparison = thermistor_t;
sensor_name_for_comparison = "Thermistor";
} else {
sensor_temp_for_comparison = dht_t;
sensor_name_for_comparison = "DHT22";
}
} else if (dht_valid) {
sensor_temp_for_comparison = dht_t;
sensor_name_for_comparison = "DHT22";
} else if (thermistor_valid) {
sensor_temp_for_comparison = thermistor_t;
sensor_name_for_comparison = "Thermistor";
} else {
Serial.println("Warning: No valid local sensor temperature for weather deviation comparison.");
// No comparison possible, so no alert for deviation
http.end();
return;
}
float deviation = fabs(sensor_temp_for_comparison - api_temp);
Serial.print("Local Sensor (" + sensor_name_for_comparison + ") Temp: ");
Serial.print(sensor_temp_for_comparison, 1);
Serial.println("°C");
Serial.print("Deviation from API: ");
Serial.print(deviation, 1);
Serial.print("°C (Threshold: ");
Serial.print(weatherDeviationThreshold, 1);
Serial.println("°C)");
if (deviation > weatherDeviationThreshold) {
String weatherAlertMsg = "ALERT: Significant deviation between sensor and weather API! (" + sensor_name_for_comparison + ": " + String(sensor_temp_for_comparison, 1) + "°C, API: " + String(api_temp, 1) + "°C, Deviation: " + String(deviation, 1) + "°C)";
client.publish(alertTopic, weatherAlertMsg.c_str()); // Publish alert to MQTT
Serial.println(weatherAlertMsg);
sendWhatsAppAlert("⚠️ Weather Deviation Alert!\nSensor (" + sensor_name_for_comparison + "): " + String(sensor_temp_for_comparison, 1) + "°C\nAPI: " + String(api_temp, 1) + "°C");
} else {
String weatherOkMsg = "STATUS: Sensor and weather API temperatures are in sync. (" + sensor_name_for_comparison + ": " + String(sensor_temp_for_comparison, 1) + "°C, API: " + String(api_temp, 1) + "°C, Deviation: " + String(deviation, 1) + "°C)";
client.publish(alertTopic, weatherOkMsg.c_str()); // Publish status to MQTT
Serial.println(weatherOkMsg);
}
} else {
Serial.print(F("Error: deserializeJson() failed for weather data: "));
Serial.println(error.f_str()); // Print detailed JSON parsing error
Serial.println("This could mean the JSON response was too large for the buffer, or malformed.");
}
} else {
Serial.print("Error on HTTP GET request to OpenWeatherMap: ");
Serial.println(httpCode); // Print HTTP error code
Serial.println("Error Description: " + http.errorToString(httpCode)); // Print HTTP error description
Serial.println("Common reasons: 401 Unauthorized (API key), -1 (connection issue/DNS).");
}
http.end(); // Close HTTP connection to free up resources
} else {
Serial.println("WiFi not connected. Cannot fetch weather data.");
}
Serial.println("--- Finished Weather Data Fetch ---");
}
void loop() {
// Ensure MQTT connection is maintained
if (!client.connected()) {
reconnect();
}
client.loop(); // Must be called frequently to process MQTT messages and maintain connection
unsigned long currentMillis = millis(); // Get current time
// Read sensors at defined interval
if (currentMillis - lastSensorReadMillis >= sensorReadInterval) {
lastSensorReadMillis = currentMillis;
readSensors();
}
// Fetch weather data at defined interval
if (currentMillis - lastWeatherFetchMillis >= weatherFetchInterval) {
lastWeatherFetchMillis = currentMillis;
fetchWeatherData();
}
// A small delay to prevent the watchdog timer from resetting the ESP32 if the loop runs too fast
delay(10);
}