// ========== LIBRARY ========== #include #include #include #include #include #include #include #include #include // ========== KONFIGURASI ========== const char* mqtt_server = "broker.emqx.io"; const int mqtt_port = 1883; const char* ntpServer = "pool.ntp.org"; const long gmtOffset_sec = 7 * 3600; const int daylightOffset_sec = 0; const char* topic_data = "aquamonn/data"; const char* topic_relay = "aquamonn/relay"; const char* topic_relay_status = "aquamonn/relay_status"; const char* topic_lampu = "aquamonn/lampu"; const char* topic_schedule = "aquamonn/schedule"; const char* topic_lampu_status = "aquamonn/lampu_status"; #define PH_SENSOR_PIN 34 #define ONE_WIRE_BUS 23 #define RELAY_LAMPU 25 #define RELAY_POMPA 26 #define EEPROM_SIZE 64 #define EEPROM_MAGIC_NUMBER 0xA5 #define EEPROM_ADDR_MAGIC 0 #define EEPROM_ADDR_AUTO_MODE 1 #define EEPROM_ADDR_ON_HOUR 2 #define EEPROM_ADDR_ON_MINUTE 3 #define EEPROM_ADDR_OFF_HOUR 4 #define EEPROM_ADDR_OFF_MINUTE 5 #define INTERVAL_SENSOR 2000 #define INTERVAL_NTP_SYNC 3600000 // Kalibrasi pH float V0 = 3.15, V7 = 2.60, V10 = 2.06; // ========== OBJEK ========== OneWire oneWire(ONE_WIRE_BUS); DallasTemperature sensors(&oneWire); WiFiClient espClient; PubSubClient client(espClient); // ========== VARIABEL ========== float suhu = 25.0, pH = 7.0, amonia = 0.0; float voltPh = 0.0; bool pompaAktif = false, lampuAktif = false, pompaAutoMode = true; float batasAmonia = 2.0; bool lampuAutoMode = true; int lampuOnHour = 6, lampuOnMinute = 0, lampuOffHour = 18, lampuOffMinute = 0; unsigned long lastSensorRead = 0, lastMQTT = 0, lastNTPSync = 0; bool ntpSynced = false, scheduleChanged = false; unsigned long lastScheduleChange = 0; // ========== EEPROM ========== void saveToEEPROM() { EEPROM.begin(EEPROM_SIZE); EEPROM.write(EEPROM_ADDR_MAGIC, EEPROM_MAGIC_NUMBER); EEPROM.write(EEPROM_ADDR_AUTO_MODE, lampuAutoMode ? 1 : 0); EEPROM.write(EEPROM_ADDR_ON_HOUR, lampuOnHour); EEPROM.write(EEPROM_ADDR_ON_MINUTE, lampuOnMinute); EEPROM.write(EEPROM_ADDR_OFF_HOUR, lampuOffHour); EEPROM.write(EEPROM_ADDR_OFF_MINUTE, lampuOffMinute); EEPROM.commit(); EEPROM.end(); Serial.println("✅ Jadwal tersimpan"); } void loadFromEEPROM() { EEPROM.begin(EEPROM_SIZE); if (EEPROM.read(EEPROM_ADDR_MAGIC) == EEPROM_MAGIC_NUMBER) { lampuAutoMode = EEPROM.read(EEPROM_ADDR_AUTO_MODE); lampuOnHour = EEPROM.read(EEPROM_ADDR_ON_HOUR); lampuOnMinute = EEPROM.read(EEPROM_ADDR_ON_MINUTE); lampuOffHour = EEPROM.read(EEPROM_ADDR_OFF_HOUR); lampuOffMinute = EEPROM.read(EEPROM_ADDR_OFF_MINUTE); } else resetToDefaultSchedule(); EEPROM.end(); } void resetToDefaultSchedule() { lampuAutoMode = true; lampuOnHour = 6; lampuOnMinute = 0; lampuOffHour = 18; lampuOffMinute = 0; saveToEEPROM(); } // ========== WIFI & NTP ========== void setupWiFiManager() { WiFiManager wm; wm.setConfigPortalTimeout(180); if (!wm.autoConnect("ESP32_Aquamonn", "12345678")) { Serial.println("❌ Gagal konek WiFi! Restart..."); delay(5000); ESP.restart(); } } bool isTimeValid() { struct tm timeinfo; if (!getLocalTime(&timeinfo, 1000)) return false; return (timeinfo.tm_year + 1900) >= 2024; } void initNTP() { Serial.print("🕐 NTP Syncing..."); configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); int attempts = 0; while (!isTimeValid() && attempts++ < 30) { Serial.print("."); delay(1000); } ntpSynced = isTimeValid(); Serial.println(ntpSynced ? " ✅" : " ⚠️ Failed"); if (ntpSynced) printWaktuSekarang(); } void syncNTP() { if (millis() - lastNTPSync > INTERVAL_NTP_SYNC) { lastNTPSync = millis(); if (!isTimeValid()) { configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); delay(2000); ntpSynced = isTimeValid(); } } } String getWaktuString() { struct tm timeinfo; if (!getLocalTime(&timeinfo) || !isTimeValid()) return "00:00:00"; char buf[20]; sprintf(buf, "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); return String(buf); } String getTanggalString() { struct tm timeinfo; if (!getLocalTime(&timeinfo) || !isTimeValid()) return "0000-00-00"; char buf[20]; sprintf(buf, "%04d-%02d-%02d", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday); return String(buf); } int getCurrentHour() { struct tm timeinfo; if (!getLocalTime(&timeinfo) || !isTimeValid()) return 0; return timeinfo.tm_hour; } int getCurrentMinute() { struct tm timeinfo; if (!getLocalTime(&timeinfo) || !isTimeValid()) return 0; return timeinfo.tm_min; } void printWaktuSekarang() { struct tm timeinfo; if (getLocalTime(&timeinfo) && isTimeValid()) { Serial.printf(" 📅 %02d:%02d:%02d - %04d-%02d-%02d\n", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday); } } // ========== MQTT ========== void connectMQTT() { while (!client.connected()) { String clientId = "ESP32_Aquamonn_" + String(WiFi.macAddress()); clientId.replace(":", ""); Serial.printf("🔌 MQTT connecting...\n"); if (client.connect(clientId.c_str())) { Serial.println("✅ MQTT Terhubung!"); client.subscribe(topic_relay); client.subscribe(topic_lampu); client.subscribe(topic_schedule); kirimStatusPompa(); kirimStatusLampu(); kirimData(); } else { Serial.printf("❌ Gagal, state=%d, coba lagi 5dtk\n", client.state()); delay(5000); } } } // ========== FUNGSI UTAMA ========== void kontrolLampuBySchedule() { if (!lampuAutoMode || !isTimeValid()) return; int currentTotalMinutes = getCurrentHour() * 60 + getCurrentMinute(); int onTotalMinutes = lampuOnHour * 60 + lampuOnMinute; int offTotalMinutes = lampuOffHour * 60 + lampuOffMinute; bool seharusnyaNyala = (onTotalMinutes < offTotalMinutes) ? (currentTotalMinutes >= onTotalMinutes && currentTotalMinutes < offTotalMinutes) : (currentTotalMinutes >= onTotalMinutes || currentTotalMinutes < offTotalMinutes); if (seharusnyaNyala != lampuAktif) { digitalWrite(RELAY_LAMPU, seharusnyaNyala ? LOW : HIGH); lampuAktif = seharusnyaNyala; Serial.printf("💡 Lampu: %s (schedule)\n", lampuAktif ? "ON" : "OFF"); kirimStatusLampu(); } } void setLampu(String command) { if (command == "ON" || command == "1") { digitalWrite(RELAY_LAMPU, LOW); lampuAktif = true; lampuAutoMode = false; } else if (command == "OFF" || command == "0") { digitalWrite(RELAY_LAMPU, HIGH); lampuAktif = false; lampuAutoMode = false; } saveToEEPROM(); kirimStatusLampu(); Serial.printf("💡 Lampu: %s (manual)\n", lampuAktif ? "ON" : "OFF"); } void setLampuSchedule(String payload) { StaticJsonDocument<200> doc; if (deserializeJson(doc, payload)) { Serial.println("❌ JSON parse error"); return; } bool needSave = false; if (doc.containsKey("auto_mode")) { lampuAutoMode = doc["auto_mode"]; needSave = true; } if (lampuAutoMode) { if (doc.containsKey("on_hour")) { lampuOnHour = doc["on_hour"]; needSave = true; } if (doc.containsKey("on_minute")) { lampuOnMinute = doc["on_minute"]; needSave = true; } if (doc.containsKey("off_hour")) { lampuOffHour = doc["off_hour"]; needSave = true; } if (doc.containsKey("off_minute")) { lampuOffMinute = doc["off_minute"]; needSave = true; } } if (needSave) { saveToEEPROM(); scheduleChanged = true; lastScheduleChange = millis(); kontrolLampuBySchedule(); Serial.printf("📅 Jadwal: %02d:%02d - %02d:%02d\n", lampuOnHour, lampuOnMinute, lampuOffHour, lampuOffMinute); } kirimStatusLampu(); } void kontrolPompa() { if (!pompaAutoMode) return; bool seharusnyaNyala = (amonia > batasAmonia); if (seharusnyaNyala != pompaAktif) { digitalWrite(RELAY_POMPA, seharusnyaNyala ? LOW : HIGH); pompaAktif = seharusnyaNyala; Serial.printf("💧 Pompa: %s (NH₃: %.2f%%)\n", pompaAktif ? "ON" : "OFF", amonia); kirimStatusPompa(); } } void setPompaConfig(String payload) { StaticJsonDocument<200> doc; if (deserializeJson(doc, payload)) return; if (doc.containsKey("pompa_auto_mode")) pompaAutoMode = doc["pompa_auto_mode"]; if (doc.containsKey("batas_amonia")) batasAmonia = doc["batas_amonia"]; if (!pompaAutoMode && doc.containsKey("pompa_command")) { String cmd = doc["pompa_command"]; digitalWrite(RELAY_POMPA, cmd == "ON" ? LOW : HIGH); pompaAktif = (cmd == "ON"); kirimStatusPompa(); } } void kirimStatusLampu() { StaticJsonDocument<200> doc; doc["lampu_status"] = lampuAktif ? "ON" : "OFF"; doc["lampu_motime"] = String(lampuOnHour) + ":" + String(lampuOnMinute); doc["lampu_off_time"] = String(lampuOffHour) + ":" + String(lampuOffMinute); doc["ntp_synced"] = ntpSynced && isTimeValid(); char buffer[200]; serializeJson(doc, buffer); client.publish(topic_lampu_status, buffer); } void kirimStatusPompa() { StaticJsonDocument<150> doc; doc["relay"] = pompaAktif ? 1 : 0; doc["relay_status"] = pompaAktif ? "ON" : "OFF"; doc["pompa_mode"] = pompaAutoMode ? "AUTO" : "MANUAL"; doc["batas_amonia"] = batasAmonia; char buffer[150]; serializeJson(doc, buffer); client.publish(topic_relay_status, buffer); } // ========== SENSOR ========== void bacaSensor() { // Baca suhu sensors.requestTemperatures(); float temp = sensors.getTempCByIndex(0); suhu = (temp != DEVICE_DISCONNECTED_C && !isnan(temp)) ? temp : 25.0; // Baca pH int raw = analogRead(PH_SENSOR_PIN); voltPh = raw * (3.3 / 4095.0); pH = (voltPh >= V7) ? 7.0 + ((voltPh - V7) / (V7 - V0)) * 3.0 : 7.0 + ((voltPh - V7) / (V10 - V7)) * 3.0; pH = constrain(pH, 0, 14); amonia = constrain((pH < 6.5) ? (pH / suhu) * 0.202 : (pH >= 6.5 && pH <= 7.0) ? (pH / suhu) * 1.131 : (pH / suhu) * 3.306, 0, 10); } void kirimData() { if (!client.connected()) return; StaticJsonDocument<350> doc; doc["suhu"] = suhu; doc["pH"] = pH; doc["amonia"] = amonia; // Persentase NH₃ (%) doc["volt_ph"] = voltPh; doc["relay"] = pompaAktif ? 1 : 0; doc["lampu"] = lampuAktif ? 1 : 0; doc["batas_amonia"] = batasAmonia; doc["lampu_mode"] = lampuAutoMode ? "AUTO" : "MANUAL"; doc["lampu_on_time"] = String(lampuOnHour) + ":" + String(lampuOnMinute); doc["lampu_off_time"] = String(lampuOffHour) + ":" + String(lampuOffMinute); doc["jam"] = getWaktuString(); doc["tanggal"] = getTanggalString(); doc["ntp_synced"] = ntpSynced && isTimeValid(); char buffer[400]; serializeJson(doc, buffer); if (client.publish(topic_data, buffer)) { Serial.println("\n─────────────────────────────────────────"); Serial.printf("📅 %s\n", getWaktuString().c_str()); Serial.printf("🌡️ %.1f°C | 🔬 %.2f | 💊 NH₃: %.2f mg/L\n", suhu, pH, amonia); Serial.printf("💧 %s | 💡 %s (%s)\n", pompaAktif ? "ON" : "OFF", lampuAktif ? "ON" : "OFF", lampuAutoMode ? "AUTO" : "MANUAL"); Serial.printf("📅 Jadwal: %02d:%02d - %02d:%02d\n", lampuOnHour, lampuOnMinute, lampuOffHour, lampuOffMinute); Serial.println("─────────────────────────────────────────"); } else { Serial.println("❌ Gagal publish data!"); } } // ========== MQTT CALLBACK ========== void callback(char* topic, byte* payload, unsigned int length) { payload[length] = '\0'; String message = String((char*)payload); Serial.printf("📨 %s: %s\n", topic, message.c_str()); if (strcmp(topic, topic_relay) == 0) setPompaConfig(message); else if (strcmp(topic, topic_lampu) == 0) setLampu(message); else if (strcmp(topic, topic_schedule) == 0) setLampuSchedule(message); } // ========== SETUP ========== void setup() { Serial.begin(115200); delay(1000); loadFromEEPROM(); pinMode(RELAY_LAMPU, OUTPUT); pinMode(RELAY_POMPA, OUTPUT); digitalWrite(RELAY_LAMPU, HIGH); digitalWrite(RELAY_POMPA, HIGH); sensors.begin(); pinMode(PH_SENSOR_PIN, INPUT); setupWiFiManager(); initNTP(); client.setServer(mqtt_server, mqtt_port); client.setCallback(callback); connectMQTT(); Serial.println("✅ Sistem siap!\n"); printWaktuSekarang(); } // ========== LOOP ========== void loop() { if (!client.connected()) connectMQTT(); client.loop(); syncNTP(); unsigned long now = millis(); if (now - lastSensorRead >= INTERVAL_SENSOR) { lastSensorRead = now; bacaSensor(); kontrolPompa(); kontrolLampuBySchedule(); } if (now - lastMQTT >= INTERVAL_SENSOR) { lastMQTT = now; kirimData(); } if (scheduleChanged && (now - lastScheduleChange > 2000)) { scheduleChanged = false; Serial.println("✅ Jadwal baru aktif!"); } }