436 lines
17 KiB
C++
436 lines
17 KiB
C++
#include <WiFi.h>
|
|
#include <WiFiManager.h>
|
|
#include <Firebase_ESP_Client.h>
|
|
#include <addons/TokenHelper.h>
|
|
#include <addons/RTDBHelper.h>
|
|
#include <DHT.h>
|
|
#include <time.h>
|
|
#include <HTTPClient.h>
|
|
#include <UrlEncode.h> // 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);
|
|
}
|
|
}
|
|
}
|