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