TKK_E32232282/Script ESP32/esp32_air_quality_ac_contro...

1182 lines
34 KiB
C++

#include <WiFi.h>
#include <time.h>
#include <DHT.h>
#include <Firebase_ESP_Client.h>
#include "addons/TokenHelper.h"
#include "addons/RTDBHelper.h"
#include <Preferences.h>
#include <IRrecv.h>
#include <IRsend.h>
#include <IRac.h>
#include <IRutils.h>
#include <IRtext.h>
#include <EEPROM.h>
#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<FirebaseJson*>();
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) + ")";
}
}