415 lines
13 KiB
C++
415 lines
13 KiB
C++
// ========== LIBRARY ==========
|
|
#include <WiFi.h>
|
|
#include <PubSubClient.h>
|
|
#include <OneWire.h>
|
|
#include <DallasTemperature.h>
|
|
#include <ArduinoJson.h>
|
|
#include <time.h>
|
|
#include <EEPROM.h>
|
|
#include <WiFiManager.h>
|
|
#include <math.h>
|
|
|
|
// ========== 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!");
|
|
}
|
|
} |