commit 863d0056082bcfac64da5729f8e0a9f5e806f48a Author: Dwikyyy Date: Wed Jul 29 23:35:05 2026 +0700 Upload files to "Script ESP32" diff --git a/Script ESP32/esp32_air_quality_ac_control.ino b/Script ESP32/esp32_air_quality_ac_control.ino new file mode 100644 index 0000000..001ae5c --- /dev/null +++ b/Script ESP32/esp32_air_quality_ac_control.ino @@ -0,0 +1,1181 @@ +#include +#include +#include +#include +#include "addons/TokenHelper.h" +#include "addons/RTDBHelper.h" +#include +#include +#include +#include +#include +#include +#include + +#define WIFI_SSID "Test" +#define WIFI_PASSWORD "12345678" +#define API_KEY "AIzaSyANA0Sj8y2FQr9XXFAgprbqGNT0TIXxRfY" +#define DATABASE_URL "https://sensor-iot-45bb6-default-rtdb.firebaseio.com/" +#define USER_EMAIL "elonaviola992@gmail.com" +#define USER_PASSWORD "12345678" + +#define DHTPIN 4 +#define DHTTYPE DHT22 +#define MQ135_PIN 34 +#define GP2Y_LEDPIN 5 +#define GP2Y_VOPIN 33 +#define STATUS_LED_PIN 2 + +const uint16_t kRecvPin = 14; +const uint16_t kSendPin = 27; +const uint16_t kCaptureBufferSize = 2048; +const uint8_t kTimeout = 80; +const uint16_t kFrequency = 38; + +#define MQ135_CAL_ADC 960 +#define MQ135_CAL_PPM 400.0 +#define MQ135_PARA 116.6020682 +#define MQ135_PARB 2.769034857 +#define MQ135_RL 22.0 +#define MQ135_VCC 5.0 +float MQ135_R0 = 76.63; + +#define MQ135_ADC_BERSIH 1250 +#define MQ135_PPM_BERSIH 400.0 +const float MQ135_SLOPE_PPM_PER_ADC = MQ135_PPM_BERSIH / (float)MQ135_ADC_BERSIH; + +#define PM25_ADC_ZERO 1450.0 +#define PM25_SLOPE 0.137 +#define PM25_OFFSET 0.0 + +const float DHT_SUHU_OFFSET = -1.7; +const float DHT_KELEMBAPAN_OFFSET = -2.0; + +const unsigned long READ_INTERVAL_MS = 5000UL; +const unsigned long WIFI_RETRY_MS = 10000UL; +const unsigned long FIREBASE_RETRY_MS = 15000UL; +const unsigned long AC_AUTO_INTERVAL_MS = 300000UL; +const unsigned long FIREBASE_POLL_MS = 2000UL; + +unsigned long lastReadMillis = 0; +int lastHistoryMinuteSaved = -1; +unsigned long lastWifiTryMillis = 0; +unsigned long lastFirebaseTry = 0; +unsigned long lastAutoACMillis = 0; +unsigned long lastFirebasePoll = 0; + +bool wifiConnected = false; +bool firebaseReady = false; +bool timeSynced = false; +bool resetReasonSudahDikirim = false; +bool autoACEnabled = true; + +enum ModeKontrol { + KONTROL_AUTO, + KONTROL_MANUAL +}; +ModeKontrol modeKontrol = KONTROL_AUTO; +unsigned long lastManualCommandTime = 0; +const unsigned long AUTO_RESUME_MS = 300000UL; + +const char* ntpServer = "pool.ntp.org"; +const long gmtOffsetSec = 7 * 3600; +const int daylightOffsetSec = 0; + +DHT dht(DHTPIN, DHTTYPE); +float suhu = 25.0, kelembapan = 50.0, co2ppm = 400.0, pm25 = 0.0; +float mq135AdcRaw = 0, gp2yAdcRaw = 0; +float mq135Voltage = 0, gp2yVoltage = 0; + +float amankanNilai(float v, float def) { + if (isnan(v) || isinf(v)) return def; + return v; +} + +FirebaseData fbdo; +FirebaseAuth auth; +FirebaseConfig config; +Preferences prefs; + +IRrecv irrecv(kRecvPin, kCaptureBufferSize, kTimeout, true); +IRsend irsend(kSendPin); +IRac ac(kSendPin); +decode_results results; + +#define EEPROM_SIZE 64 +#define ADDR_MAGIC 0 +#define ADDR_PROTOCOL 4 +#define ADDR_MODEL 6 +#define ADDR_VALID 8 + +enum WorkMode { + MODE_IDLE, + MODE_CONFIG, + MODE_SWEEP, + MODE_LEARN +}; +WorkMode currentMode = MODE_IDLE; + +struct ACProtocolDef { + decode_type_t protocol; + const char* name; + const char* description; + bool hasModel; + int minModel; + int maxModel; + const char* modelNames[8]; +}; + +ACProtocolDef acProtocols[] = { + { PANASONIC_AC, "PANASONIC_AC", "Panasonic AC", true, 1, 6, + {"", "LKE", "NKE", "DKE", "JKE", "CKP", "RKR"} }, + { DAIKIN, "DAIKIN", "Daikin", false, -1, -1, {} }, + { LG, "LG", "LG AC", true, 1, 4, + {"", "GE6711AR2853M", "LG6711A20083V", "AKB74955603", "AKB73757604"} }, + { SHARP_AC, "SHARP_AC", "Sharp AC", true, 1, 3, + {"", "A705", "A903", "A907"} }, + { GREE, "GREE", "Gree AC", true, 1, 3, + {"", "YAW1F", "YBOFB", "YX1FSF"} }, +}; + +const int TOTAL_PROTOCOLS = sizeof(acProtocols) / sizeof(acProtocols[0]); + +struct ACState { + decode_type_t protocol; + int model; + bool power; + stdAc::opmode_t mode; + float degrees; + stdAc::fanspeed_t fanspeed; + stdAc::swingv_t swingv; + stdAc::swingh_t swingh; + bool light; + bool beep; + bool econo; + bool filter; + bool turbo; + bool quiet; + bool sleep; + bool autoMode; +}; + +ACState currentState; + +int sweepIndex = 0; +int sweepModel = 0; +bool hasSavedConfig = false; +uint16_t* lastRawArray = nullptr; +uint16_t lastRawLen = 0; + +String lastAutoAction = "none"; +unsigned long lastAutoTriggerTime = 0; + +void setup() { + Serial.begin(115200); + delay(500); + + pinMode(STATUS_LED_PIN, OUTPUT); + digitalWrite(STATUS_LED_PIN, LOW); + pinMode(GP2Y_LEDPIN, OUTPUT); + digitalWrite(GP2Y_LEDPIN, HIGH); + analogReadResolution(12); + + EEPROM.begin(EEPROM_SIZE); + + dht.begin(); + hitungR0MQ135(); + + irsend.begin(); + irrecv.setUnknownThreshold(12); + irrecv.enableIRIn(); + + WiFi.setSleep(false); + connectWiFi(); + + if (wifiConnected) { + lastFirebaseTry = 0; + ensureFirebaseReady(millis()); + } + + initACState(); + catatBootKeFlash(); + + if (loadConfigFromEEPROM()) { + printCurrentConfig(); + } +} + +void initACState() { + currentState.protocol = UNKNOWN; + currentState.model = -1; + currentState.power = false; + currentState.mode = stdAc::opmode_t::kCool; + currentState.degrees = 25; + currentState.fanspeed = stdAc::fanspeed_t::kAuto; + currentState.swingv = stdAc::swingv_t::kOff; + currentState.swingh = stdAc::swingh_t::kOff; + currentState.light = true; + currentState.beep = false; + currentState.econo = false; + currentState.filter = false; + currentState.turbo = false; + currentState.quiet = false; + currentState.sleep = false; + currentState.autoMode = true; +} + +bool isDalamKonfigurasi() { + return (currentMode == MODE_CONFIG || currentMode == MODE_SWEEP); +} + +const char* modeKontrolToString() { + return (modeKontrol == KONTROL_AUTO) ? "auto" : "manual"; +} + +void loop() { + unsigned long now = millis(); + + ensureWifiConnected(now); + ensureFirebaseReady(now); + updateStatusLed(); + + if (firebaseReady && !resetReasonSudahDikirim) { + kirimDiagnostikBoot(); + resetReasonSudahDikirim = true; + } + + if (modeKontrol == KONTROL_MANUAL && lastManualCommandTime > 0) { + if (now - lastManualCommandTime >= AUTO_RESUME_MS) { + modeKontrol = KONTROL_AUTO; + lastManualCommandTime = 0; + if (firebaseReady) kirimACStateToFirebase(); + } + } + + if (!isDalamKonfigurasi()) { + if (now - lastReadMillis >= READ_INTERVAL_MS) { + lastReadMillis = now; + bacaSemuaSensor(); + tampilkanSerial(); + if (firebaseReady) { + kirimDataTerkini(); + kirimACStateToFirebase(); + } + } + + cekDanSimpanHistoryPerMenit(); + + if (modeKontrol == KONTROL_AUTO && autoACEnabled && hasSavedConfig && (now - lastAutoACMillis >= AC_AUTO_INTERVAL_MS)) { + lastAutoACMillis = now; + processAutoTrigger(); + } + } + + if (firebaseReady && (now - lastFirebasePoll >= FIREBASE_POLL_MS)) { + lastFirebasePoll = now; + pollFirebaseCommand(); + } + + if (currentMode == MODE_LEARN) { + if (irrecv.decode(&results)) { + processLearnCapture(); + irrecv.resume(); + } + } + + if (currentMode == MODE_SWEEP) { + if (irrecv.decode(&results)) { + irrecv.resume(); + } + } + + delay(10); +} + +void processAutoTrigger() { + if (currentState.protocol == UNKNOWN) return; + + bool suhuBuruk = (suhu < 18.0) || (suhu > 30.0); + bool kelembapanRendah = (kelembapan < 30.0); + bool kelembapanTinggi = (kelembapan > 70.0); + bool co2Buruk = (kategoriCO2(co2ppm) == "Buruk"); + bool pm25Buruk = (kategoriPM25(pm25) == "Buruk"); + + bool adaKondisiBuruk = suhuBuruk || kelembapanRendah || kelembapanTinggi || + co2Buruk || pm25Buruk; + + if (!adaKondisiBuruk) { + lastAutoAction = "none"; + return; + } + + bool needDry = kelembapanTinggi; + bool needCool = suhuBuruk || kelembapanRendah; + bool needFan = co2Buruk || pm25Buruk; + + String action = "none"; + + if (needDry) { + action = "auto_dry"; + currentState.power = true; + currentState.mode = stdAc::opmode_t::kDry; + currentState.fanspeed = stdAc::fanspeed_t::kAuto; + currentState.degrees = 25; + } else if (needCool) { + action = "auto_cool"; + currentState.power = true; + currentState.mode = stdAc::opmode_t::kCool; + currentState.degrees = 24; + currentState.fanspeed = stdAc::fanspeed_t::kAuto; + } else if (needFan) { + action = "auto_fan"; + currentState.power = true; + currentState.mode = stdAc::opmode_t::kFan; + currentState.fanspeed = stdAc::fanspeed_t::kHigh; + } + + if (action != "none" && action != lastAutoAction) { + lastAutoAction = action; + lastAutoTriggerTime = millis(); + sendCurrentState(); + if (firebaseReady) { + kirimACStateToFirebase(); + catatAutoTriggerHistory(action); + } + } +} + +void catatAutoTriggerHistory(const String &action) { + if (!firebaseReady || !Firebase.ready()) return; + + unsigned long epoch = (unsigned long)time(nullptr); + String path = "/ac/auto_trigger_history/" + String(epoch); + + FirebaseJson json; + json.set("action", action); + json.set("suhu", amankanNilai(suhu, 25.0)); + json.set("kelembapan", amankanNilai(kelembapan, 50.0)); + json.set("co2_ppm", amankanNilai(co2ppm, 400.0)); + json.set("pm25_ugm3", amankanNilai(pm25, 0.0)); + json.set("timestamp", epoch); + + Firebase.RTDB.setJSON(&fbdo, path.c_str(), &json); +} + +void pollFirebaseCommand() { + if (!firebaseReady || !Firebase.ready()) return; + + if (Firebase.RTDB.getJSON(&fbdo, "/ac/control")) { + FirebaseJson *json = fbdo.to(); + if (json != nullptr) { + FirebaseJsonData jsonDataCmd; + FirebaseJsonData jsonDataVal; + + json->get(jsonDataCmd, "command"); + json->get(jsonDataVal, "value"); + + String cmdStr = jsonDataCmd.stringValue; + String valStr = jsonDataVal.stringValue; + + if (cmdStr.length() > 0) { + executeFlutterCommand(cmdStr, valStr); + + FirebaseJson clearJson; + clearJson.set("command", ""); + clearJson.set("value", ""); + clearJson.set("executed", true); + clearJson.set("executed_at", (unsigned long)time(nullptr)); + Firebase.RTDB.setJSON(&fbdo, "/ac/control", &clearJson); + } + } + } +} + +void executeFlutterCommand(String &cmd, String &val) { + cmd.toLowerCase(); + val.toLowerCase(); + + if (cmd == "control_mode") { + if (val == "auto") { + modeKontrol = KONTROL_AUTO; + lastManualCommandTime = 0; + } else if (val == "manual") { + modeKontrol = KONTROL_MANUAL; + lastManualCommandTime = millis(); + } + if (firebaseReady) kirimACStateToFirebase(); + return; + } + + if (cmd == "power") { + currentState.power = (val == "on" || val == "true" || val == "1"); + sendCurrentState(); + } + else if (cmd == "mode") { + if (val == "cool") currentState.mode = stdAc::opmode_t::kCool; + else if (val == "heat") currentState.mode = stdAc::opmode_t::kHeat; + else if (val == "dry") currentState.mode = stdAc::opmode_t::kDry; + else if (val == "fan") currentState.mode = stdAc::opmode_t::kFan; + else if (val == "auto") currentState.mode = stdAc::opmode_t::kAuto; + sendCurrentState(); + } + else if (cmd == "temp") { + int t = val.toInt(); + if (t >= 16 && t <= 32) { + currentState.degrees = t; + sendCurrentState(); + } + } + else if (cmd == "fan") { + if (val == "auto") currentState.fanspeed = stdAc::fanspeed_t::kAuto; + else if (val == "low") currentState.fanspeed = stdAc::fanspeed_t::kLow; + else if (val == "med" || val == "medium") currentState.fanspeed = stdAc::fanspeed_t::kMedium; + else if (val == "high") currentState.fanspeed = stdAc::fanspeed_t::kHigh; + else if (val == "max") currentState.fanspeed = stdAc::fanspeed_t::kMax; + sendCurrentState(); + } + else if (cmd == "swing") { + currentState.swingv = (val == "on" || val == "true") ? stdAc::swingv_t::kAuto : stdAc::swingv_t::kOff; + sendCurrentState(); + } + else if (cmd == "turbo") { + currentState.turbo = (val == "on" || val == "true"); + sendCurrentState(); + } + else if (cmd == "quiet") { + currentState.quiet = (val == "on" || val == "true"); + sendCurrentState(); + } + else if (cmd == "econo") { + currentState.econo = (val == "on" || val == "true"); + sendCurrentState(); + } + else if (cmd == "light") { + currentState.light = (val == "on" || val == "true"); + sendCurrentState(); + } + else if (cmd == "filter") { + currentState.filter = (val == "on" || val == "true"); + sendCurrentState(); + } + else if (cmd == "sleep") { + currentState.sleep = (val == "on" || val == "true"); + sendCurrentState(); + } + else if (cmd == "auto_mode") { + autoACEnabled = (val == "on" || val == "true" || val == "1"); + } + else if (cmd == "config") { + startConfigMode(); + } + else if (cmd == "select_brand") { + if (currentMode == MODE_CONFIG) { + int idx = val.toInt(); + if (idx > 0 && idx <= TOTAL_PROTOCOLS) { + startSweepForBrand(idx - 1); + } + } + } + else if (cmd == "confirm") { + if (currentMode == MODE_SWEEP) { + if (val == "y") sweepConfirm(true); + else if (val == "n") sweepConfirm(false); + else if (val == "skip") skipBrand(); + } + } + else if (cmd == "cancel_config") { + if (currentMode == MODE_CONFIG || currentMode == MODE_SWEEP) { + batalkanKonfigurasi(); + } + } + else if (cmd == "save_config") { + saveConfigToEEPROM(); + kirimStatusKonfigurasiKeFirebase(); + } + else if (cmd == "load_config") { + if (loadConfigFromEEPROM()) { + printCurrentConfig(); + } + kirimStatusKonfigurasiKeFirebase(); + } + else if (cmd == "clear_config") { + clearEEPROM(); + kirimStatusKonfigurasiKeFirebase(); + } + else if (cmd == "learn") { + startLearnMode(); + } + else if (cmd == "sendraw") { + sendLastRawData(); + } +} + +void kirimStatusKonfigurasiKeFirebase() { + if (!firebaseReady || !Firebase.ready()) return; + + FirebaseJson json; + String modeStr; + switch (currentMode) { + case MODE_CONFIG: modeStr = "config"; break; + case MODE_SWEEP: modeStr = "sweep"; break; + case MODE_LEARN: modeStr = "learn"; break; + default: modeStr = "idle"; break; + } + json.set("mode", modeStr); + json.set("has_config", hasSavedConfig); + + if (currentMode == MODE_SWEEP && sweepIndex < TOTAL_PROTOCOLS) { + ACProtocolDef &p = acProtocols[sweepIndex]; + String modelName = "(default)"; + if (p.hasModel && sweepModel > 0) modelName = String(p.modelNames[sweepModel]); + json.set("step", "confirm"); + json.set("brand_index", sweepIndex + 1); + json.set("brand_name", p.name); + json.set("brand_desc", p.description); + json.set("model_name", modelName); + json.set("message", "AC merespon sinyal ini? kirim command 'confirm' dengan value y/n/skip"); + } else if (currentMode == MODE_CONFIG) { + json.set("step", "select_brand"); + json.set("message", "Pilih merek AC: kirim command 'select_brand' dengan value nomor 1-" + String(TOTAL_PROTOCOLS)); + } else if (currentMode == MODE_LEARN) { + json.set("step", "learning"); + json.set("message", "Arahkan remote asli ke IR receiver lalu tekan tombol"); + } else { + json.set("step", hasSavedConfig ? "ready" : "not_configured"); + json.set("message", hasSavedConfig ? "Konfigurasi AC siap digunakan" : "Belum ada konfigurasi. Kirim command 'config' untuk mulai setup"); + } + json.set("updated_at", (unsigned long)time(nullptr)); + + Firebase.RTDB.setJSON(&fbdo, "/ac/config_status", &json); +} + +void kirimDaftarMerekKeFirebase() { + if (!firebaseReady || !Firebase.ready()) return; + + FirebaseJsonArray arr; + for (int i = 0; i < TOTAL_PROTOCOLS; i++) { + FirebaseJson item; + item.set("index", i + 1); + item.set("name", acProtocols[i].name); + item.set("description", acProtocols[i].description); + arr.add(item); + } + Firebase.RTDB.setArray(&fbdo, "/ac/config_brands", &arr); +} + +void kirimACStateToFirebase() { + if (!firebaseReady || !Firebase.ready()) return; + + FirebaseJson json; + json.set("power", currentState.power); + json.set("mode", modeToString(currentState.mode)); + json.set("temp", currentState.degrees); + json.set("fan", fanToString(currentState.fanspeed)); + json.set("swing", currentState.swingv != stdAc::swingv_t::kOff); + json.set("turbo", currentState.turbo); + json.set("quiet", currentState.quiet); + json.set("econo", currentState.econo); + json.set("light", currentState.light); + json.set("filter", currentState.filter); + json.set("sleep", currentState.sleep); + json.set("auto_mode", autoACEnabled); + json.set("control_mode", modeKontrolToString()); + json.set("last_auto_action", lastAutoAction); + json.set("protocol", typeToString(currentState.protocol)); + json.set("model", currentState.model); + json.set("has_config", hasSavedConfig); + json.set("last_update", (unsigned long)time(nullptr)); + + Firebase.RTDB.setJSON(&fbdo, "/ac/state", &json); +} + +bool sendCurrentState() { + if (currentState.protocol == UNKNOWN) { + return false; + } + + ac.next.protocol = currentState.protocol; + ac.next.model = currentState.model; + ac.next.power = currentState.power; + ac.next.mode = currentState.mode; + ac.next.celsius = true; + ac.next.degrees = currentState.degrees; + ac.next.fanspeed = currentState.fanspeed; + ac.next.swingv = currentState.swingv; + ac.next.swingh = currentState.swingh; + ac.next.light = currentState.light; + ac.next.beep = currentState.beep; + ac.next.econo = currentState.econo; + ac.next.filter = currentState.filter; + ac.next.turbo = currentState.turbo; + ac.next.quiet = currentState.quiet; + ac.next.sleep = currentState.sleep ? 0 : -1; + ac.next.clock = -1; + + bool ok = ac.sendAc(); + + if (ok) { + printStateJSON(); + if (firebaseReady) kirimACStateToFirebase(); + } + return ok; +} + +void startConfigMode() { + clearEEPROM(); + sweepIndex = 0; + sweepModel = 0; + + currentMode = MODE_CONFIG; + kirimDaftarMerekKeFirebase(); + kirimStatusKonfigurasiKeFirebase(); +} + +void batalkanKonfigurasi() { + currentMode = MODE_IDLE; + lastReadMillis = millis(); + kirimStatusKonfigurasiKeFirebase(); +} + +void listBrands() { + for (int i = 0; i < TOTAL_PROTOCOLS; i++) { + Serial.print(i + 1); + Serial.print(F(". ")); + Serial.print(acProtocols[i].name); + Serial.print(F(" - ")); + Serial.println(acProtocols[i].description); + if ((i + 1) % 10 == 0) delay(5); + } +} + +void startSweepForBrand(int index) { + if (index < 0 || index >= TOTAL_PROTOCOLS) { + return; + } + sweepIndex = index; + sweepModel = (acProtocols[index].hasModel) ? 0 : -1; + currentMode = MODE_SWEEP; + sweepSendCurrent(); +} + +void sweepSendCurrent() { + if (sweepIndex >= TOTAL_PROTOCOLS) { + currentMode = MODE_IDLE; + lastReadMillis = millis(); + kirimStatusKonfigurasiKeFirebase(); + return; + } + ACProtocolDef &p = acProtocols[sweepIndex]; + int modelToUse = -1; + if (p.hasModel) { + if (sweepModel == 0) { modelToUse = -1; } + else { modelToUse = sweepModel; } + } + currentState.protocol = p.protocol; + currentState.model = modelToUse; + currentState.power = true; + currentState.mode = stdAc::opmode_t::kCool; + currentState.degrees = 25; + currentState.fanspeed = stdAc::fanspeed_t::kAuto; + irrecv.resume(); + bool ok = sendCurrentState(); + if (ok) { + kirimStatusKonfigurasiKeFirebase(); + } + else { delay(500); sweepNext(); } +} + +void sweepConfirm(bool responded) { + if (responded) { + saveConfigToEEPROM(); + currentMode = MODE_IDLE; + lastReadMillis = millis(); + kirimStatusKonfigurasiKeFirebase(); + kirimACStateToFirebase(); + } else { + sweepNext(); + } +} + +void sweepNext() { + ACProtocolDef &p = acProtocols[sweepIndex]; + if (p.hasModel && sweepModel < p.maxModel) sweepModel++; + else { sweepModel = 0; sweepIndex++; } + sweepSendCurrent(); +} + +void skipBrand() { + sweepModel = 0; sweepIndex++; + sweepSendCurrent(); +} + +void saveConfigToEEPROM() { + if (currentState.protocol == UNKNOWN) { + return; + } + EEPROM.writeString(ADDR_MAGIC, "IRAC"); + EEPROM.put(ADDR_PROTOCOL, (int)currentState.protocol); + EEPROM.put(ADDR_MODEL, currentState.model); + EEPROM.write(ADDR_VALID, 0x01); + EEPROM.commit(); + hasSavedConfig = true; + printCurrentConfig(); +} + +bool loadConfigFromEEPROM() { + String magic = EEPROM.readString(ADDR_MAGIC); + if (magic != "IRAC") return false; + uint8_t valid = EEPROM.read(ADDR_VALID); + if (valid != 0x01) return false; + int protocolVal, modelVal; + EEPROM.get(ADDR_PROTOCOL, protocolVal); + EEPROM.get(ADDR_MODEL, modelVal); + currentState.protocol = (decode_type_t)protocolVal; + currentState.model = modelVal; + hasSavedConfig = true; + return true; +} + +void clearEEPROM() { + for (int i = 0; i < EEPROM_SIZE; i++) EEPROM.write(i, 0); + EEPROM.commit(); + hasSavedConfig = false; + initACState(); +} + +void printCurrentConfig() { + Serial.println(F("--- KONFIGURASI AC ---")); + if (currentState.protocol == UNKNOWN) { + Serial.println(F("Protocol: BELUM DIKONFIGURASI")); + } else { + Serial.print(F("Protocol: ")); Serial.println(typeToString(currentState.protocol)); + Serial.print(F("Model : ")); Serial.println(currentState.model); + } + Serial.print(F("Auto : ")); Serial.println(autoACEnabled ? F("ON") : F("OFF")); + Serial.print(F("Control : ")); Serial.println(modeKontrol == KONTROL_AUTO ? F("AUTO") : F("MANUAL")); +} + +void printStateJSON() { + Serial.print(F("{\"ac_state\":{\"power\":")); + Serial.print(currentState.power ? F("true") : F("false")); + Serial.print(F(",\"mode\":\"")); + Serial.print(modeToString(currentState.mode)); + Serial.print(F("\",\"temp\":")); + Serial.print(currentState.degrees); + Serial.print(F(",\"fan\":\"")); + Serial.print(fanToString(currentState.fanspeed)); + Serial.print(F("\",\"swing\":")); + Serial.print(currentState.swingv != stdAc::swingv_t::kOff ? F("true") : F("false")); + Serial.print(F(",\"turbo\":")); + Serial.print(currentState.turbo ? F("true") : F("false")); + Serial.print(F(",\"quiet\":")); + Serial.print(currentState.quiet ? F("true") : F("false")); + Serial.print(F(",\"econo\":")); + Serial.print(currentState.econo ? F("true") : F("false")); + Serial.print(F(",\"light\":")); + Serial.print(currentState.light ? F("true") : F("false")); + Serial.print(F(",\"filter\":")); + Serial.print(currentState.filter ? F("true") : F("false")); + Serial.print(F(",\"sleep\":")); + Serial.print(currentState.sleep ? F("true") : F("false")); + Serial.print(F(",\"auto_mode\":")); + Serial.print(autoACEnabled ? F("true") : F("false")); + Serial.print(F(",\"control_mode\":\"")); + Serial.print(modeKontrolToString()); + Serial.print(F("\",\"protocol\":\"")); + Serial.print(typeToString(currentState.protocol)); + Serial.print(F("\",\"model\":")); + Serial.print(currentState.model); + Serial.print(F(",\"last_auto\":\"")); + Serial.print(lastAutoAction); + Serial.println(F("\"}}")); +} + +const char* modeToString(stdAc::opmode_t mode) { + switch (mode) { + case stdAc::opmode_t::kAuto: return "auto"; + case stdAc::opmode_t::kCool: return "cool"; + case stdAc::opmode_t::kHeat: return "heat"; + case stdAc::opmode_t::kDry: return "dry"; + case stdAc::opmode_t::kFan: return "fan"; + default: return "unknown"; + } +} + +const char* fanToString(stdAc::fanspeed_t fan) { + switch (fan) { + case stdAc::fanspeed_t::kAuto: return "auto"; + case stdAc::fanspeed_t::kMin: return "min"; + case stdAc::fanspeed_t::kLow: return "low"; + case stdAc::fanspeed_t::kMedium: return "med"; + case stdAc::fanspeed_t::kHigh: return "high"; + case stdAc::fanspeed_t::kMax: return "max"; + default: return "unknown"; + } +} + +void startLearnMode() { + currentMode = MODE_LEARN; +} + +void processLearnCapture() { + decode_type_t detected = results.decode_type; + for (int i = 0; i < TOTAL_PROTOCOLS; i++) { + if (acProtocols[i].protocol == detected) { + currentState.protocol = detected; + currentState.model = -1; + break; + } + } + if (lastRawArray != nullptr) delete[] lastRawArray; + lastRawLen = getCorrectedRawLength(&results); + lastRawArray = resultToRawArray(&results); +} + +void sendLastRawData() { + if (lastRawArray == nullptr || lastRawLen == 0) { + return; + } + irsend.sendRaw(lastRawArray, lastRawLen, kFrequency); +} + +void hitungR0MQ135() { + float voutCal = (MQ135_CAL_ADC / 4095.0) * 3.3; + if (voutCal <= 0.0) voutCal = 0.01; + float rsCal = ((MQ135_VCC * MQ135_RL) / voutCal) - MQ135_RL; + float ratioCal = pow(MQ135_PARA / MQ135_CAL_PPM, 1.0 / MQ135_PARB); + MQ135_R0 = rsCal / ratioCal; +} + +float bacaCO2ppm() { + long total = 0; + for (int i = 0; i < 10; i++) { total += analogRead(MQ135_PIN); delay(5); } + float adcAvg = total / 10.0; + mq135AdcRaw = adcAvg; + float vout = (adcAvg / 4095.0) * 3.3; + mq135Voltage = vout; + + float ppm = adcAvg * MQ135_SLOPE_PPM_PER_ADC; + if (ppm < 0 || isnan(ppm)) ppm = 0; + + return ppm; +} + +float bacaPM25() { + long total = 0; + for (int i = 0; i < 5; i++) { + digitalWrite(GP2Y_LEDPIN, LOW); + delayMicroseconds(280); + delayMicroseconds(40); + int adc = analogRead(GP2Y_VOPIN); + digitalWrite(GP2Y_LEDPIN, HIGH); + delayMicroseconds(9680); + total += adc; + } + float adcAvg = total / 5.0; + gp2yAdcRaw = adcAvg; + gp2yVoltage = (adcAvg / 4095.0) * 3.3; + float ugm3 = PM25_SLOPE * (adcAvg - PM25_ADC_ZERO) + PM25_OFFSET; + + if (ugm3 < 0 || isnan(ugm3)) ugm3 = 0; + + return ugm3; +} + +void bacaSemuaSensor() { + float t = dht.readTemperature(); + float h = dht.readHumidity(); + if (!isnan(t)) { + suhu = t + DHT_SUHU_OFFSET; + } + if (!isnan(h)) { + kelembapan = h + DHT_KELEMBAPAN_OFFSET; + } + co2ppm = bacaCO2ppm(); + pm25 = bacaPM25(); +} + +String kategoriSuhu(float t) { + if (t >= 24 && t <= 27) return "Sangat Baik"; + else if ((t >= 20 && t < 24) || (t > 27 && t <= 30)) return "Baik"; + else return "Buruk"; +} + +String kategoriKelembapan(float h) { + if (h >= 40 && h <= 60) return "Sangat Baik"; + else if ((h >= 30 && h < 40) || (h > 60 && h <= 70)) return "Baik"; + else return "Buruk"; +} + +String kategoriCO2(float ppm) { + if (ppm < 400) return "Sangat Baik"; + else if (ppm <= 1000) return "Baik"; + else return "Buruk"; +} + +String kategoriPM25(float ugm3) { + if (ugm3 <= 15) return "Sangat Baik"; + else if (ugm3 <= 35) return "Baik"; + else return "Buruk"; +} + +int rankStatus(const String &s) { + if (s == "Sangat Baik") return 2; + if (s == "Baik") return 1; + return 0; +} + +String statusKeseluruhan(const String &s1, const String &s2, const String &s3, const String &s4) { + int r = min(rankStatus(s1), min(rankStatus(s2), min(rankStatus(s3), rankStatus(s4)))); + if (r >= 2) return "Sangat Baik"; + if (r == 1) return "Baik"; + return "Buruk"; +} + +void tampilkanSerial() { + String sSuhu = kategoriSuhu(suhu); + String sKelembapan = kategoriKelembapan(kelembapan); + String sCO2 = kategoriCO2(co2ppm); + String sPM25 = kategoriPM25(pm25); + String sKeseluruhan = statusKeseluruhan(sSuhu, sKelembapan, sCO2, sPM25); + + Serial.println(F("---------------------------------------")); + Serial.printf("Suhu : %.1f C [%s]\n", suhu, sSuhu.c_str()); + Serial.printf("Kelembapan : %.1f %% [%s]\n", kelembapan, sKelembapan.c_str()); + Serial.printf("CO2 : %.1f ppm [%s]\n", co2ppm, sCO2.c_str()); + Serial.printf("PM2.5 : %.1f ug/m3 [%s]\n", pm25, sPM25.c_str()); + Serial.printf("STATUS : %s\n", sKeseluruhan.c_str()); + Serial.printf("AC Auto : %s | Control: %s | Last: %s\n", + autoACEnabled ? "ON" : "OFF", + modeKontrol == KONTROL_AUTO ? "AUTO" : "MANUAL", + lastAutoAction.c_str()); + Serial.printf("WiFi : %s | Firebase: %s\n", wifiConnected ? "OK" : "NO", firebaseReady ? "OK" : "NO"); +} + +void kirimDataTerkini() { + if (!firebaseReady || WiFi.status() != WL_CONNECTED) return; + if (!Firebase.ready()) { firebaseReady = false; return; } + + String sSuhu = kategoriSuhu(suhu); + String sKelembapan = kategoriKelembapan(kelembapan); + String sCO2 = kategoriCO2(co2ppm); + String sPM25 = kategoriPM25(pm25); + String sKeseluruhan = statusKeseluruhan(sSuhu, sKelembapan, sCO2, sPM25); + + FirebaseJson json; + json.set("suhu", amankanNilai(suhu, 25.0), 2); + json.set("suhu_status", sSuhu); + json.set("kelembapan", amankanNilai(kelembapan, 50.0)); + json.set("kelembapan_status", sKelembapan); + json.set("co2_ppm", amankanNilai(co2ppm, 400.0)); + json.set("co2_status", sCO2); + json.set("co2_adc_raw", amankanNilai(mq135AdcRaw, 0.0)); + json.set("pm25_ugm3", amankanNilai(pm25, 0.0)); + json.set("pm25_status", sPM25); + json.set("pm25_adc_raw", amankanNilai(gp2yAdcRaw, 0.0)); + json.set("status_keseluruhan", sKeseluruhan); + json.set("wifi_status", wifiConnected ? "connected" : "disconnected"); + json.set("ac_auto_enabled", autoACEnabled); + json.set("ac_control_mode", modeKontrolToString()); + json.set("ac_last_auto_action", lastAutoAction); + json.set("last_update", (unsigned long)time(nullptr)); + + if (!Firebase.RTDB.setJSON(&fbdo, "/monitoring/current", &json)) { + if (fbdo.errorReason().indexOf("auth") >= 0 || fbdo.errorReason().indexOf("token") >= 0) { + firebaseReady = false; + } + } +} + +void cekDanSimpanHistoryPerMenit() { + if (!timeSynced) return; + + time_t epoch = time(nullptr); + struct tm timeinfo; + localtime_r(&epoch, &timeinfo); + + bool menitKelipatan5 = (timeinfo.tm_min % 5 == 0); + + if (menitKelipatan5 && timeinfo.tm_min != lastHistoryMinuteSaved) { + lastHistoryMinuteSaved = timeinfo.tm_min; + if (firebaseReady) simpanHistory(); + } +} + +void simpanHistory() { + if (!firebaseReady || !timeSynced) return; + if (!Firebase.ready()) { firebaseReady = false; return; } + + unsigned long epoch = (unsigned long)time(nullptr); + String path = "/monitoring/history/" + String(epoch); + + String sSuhu = kategoriSuhu(suhu); + String sKelembapan = kategoriKelembapan(kelembapan); + String sCO2 = kategoriCO2(co2ppm); + String sPM25 = kategoriPM25(pm25); + String sKeseluruhan = statusKeseluruhan(sSuhu, sKelembapan, sCO2, sPM25); + + FirebaseJson json; + json.set("suhu", amankanNilai(suhu, 25.0), 2); + json.set("suhu_status", sSuhu); + json.set("kelembapan", amankanNilai(kelembapan, 50.0)); + json.set("kelembapan_status", sKelembapan); + json.set("co2_ppm", amankanNilai(co2ppm, 400.0)); + json.set("co2_status", sCO2); + json.set("pm25_ugm3", amankanNilai(pm25, 0.0)); + json.set("pm25_status", sPM25); + json.set("status_keseluruhan", sKeseluruhan); + json.set("timestamp", epoch); + + Firebase.RTDB.setJSON(&fbdo, path.c_str(), &json); +} + +void connectWiFi() { + WiFi.mode(WIFI_STA); + WiFi.setAutoReconnect(true); + WiFi.persistent(true); + + const int MAX_PERCOBAAN_AWAL = 3; + int percobaan = 0; + + while (WiFi.status() != WL_CONNECTED && percobaan < MAX_PERCOBAAN_AWAL) { + percobaan++; + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + unsigned long start = millis(); + while (WiFi.status() != WL_CONNECTED && millis() - start < 20000) { + delay(300); + digitalWrite(STATUS_LED_PIN, !digitalRead(STATUS_LED_PIN)); + } + + if (WiFi.status() != WL_CONNECTED) { + delay(1500); + } + } + + if (WiFi.status() == WL_CONNECTED) { + wifiConnected = true; + syncTime(); + } +} + +void ensureWifiConnected(unsigned long now) { + if (WiFi.status() == WL_CONNECTED) { + if (!wifiConnected) { + wifiConnected = true; + syncTime(); + } + return; + } + if (wifiConnected) { + wifiConnected = false; + firebaseReady = false; + } + if (now - lastWifiTryMillis >= WIFI_RETRY_MS) { + lastWifiTryMillis = now; + WiFi.disconnect(); + delay(100); + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + } +} + +void syncTime() { + if (WiFi.status() != WL_CONNECTED) return; + configTime(gmtOffsetSec, daylightOffsetSec, ntpServer); + struct tm timeinfo; + if (getLocalTime(&timeinfo, 5000)) { + timeSynced = true; + } +} + +void ensureFirebaseReady(unsigned long now) { + if (firebaseReady) { + if (Firebase.ready()) return; + firebaseReady = false; + } + if (WiFi.status() != WL_CONNECTED) return; + if (!timeSynced) return; + if (now - lastFirebaseTry < FIREBASE_RETRY_MS) return; + lastFirebaseTry = now; + + if (ESP.getFreeHeap() < 20000) { + return; + } + + fbdo.clear(); + delay(100); + config.api_key = API_KEY; + config.database_url = DATABASE_URL; + auth.user.email = USER_EMAIL; + auth.user.password = USER_PASSWORD; + config.token_status_callback = tokenStatusCallback; + config.timeout.socketConnection = 5000; + config.timeout.serverResponse = 5000; + + Firebase.begin(&config, &auth); + Firebase.reconnectWiFi(true); + + unsigned long start = millis(); + while (!Firebase.ready() && millis() - start < 8000) { + delay(100); + yield(); + } + + if (Firebase.ready()) { + firebaseReady = true; + kirimStatusKonfigurasiKeFirebase(); + } +} + +void updateStatusLed() { + static unsigned long lastToggle = 0; + static bool ledState = false; + unsigned long now = millis(); + if (firebaseReady) { + digitalWrite(STATUS_LED_PIN, HIGH); + return; + } + unsigned long interval = wifiConnected ? 200 : 600; + if (now - lastToggle >= interval) { + lastToggle = now; + ledState = !ledState; + digitalWrite(STATUS_LED_PIN, ledState ? HIGH : LOW); + } +} + +void kirimDiagnostikBoot() { + esp_reset_reason_t alasan = esp_reset_reason(); + String teksAlasan = alasanResetKeString(alasan); + FirebaseJson json; + json.set("reset_reason", teksAlasan); + json.set("boot_time", (unsigned long)time(nullptr)); + json.set("free_heap_saat_boot", (int)ESP.getFreeHeap()); + Firebase.RTDB.setJSON(&fbdo, "/monitoring/diagnostik_boot_terakhir", &json); +} + +void catatBootKeFlash() { + prefs.begin("diag", false); + int jumlahBoot = prefs.getInt("boot_count", 0) + 1; + prefs.putInt("boot_count", jumlahBoot); + esp_reset_reason_t alasan = esp_reset_reason(); + String teksAlasan = alasanResetKeString(alasan); + prefs.putString("last_reason", teksAlasan); + if (alasan != ESP_RST_POWERON) { + int jumlahAneh = prefs.getInt("abnormal_count", 0) + 1; + prefs.putInt("abnormal_count", jumlahAneh); + } + prefs.end(); +} + +String alasanResetKeString(esp_reset_reason_t alasan) { + switch (alasan) { + case ESP_RST_POWERON: return "Power-on (normal)"; + case ESP_RST_BROWNOUT: return "BROWNOUT - suplai daya kurang!"; + case ESP_RST_TASK_WDT: return "TASK WATCHDOG - blocking terlalu lama"; + case ESP_RST_INT_WDT: return "INTERRUPT WATCHDOG"; + case ESP_RST_PANIC: return "PANIC/CRASH firmware"; + case ESP_RST_SW: return "Software reset (ESP.restart())"; + case ESP_RST_DEEPSLEEP: return "Bangun dari deep sleep"; + case ESP_RST_EXT: return "External reset (tombol RESET)"; + default: return "Lainnya (kode: " + String((int)alasan) + ")"; + } +}