TKK_E32231899/docs/esp32-firmware.ino

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);
}
}
}