TKK_E32232007/Amonia dan jadwal lampu.ino

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