#include #include #include #include #include #include #include #include #include // library "ESP32 URL Encode" by Alessandro Pezzato // ── KONFIGURASI HOTSPOT SETUP ── #define AP_SSID "Naga Terbang" #define AP_PASS "naga1234" // ── KONFIGURASI FIREBASE ── #define FIREBASE_HOST "monitoring-tanaman-d2cd2-default-rtdb.firebaseio.com" #define FIREBASE_AUTH "nl635pPMhf4y5tUlHRvEzBbG14oLyrD3m3vymjKt" // ── PIN PERANGKAT ── #define DHT_PIN 4 #define DHT_TYPE DHT22 #define SOIL_PIN 32 #define RELAY_PIN 26 #define FAN_PIN 27 // ← pin relay fan, sesuaikan jika berbeda // ── KALIBRASI SOIL MOISTURE ── #define SOIL_DRY 4095 #define SOIL_WET 0 // ── KALIBRASI DHT22 ── #define OFFSET_SUHU -1.1 #define OFFSET_KELEMBAPAN -10.4 // ── INTERVAL ── #define INTERVAL_SENSOR 60000 #define INTERVAL_HISTORY 60000 // ══════════════════════════════════════════════ // ── KONFIGURASI PENYIRAMAN ── // ══════════════════════════════════════════════ #define SOIL_MULAI_SIRAM 55 #define SOIL_STOP_SIRAM 65 #define DURASI_SIRAM_MS (3 * 60 * 1000) #define MAX_SIRAM_SENSOR 2 #define JAM_RESET 6 #define WINDOW_SENSOR_MULAI 10 #define WINDOW_SENSOR_SELESAI 13 // ── KONFIGURASI NOTIFIKASI WHATSAPP (Callmebot) ── // 1. Save +34 644 44 74 99 di kontak WhatsApp // 2. Kirim pesan: I allow callmebot to send me messages // 3. Isi nomor HP dan API key yang diterima #define WA_PHONE "628xxxxxxxxx" // nomor HP kamu format internasional tanpa + #define WA_APIKEY "xxxxxxx" // API key dari Callmebot // ── KONFIGURASI FAN (DHT22 sekarang deteksi suhu alat) ── #define BATAS_SUHU_FAN_ON 28.0 // fan ON jika suhu >= 28°C #define BATAS_SUHU_FAN_OFF 26.0 // fan OFF jika suhu <= 26°C const int jadwalTetap[] = {7, 8, 9, 14, 16}; const int TOTAL_JADWAL = sizeof(jadwalTetap) / sizeof(jadwalTetap[0]); // ══════════════════════════════════════════════ DHT dht(DHT_PIN, DHT_TYPE); FirebaseData fbdo; FirebaseData fbdoStream; FirebaseData fbdoStreamFan; FirebaseAuth auth; FirebaseConfig config; // ── STATE PENYIRAMAN ── bool pompaNyala = false; unsigned long pompaMulai = 0; bool jadwalSudahDieksekusi[5] = {false}; int jamTerakhirReset = -1; int jumlahSiramSensor = 0; int jumlahSiramJadwal = 0; // ── STATE FAN ── bool fanNyala = false; // -1 = AUTO, 0 = MANUAL OFF, 1 = MANUAL ON int fanOverride = -1; // ── FLAG ANTI RACE CONDITION ── bool sedangMenyalakan = false; // ── STATE OVERRIDE POMPA ── int manualOverride = -1; // ── INTERVAL ── unsigned long lastSensor = 0; unsigned long lastHistory = 0; // ── ANTI SPAM NOTIFIKASI ── bool notifSuhuTerkirim = false; // agar tidak kirim berulang unsigned long lastNotifWA = 0; #define INTERVAL_NOTIF_WA 300000 // minimal 5 menit antar notif // ══════════════════════════════════════════════ // KIRIM NOTIFIKASI WHATSAPP via Callmebot // ══════════════════════════════════════════════ void kirimNotifWA(String pesan) { if (WiFi.status() != WL_CONNECTED) return; String url = "https://api.callmebot.com/whatsapp.php?phone=" + String(WA_PHONE) + "&apikey=" + String(WA_APIKEY) + "&text=" + urlEncode(pesan); HTTPClient http; http.begin(url); int code = http.GET(); Serial.printf("📲 [WA] HTTP %d — %s\n", code, pesan.c_str()); http.end(); } // ══════════════════════════════════════════════ // FUNGSI RELAY POMPA // ══════════════════════════════════════════════ void nyalakanPompaJadwal(int idx) { if (pompaNyala || sedangMenyalakan) return; sedangMenyalakan = true; digitalWrite(RELAY_PIN, LOW); pompaNyala = true; pompaMulai = millis(); jumlahSiramJadwal++; jadwalSudahDieksekusi[idx] = true; sedangMenyalakan = false; Serial.printf("💧 [POMPA] ON — JADWAL %02d:00 | %d/%d\n", jadwalTetap[idx], jumlahSiramJadwal, TOTAL_JADWAL); } void nyalakanPompaSensor() { if (pompaNyala || sedangMenyalakan) return; if (jumlahSiramSensor >= MAX_SIRAM_SENSOR) { Serial.printf("🚫 [SENSOR] Ditolak — %d/%d\n", jumlahSiramSensor, MAX_SIRAM_SENSOR); return; } sedangMenyalakan = true; jumlahSiramSensor++; digitalWrite(RELAY_PIN, LOW); pompaNyala = true; pompaMulai = millis(); sedangMenyalakan = false; Serial.printf("💧 [POMPA] ON — SENSOR %d/%d\n", jumlahSiramSensor, MAX_SIRAM_SENSOR); } void matikanPompa() { if (!pompaNyala) return; digitalWrite(RELAY_PIN, HIGH); pompaNyala = false; Serial.println("🔴 [POMPA] OFF"); } // ══════════════════════════════════════════════ // FUNGSI FAN // ══════════════════════════════════════════════ void nyalakanFan() { if (fanNyala) return; digitalWrite(FAN_PIN, LOW); // active LOW relay fanNyala = true; Firebase.RTDB.setInt(&fbdo, "/fan", 1); Serial.println("🌀 [FAN] ON"); } void matikanFan() { if (!fanNyala) return; digitalWrite(FAN_PIN, HIGH); fanNyala = false; Firebase.RTDB.setInt(&fbdo, "/fan", 0); Serial.println("🔵 [FAN] OFF"); } // ══════════════════════════════════════════════ // STREAM CALLBACK POMPA (/relay) // ══════════════════════════════════════════════ void streamCallback(FirebaseStream data) { Serial.println("🔔 [RELAY] Stream — type: " + data.dataType()); int val = -1; if (data.dataType() == "int") val = data.intData(); else if (data.dataType() == "boolean") val = data.boolData() ? 1 : 0; else if (data.dataType() == "number") val = (int)data.floatData(); if (val == 1) { manualOverride = 1; if (!pompaNyala && !sedangMenyalakan) { sedangMenyalakan = true; digitalWrite(RELAY_PIN, LOW); pompaNyala = true; pompaMulai = millis(); sedangMenyalakan = false; Serial.println("💧 [RELAY] ON — MANUAL"); } } else if (val == 0) { manualOverride = 0; if (pompaNyala) { digitalWrite(RELAY_PIN, HIGH); pompaNyala = false; Serial.println("🔴 [RELAY] OFF — MANUAL"); } } else { manualOverride = -1; Serial.println("🤖 [RELAY] Mode AUTO"); } } // ══════════════════════════════════════════════ // STREAM CALLBACK FAN (/fan) // ══════════════════════════════════════════════ void fanStreamCallback(FirebaseStream data) { Serial.println("🔔 [FAN] Stream — type: " + data.dataType()); int val = -1; if (data.dataType() == "int") val = data.intData(); else if (data.dataType() == "boolean") val = data.boolData() ? 1 : 0; else if (data.dataType() == "number") val = (int)data.floatData(); Serial.printf("🔔 [FAN] Nilai dari Firebase: %d\n", val); if (val == 1) { fanOverride = 1; if (!fanNyala) { digitalWrite(FAN_PIN, LOW); fanNyala = true; Serial.println("🌀 [FAN] ON — MANUAL"); } } else if (val == 0) { fanOverride = 0; if (fanNyala) { digitalWrite(FAN_PIN, HIGH); fanNyala = false; Serial.println("🔵 [FAN] OFF — MANUAL"); } } else { fanOverride = -1; Serial.println("🤖 [FAN] Mode AUTO"); } } void streamTimeoutCallback(bool timeout) { if (timeout) Serial.println("⚠️ Stream timeout, retrying..."); } // ══════════════════════════════════════════════ // FUNGSI BANTU // ══════════════════════════════════════════════ String getTimestamp() { struct tm timeinfo; if (!getLocalTime(&timeinfo)) return "n/a"; char buf[25]; strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &timeinfo); return String(buf); } bool dalamWindowSensor(int jam) { return (jam >= WINDOW_SENSOR_MULAI && jam < WINDOW_SENSOR_SELESAI); } // ══════════════════════════════════════════════ // SETUP // ══════════════════════════════════════════════ void setup() { Serial.begin(115200); dht.begin(); pinMode(RELAY_PIN, OUTPUT); digitalWrite(RELAY_PIN, HIGH); pinMode(FAN_PIN, OUTPUT); digitalWrite(FAN_PIN, HIGH); WiFiManager wifiManager; wifiManager.setConnectTimeout(20); wifiManager.setConfigPortalTimeout(120); if (!wifiManager.autoConnect(AP_SSID, AP_PASS)) { Serial.println("⚠️ WiFi gagal. Restart..."); delay(3000); ESP.restart(); } Serial.println("✅ WiFi: " + WiFi.localIP().toString()); config.database_url = FIREBASE_HOST; config.signer.tokens.legacy_token = FIREBASE_AUTH; Firebase.begin(&config, &auth); Firebase.reconnectWiFi(true); // ── Sync /relay saat startup ── if (Firebase.RTDB.getInt(&fbdo, "/relay")) { int v = fbdo.intData(); if (v == 1) { manualOverride=1; digitalWrite(RELAY_PIN,LOW); pompaNyala=true; pompaMulai=millis(); Serial.println("💧 [STARTUP] Pompa ON"); } else if (v == 0) { manualOverride=0; Serial.println("🔴 [STARTUP] Pompa OFF"); } else { manualOverride=-1; Serial.println("🤖 [STARTUP] Pompa AUTO"); } } // ── Sync /fan saat startup ── if (Firebase.RTDB.getInt(&fbdo, "/fan")) { int v = fbdo.intData(); if (v == 1) { fanOverride=1; digitalWrite(FAN_PIN,LOW); fanNyala=true; Serial.println("🌀 [STARTUP] Fan ON"); } else if (v == 0) { fanOverride=0; Serial.println("🔵 [STARTUP] Fan OFF"); } else { fanOverride=-1; Serial.println("🤖 [STARTUP] Fan AUTO"); } } // ── Stream /relay ── if (!Firebase.RTDB.beginStream(&fbdoStream, "/relay")) Serial.println("Stream relay error: " + fbdoStream.errorReason()); Firebase.RTDB.setStreamCallback(&fbdoStream, streamCallback, streamTimeoutCallback); // ── Stream /fan ── if (!Firebase.RTDB.beginStream(&fbdoStreamFan, "/fan")) Serial.println("Stream fan error: " + fbdoStreamFan.errorReason()); Firebase.RTDB.setStreamCallback(&fbdoStreamFan, fanStreamCallback, streamTimeoutCallback); configTime(7 * 3600, 0, "pool.ntp.org", "time.nist.gov", "id.pool.ntp.org"); Serial.print("⏰ NTP sync"); struct tm timeinfo; int retry = 0; while (!getLocalTime(&timeinfo) && retry < 20) { delay(500); Serial.print("."); retry++; } Serial.println(getLocalTime(&timeinfo) ? "\n✅ NTP OK: " + getTimestamp() : "\n⚠️ NTP GAGAL"); Serial.println("══════════════════════════════"); Serial.printf("🌀 Fan ON jika suhu >= %.1f°C\n", BATAS_SUHU_FAN_ON); Serial.printf("🌀 Fan OFF jika suhu <= %.1f°C\n", BATAS_SUHU_FAN_OFF); Serial.printf("💧 Soil ON : <= %d%% | OFF : >= %d%%\n", SOIL_MULAI_SIRAM, SOIL_STOP_SIRAM); Serial.println("══════════════════════════════"); } // ══════════════════════════════════════════════ // LOOP // ══════════════════════════════════════════════ void loop() { unsigned long now = millis(); // Safety: matikan pompa jika melebihi durasi (AUTO) if (pompaNyala && manualOverride == -1 && (now - pompaMulai >= DURASI_SIRAM_MS)) { Serial.println("⏱️ [POMPA] Timeout — matikan"); matikanPompa(); } if (now - lastSensor >= INTERVAL_SENSOR) { lastSensor = now; // ── Baca Sensor ── float tRaw = dht.readTemperature(); float hRaw = dht.readHumidity(); int soilRaw = analogRead(SOIL_PIN); int soil = constrain(map(soilRaw, SOIL_DRY, SOIL_WET, 0, 100), 0, 100); if (isnan(tRaw) || isnan(hRaw)) Serial.println("⚠️ DHT22 error!"); float t = isnan(tRaw) ? 0 : constrain(tRaw + OFFSET_SUHU, -40.0, 80.0); float h = isnan(hRaw) ? 0 : constrain(hRaw + OFFSET_KELEMBAPAN, 0.0, 100.0); // ── Waktu ── struct tm tm; bool ntpOk = getLocalTime(&tm); int jam = ntpOk ? tm.tm_hour : -1; int menit = ntpOk ? tm.tm_min : 0; // ══ LOGIKA POMPA ══ if (manualOverride == 1) { if (!pompaNyala) { digitalWrite(RELAY_PIN, LOW); pompaNyala = true; } } else if (manualOverride == 0) { if (pompaNyala) matikanPompa(); } else { if (ntpOk && jam == JAM_RESET && menit == 0 && jamTerakhirReset != tm.tm_yday) { jamTerakhirReset = tm.tm_yday; jumlahSiramSensor = 0; jumlahSiramJadwal = 0; for (int i = 0; i < TOTAL_JADWAL; i++) jadwalSudahDieksekusi[i] = false; Serial.println("🔄 [RESET] Counter direset (06:00)"); } if (ntpOk && !dalamWindowSensor(jam)) { for (int i = 0; i < TOTAL_JADWAL; i++) if (jam == jadwalTetap[i] && !jadwalSudahDieksekusi[i]) nyalakanPompaJadwal(i); } if (ntpOk && dalamWindowSensor(jam)) { if (soil <= SOIL_MULAI_SIRAM && !pompaNyala) nyalakanPompaSensor(); if (soil >= SOIL_STOP_SIRAM && pompaNyala) matikanPompa(); } } // ══ NOTIFIKASI WHATSAPP ══ unsigned long nowMs = millis(); if (t >= BATAS_SUHU_FAN_ON) { // kirim notif hanya sekali saat kondisi berubah ATAU sudah > 5 menit if (!notifSuhuTerkirim || (nowMs - lastNotifWA >= INTERVAL_NOTIF_WA)) { String pesan = "⚠️ *HIDROGENZ FARM ALERT*\n" "🌡️ Suhu box perangkat terlalu panas!\n" "Suhu saat ini: " + String(t, 1) + "°C\n" "Batas normal : < " + String(BATAS_SUHU_FAN_ON, 0) + "°C\n" "Fan otomatis : " + String(fanNyala ? "ON ✅" : "OFF ❌") + "\n" "Waktu : " + getTimestamp(); kirimNotifWA(pesan); notifSuhuTerkirim = true; lastNotifWA = nowMs; } } else if (t <= BATAS_SUHU_FAN_OFF) { // reset flag saat suhu sudah normal kembali if (notifSuhuTerkirim) { notifSuhuTerkirim = false; Serial.println("✅ [WA] Suhu normal kembali — flag reset"); } } // ══ LOGIKA FAN ══ if (fanOverride == 1) { // MANUAL ON — pastikan tetap nyala if (!fanNyala) { digitalWrite(FAN_PIN, LOW); fanNyala = true; } } else if (fanOverride == 0) { // MANUAL OFF — pastikan tetap mati if (fanNyala) { digitalWrite(FAN_PIN, HIGH); fanNyala = false; } } else { // AUTO: ON jika suhu >= SUHU_FAN_ON, OFF jika suhu <= SUHU_FAN_OFF (hysteresis) if (t >= BATAS_SUHU_FAN_ON && !fanNyala) { nyalakanFan(); } else if (t <= BATAS_SUHU_FAN_OFF && fanNyala) { matikanFan(); } } // ── Log Serial ── String ts = getTimestamp(); Serial.printf("✅ T:%.1f°C H:%.1f%% Soil:%d%% Pompa:%s Fan:%s Mode-P:%s Mode-F:%s ⏰%s\n", t, h, soil, pompaNyala ? "ON" : "OFF", fanNyala ? "ON" : "OFF", manualOverride== 1?"MAN-ON": manualOverride==0?"MAN-OFF":"AUTO", fanOverride == 1?"MAN-ON": fanOverride ==0?"MAN-OFF":"AUTO", ts.c_str()); // ── Firebase latest ── if (Firebase.ready()) { FirebaseJson jsonLatest; jsonLatest.set("suhu", t); jsonLatest.set("kelembapan", h); jsonLatest.set("soil", soil); jsonLatest.set("pompa", pompaNyala ? "ON" : "OFF"); jsonLatest.set("fan", fanNyala ? "ON" : "OFF"); jsonLatest.set("siram_jadwal", jumlahSiramJadwal); jsonLatest.set("siram_sensor", jumlahSiramSensor); jsonLatest.set("updated_at", ts); Firebase.RTDB.setJSON(&fbdo, "/sensor/latest", &jsonLatest); } // ── Firebase history ── if (Firebase.ready() && (now - lastHistory >= INTERVAL_HISTORY)) { lastHistory = now; FirebaseJson jsonHistory; jsonHistory.set("suhu", t); jsonHistory.set("kelembapan", h); jsonHistory.set("soil", soil); jsonHistory.set("pompa", pompaNyala ? "ON" : "OFF"); jsonHistory.set("fan", fanNyala ? "ON" : "OFF"); jsonHistory.set("updated_at", ts); jsonHistory.set("created_at", ts); Firebase.RTDB.pushJSON(&fbdo, "/sensor/history", &jsonHistory); Serial.println("📦 History saved: " + ts); } } }