first commit

This commit is contained in:
Shintia Kamilatul L 2026-07-14 13:18:51 +05:00
commit 82328255c0
16 changed files with 2051 additions and 0 deletions

BIN
assets/img/bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

BIN
assets/img/ikan.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

9
auth.php Normal file
View File

@ -0,0 +1,9 @@
<?php
session_start();
// kalau belum login → lempar ke index
if (!isset($_SESSION['logged_in'])) {
header("Location: index.php");
exit;
}
?>

856
biuh.ino Normal file
View File

@ -0,0 +1,856 @@
#include <Arduino.h>
#include <WiFi.h>
// Enable verbose debug from Firebase_ESP_Client (helps reveal which host/step fails)
#define FIREBASE_ESP_CLIENT_DEBUG_PORT Serial
#define FIREBASE_ESP_CLIENT_DEBUG_LEVEL 2
#include <Firebase_ESP_Client.h>
#include <WiFiClientSecure.h>
#include "DHT.h"
#include <Wire.h>
#include "RTClib.h"
#include "time.h"
#include <esp_heap_caps.h>
static const char* WIFI_SSID = "oven";
static const char* WIFI_PASSWORD = "1sampai8";
static const char* FIREBASE_API_KEY = "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w";
static const char* FIREBASE_DATABASE_URL = "https://smartoven-f9fdd-default-rtdb.firebaseio.com/";
char apiKey[100] = "";
char databaseUrl[200] = "";
FirebaseData fbdo;
FirebaseAuth auth;
FirebaseConfig config;
static bool signupOK = false;
static bool signUpAnonymousWithRetry(uint32_t retryDelayMs = 2000) {
// Anonymous sign-in: email/password kosong
for (;;) {
signupOK = Firebase.signUp(&config, &auth, "", "");
if (signupOK) {
Serial.println("[FB] signUp(anonymous): OK");
return true;
}
Serial.print("[FB] signUp(anonymous): FAIL ");
Serial.println(config.signer.signupError.message.c_str());
delay(retryDelayMs);
}
}
static bool waitFirebaseReady(uint32_t timeoutMs = 15000) {
Serial.print("[FB] Waiting ready");
uint32_t start = millis();
while (!Firebase.ready() && (millis() - start) < timeoutMs) {
delay(250);
Serial.print('.');
}
Serial.println();
Serial.println(Firebase.ready() ? "[FB] Ready" : "[FB] Not ready (timeout)");
return Firebase.ready();
}
static bool fbIsPathNotExist() {
// Firebase_ESP_Client uses negative codes for internal errors.
// -103 is commonly used for "path not exist" in RTDB get.
if (fbdo.httpCode() == -103) return true;
String r = fbdo.errorReason();
r.toLowerCase();
return r.indexOf("path not exist") >= 0;
}
// ===== HISTORY (RTDB) =====
static const bool ENABLE_HISTORY = true;
static const uint32_t HISTORY_INTERVAL_MS = 1800000; // 30 menit
static uint32_t lastHistoryPushMs = 0;
static bool fbPushHistory(float temp, float hum, int heaterDb, int fanDb, int mode, bool active) {
if (WiFi.status() != WL_CONNECTED) return false;
if (!Firebase.ready()) return false;
time_t epoch = time(nullptr);
if (epoch <= 0) {
Serial.println("[FB] WARN pushHistory skip (time not set)");
return false;
}
FirebaseJson json;
json.set("ts", (int)epoch);
json.set("temp", temp);
json.set("hum", hum);
json.set("heater", heaterDb);
json.set("fan", fanDb);
json.set("mode", mode);
json.set("active", active ? 1 : 0);
bool ok = Firebase.RTDB.pushJSON(&fbdo, "/history", &json);
if (ok) {
Serial.print("[FB] OK pushHistory key=");
Serial.println(fbdo.pushName());
} else {
Serial.print("[FB] FAIL pushHistory code=");
Serial.print(fbdo.httpCode());
Serial.print(" reason=");
Serial.println(fbdo.errorReason());
}
return ok;
}
// ===== FIREBASE HELPERS (STATUS LOG) =====
static bool fbSetFloat(const char* path, float value) {
if (WiFi.status() != WL_CONNECTED) {
Serial.print("[FB] FAIL setFloat ");
Serial.print(path);
Serial.println(" (WiFi not connected)");
return false;
}
if (!Firebase.ready()) {
Serial.print("[FB] FAIL setFloat ");
Serial.print(path);
Serial.println(" (Firebase not ready)");
return false;
}
bool ok = Firebase.RTDB.setFloat(&fbdo, path, value);
if (ok) {
Serial.print("[FB] OK setFloat ");
Serial.print(path);
Serial.print(" = ");
Serial.println(value);
} else {
Serial.print("[FB] FAIL setFloat ");
Serial.print(path);
Serial.print(" code=");
Serial.print(fbdo.httpCode());
Serial.print(" reason=");
Serial.println(fbdo.errorReason());
}
return ok;
}
static bool fbSetInt(const char* path, int value) {
if (WiFi.status() != WL_CONNECTED) {
Serial.print("[FB] FAIL setInt ");
Serial.print(path);
Serial.println(" (WiFi not connected)");
return false;
}
if (!Firebase.ready()) {
Serial.print("[FB] FAIL setInt ");
Serial.print(path);
Serial.println(" (Firebase not ready)");
return false;
}
bool ok = Firebase.RTDB.setInt(&fbdo, path, value);
if (ok) {
Serial.print("[FB] OK setInt ");
Serial.print(path);
Serial.print(" = ");
Serial.println(value);
} else {
Serial.print("[FB] FAIL setInt ");
Serial.print(path);
Serial.print(" code=");
Serial.print(fbdo.httpCode());
Serial.print(" reason=");
Serial.println(fbdo.errorReason());
}
return ok;
}
static bool fbGetInt(const char* path, int& outValue) {
if (WiFi.status() != WL_CONNECTED) {
Serial.print("[FB] FAIL getInt ");
Serial.print(path);
Serial.println(" (WiFi not connected)");
return false;
}
if (!Firebase.ready()) {
Serial.print("[FB] FAIL getInt ");
Serial.print(path);
Serial.println(" (Firebase not ready)");
return false;
}
bool ok = Firebase.RTDB.getInt(&fbdo, path);
if (ok) {
outValue = fbdo.intData();
Serial.print("[FB] OK getInt ");
Serial.print(path);
Serial.print(" = ");
Serial.println(outValue);
} else {
if (fbIsPathNotExist()) {
outValue = 0;
Serial.print("[FB] WARN getInt ");
Serial.print(path);
Serial.println(" (path not exist -> default 0)");
return true;
}
Serial.print("[FB] FAIL getInt ");
Serial.print(path);
Serial.print(" code=");
Serial.print(fbdo.httpCode());
Serial.print(" reason=");
Serial.println(fbdo.errorReason());
}
return ok;
}
static bool fbGetFloat(const char* path, float& outValue) {
if (WiFi.status() != WL_CONNECTED) {
Serial.print("[FB] FAIL getFloat ");
Serial.print(path);
Serial.println(" (WiFi not connected)");
return false;
}
if (!Firebase.ready()) {
Serial.print("[FB] FAIL getFloat ");
Serial.print(path);
Serial.println(" (Firebase not ready)");
return false;
}
bool ok = Firebase.RTDB.getFloat(&fbdo, path);
if (ok) {
outValue = fbdo.floatData();
Serial.print("[FB] OK getFloat ");
Serial.print(path);
Serial.print(" = ");
Serial.println(outValue);
} else {
if (fbIsPathNotExist()) {
outValue = 0.0f;
Serial.print("[FB] WARN getFloat ");
Serial.print(path);
Serial.println(" (path not exist -> default 0)");
return true;
}
Serial.print("[FB] FAIL getFloat ");
Serial.print(path);
Serial.print(" code=");
Serial.print(fbdo.httpCode());
Serial.print(" reason=");
Serial.println(fbdo.errorReason());
}
return ok;
}
// ===== RTC =====
RTC_DS3231 rtc;
static bool rtcAvailable = false;
static void scanI2C() {
Serial.println("I2C scan...");
uint8_t found = 0;
for (uint8_t addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
uint8_t err = Wire.endTransmission();
if (err == 0) {
Serial.print(" Found device at 0x");
if (addr < 16) Serial.print('0');
Serial.println(addr, HEX);
found++;
}
}
if (found == 0) {
Serial.println(" No I2C devices found (cek wiring SDA/SCL + pullup + pin Wire.begin)");
}
}
static void printHeap(const char* tag) {
Serial.print("[HEAP] ");
Serial.print(tag);
Serial.print(" free=");
Serial.print(ESP.getFreeHeap());
Serial.print(" min=");
Serial.print(ESP.getMinFreeHeap());
Serial.print(" 8bit=");
Serial.println((uint32_t)heap_caps_get_free_size(MALLOC_CAP_8BIT));
}
static bool extractHostFromUrl(const char* url, char* outHost, size_t outSize) {
if (!url || !outHost || outSize < 4) return false;
outHost[0] = '\0';
const char* p = url;
if (strncmp(p, "https://", 8) == 0) p += 8;
else if (strncmp(p, "http://", 7) == 0) p += 7;
const char* end = strchr(p, '/');
size_t len = end ? (size_t)(end - p) : strlen(p);
if (len == 0 || len >= outSize) return false;
memcpy(outHost, p, len);
outHost[len] = '\0';
return true;
}
static bool testTcpConnect(const char* host, uint16_t port) {
WiFiClient client;
client.setTimeout(5000);
bool ok = client.connect(host, port);
client.stop();
Serial.print("[NET] TCP ");
Serial.print(host);
Serial.print(":");
Serial.print(port);
Serial.print(" => ");
Serial.println(ok ? "OK" : "FAIL");
return ok;
}
static bool testTlsConnectInsecure(const char* host, uint16_t port) {
WiFiClientSecure client;
client.setInsecure();
client.setTimeout(7000);
printHeap("before TLS");
bool ok = client.connect(host, port);
Serial.print("[NET] TLS(insecure) ");
Serial.print(host);
Serial.print(":");
Serial.print(port);
Serial.print(" => ");
Serial.println(ok ? "OK" : "FAIL");
client.stop();
printHeap("after TLS");
return ok;
}
static void netDiagnostics() {
Serial.println("[NET] Diagnostics...");
printHeap("start");
// DNS test
IPAddress ip;
bool dnsOk = WiFi.hostByName("www.google.com", ip);
Serial.print("[NET] DNS www.google.com => ");
Serial.println(dnsOk ? ip.toString() : String("FAIL"));
// TCP/TLS to Google
testTcpConnect("www.google.com", 443);
testTlsConnectInsecure("www.google.com", 443);
// Firebase host test
char fbHost[128];
if (extractHostFromUrl(FIREBASE_DATABASE_URL, fbHost, sizeof(fbHost))) {
bool fbDnsOk = WiFi.hostByName(fbHost, ip);
Serial.print("[NET] DNS ");
Serial.print(fbHost);
Serial.print(" => ");
Serial.println(fbDnsOk ? ip.toString() : String("FAIL"));
testTcpConnect(fbHost, 443);
testTlsConnectInsecure(fbHost, 443);
} else {
Serial.println("[NET] Could not parse Firebase host from URL");
}
}
// ===== NTP =====
const char* ntpServer1 = "pool.ntp.org";
const char* ntpServer2 = "time.google.com";
const char* ntpServer3 = "time.nist.gov";
const long gmtOffset_sec = 7 * 3600;
const int daylightOffset_sec = 0;
unsigned long lastSync = 0;
const unsigned long syncInterval = 3600000; // 1 jam
// ===== DHT =====
#define DHTPIN 23
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
// ===== RELAY =====
const int relayHeater = 12;
const int relayFan = 13;
// ===== LED =====
const int ledHeater = 12;
// ===== RELAY LOGIC =====
#define RELAY_ON LOW
#define RELAY_OFF HIGH
// ===== VARIABLE =====
int startHour, startMinute;
int endHour, endMinute;
float targetTemp;
float targetTempFan;
// ===== KALIBRASI =====
float tempOffset = 0;
float humOffset = 0;
int mode = 0;
int manualHeater = 0;
int manualFan = 0;
// ===== WIFI CONNECT =====
static bool connectWiFi(uint32_t timeoutMs = 20000) {
WiFi.mode(WIFI_STA);
WiFi.setSleep(false);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("WiFi connecting to: ");
Serial.println(WIFI_SSID);
uint32_t start = millis();
while (WiFi.status() != WL_CONNECTED && (millis() - start) < timeoutMs) {
delay(300);
Serial.print('.');
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.print("WiFi connected, IP: ");
Serial.println(WiFi.localIP());
return true;
}
Serial.println("WiFi gagal connect");
return false;
}
static int monthStrToIndex(const char* mon) {
// __DATE__ format: "Mmm dd yyyy"
static const char* months[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
for (int i = 0; i < 12; i++) {
if (strncmp(mon, months[i], 3) == 0) return i;
}
return 0;
}
static bool setTimeFromCompileUtc() {
// fallback kalau NTP diblokir: set waktu dari waktu compile sketch
// Ini cukup buat TLS (sertifikat) asal firmware baru di-upload (waktunya mendekati sekarang).
const char* dateStr = __DATE__; // "May 20 2026"
const char* timeStr = __TIME__; // "HH:MM:SS"
char mon[4] = {0};
int day = 1;
int year = 2020;
int hour = 0;
int minute = 0;
int second = 0;
memcpy(mon, dateStr, 3);
mon[3] = '\0';
day = atoi(dateStr + 4);
year = atoi(dateStr + 7);
hour = atoi(timeStr);
minute = atoi(timeStr + 3);
second = atoi(timeStr + 6);
struct tm t;
memset(&t, 0, sizeof(t));
t.tm_year = year - 1900;
t.tm_mon = monthStrToIndex(mon);
t.tm_mday = day;
t.tm_hour = hour;
t.tm_min = minute;
t.tm_sec = second;
t.tm_isdst = 0;
setenv("TZ", "UTC0", 1);
tzset();
time_t epoch = mktime(&t);
if (epoch <= 0) return false;
struct timeval tv;
tv.tv_sec = epoch;
tv.tv_usec = 0;
settimeofday(&tv, nullptr);
Serial.print("[TIME] Fallback set from compile UTC: ");
Serial.println(ctime(&epoch));
return true;
}
// ===== SYNC RTC =====
static bool syncNtpTime(uint32_t timeoutMs = 20000) {
// set timezone for localtime
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2, ntpServer3);
struct tm timeinfo;
uint32_t start = millis();
while (!getLocalTime(&timeinfo) && (millis() - start) < timeoutMs) {
delay(500);
Serial.print('#');
}
Serial.println();
if (!getLocalTime(&timeinfo)) {
Serial.println("NTP gagal");
return false;
}
// validasi kasar: tahun harus sudah masuk akal
if (timeinfo.tm_year + 1900 < 2020) {
Serial.println("NTP dapat tapi waktu belum valid");
return false;
}
Serial.print("NTP OK: ");
Serial.print(timeinfo.tm_year + 1900);
Serial.print('-');
Serial.print(timeinfo.tm_mon + 1);
Serial.print('-');
Serial.print(timeinfo.tm_mday);
Serial.print(' ');
Serial.print(timeinfo.tm_hour);
Serial.print(':');
Serial.print(timeinfo.tm_min);
Serial.print(':');
Serial.println(timeinfo.tm_sec);
if (rtcAvailable) {
rtc.adjust(DateTime(
timeinfo.tm_year + 1900,
timeinfo.tm_mon + 1,
timeinfo.tm_mday,
timeinfo.tm_hour,
timeinfo.tm_min,
timeinfo.tm_sec
));
Serial.println("RTC Sync NTP");
}
return true;
}
static void syncRTC() {
if (syncNtpTime()) {
Serial.println(rtcAvailable ? "RTC/NTP sync OK" : "NTP sync OK (RTC tidak ada)");
}
}
// ===== SETUP =====
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("BOOT...");
// ===== PIN MODE =====
pinMode(relayHeater, OUTPUT);
pinMode(relayFan, OUTPUT);
pinMode(ledHeater, OUTPUT);
digitalWrite(relayHeater, RELAY_OFF);
digitalWrite(relayFan, RELAY_OFF);
digitalWrite(ledHeater, LOW);
// ===== DHT =====
dht.begin();
// ===== I2C =====
Wire.begin(13, 14);
scanI2C();
// ===== RTC =====
rtcAvailable = rtc.begin();
if (!rtcAvailable) {
Serial.println("RTC error (DS3231 tidak terdeteksi). Lanjut tanpa RTC (pakai NTP). ");
}
// ===== FIREBASE CONFIG (HARDCODE) =====
strncpy(apiKey, FIREBASE_API_KEY, sizeof(apiKey) - 1);
apiKey[sizeof(apiKey) - 1] = '\0';
strncpy(databaseUrl, FIREBASE_DATABASE_URL, sizeof(databaseUrl) - 1);
databaseUrl[sizeof(databaseUrl) - 1] = '\0';
if (strlen(apiKey) == 0 || strlen(databaseUrl) == 0) {
Serial.println("[WARN] FIREBASE_API_KEY / FIREBASE_DATABASE_URL masih kosong. Isi dulu di bagian atas sketch.");
}
// ===== WIFI =====
if (!connectWiFi()) {
// retry terus sampai konek
while (!connectWiFi()) {
delay(2000);
}
}
// ===== NTP TIME (PENTING UNTUK SSL/TLS) =====
// Banyak error SSL Firebase terjadi kalau waktu belum terset saat TLS handshake.
Serial.println("Sync NTP...");
bool ntpOk = syncNtpTime(60000);
if (!ntpOk) {
Serial.println("[TIME] NTP gagal. Coba fallback waktu compile...");
setTimeFromCompileUtc();
}
time_t nowEpoch = time(nullptr);
Serial.print("[TIME] Now epoch: ");
Serial.println((long)nowEpoch);
if (nowEpoch > 0) {
Serial.print("[TIME] Now: ");
Serial.println(ctime(&nowEpoch));
}
Serial.print("Heap before Firebase: ");
Serial.println(ESP.getFreeHeap());
// ===== NETWORK DIAGNOSTICS =====
// Ini bantu bedain: DNS/port443 block vs TLS init/heap issue.
netDiagnostics();
// ===== FIREBASE =====
config.api_key = apiKey;
config.database_url = databaseUrl;
// Anonymous auth ONLY (tanpa test_mode).
// Pastikan di Firebase Console: Authentication -> Sign-in method -> Anonymous di-enable.
signUpAnonymousWithRetry();
// buffer & timeout tuning (membantu masalah SSL init / write error)
// coba lebih kecil dulu biar nggak kehabisan heap
fbdo.setBSSLBufferSize(2048, 512);
fbdo.setResponseSize(2048);
config.timeout.serverResponse = 10 * 1000;
Firebase.begin(&config, &auth);
Firebase.reconnectWiFi(true);
waitFirebaseReady();
// ===== RTC SYNC =====
if (rtcAvailable) {
// kalau RTC ada, pastikan sudah disesuaikan
syncRTC();
}
lastSync = millis();
Serial.println("🔥 SYSTEM READY");
}
// ===== LOOP =====
void loop() {
// ===== WIFI CHECK =====
if (WiFi.status() != WL_CONNECTED) {
WiFi.reconnect();
}
// ===== RTC PERIODIC SYNC =====
if (WiFi.status() == WL_CONNECTED &&
millis() - lastSync > syncInterval) {
syncRTC();
lastSync = millis();
}
// ===== FIREBASE READY CHECK =====
if (WiFi.status() != WL_CONNECTED) {
Serial.println("[FB] Skip: WiFi not connected");
delay(1000);
return;
}
if (!Firebase.ready()) {
static uint32_t lastNotReadyLog = 0;
if (millis() - lastNotReadyLog > 5000) {
Serial.println("[FB] Skip: Firebase not ready");
lastNotReadyLog = millis();
}
delay(200);
// tetap lanjut loop (biar logic lokal jalan), tapi operasi Firebase akan FAIL di wrapper
}
if (!signupOK) {
Serial.println("[FB] Skip: signUp not OK");
delay(2000);
return;
}
// ===== GET FIREBASE =====
fbGetInt("/control/start_hour", startHour);
fbGetInt("/control/start_minute", startMinute);
fbGetInt("/control/end_hour", endHour);
fbGetInt("/control/end_minute", endMinute);
fbGetFloat("/control/target_temp", targetTemp);
fbGetFloat("/control/target_temp_fan", targetTempFan);
fbGetInt("/control/mode", mode);
fbGetInt("/manual/heater", manualHeater);
fbGetInt("/manual/fan", manualFan);
// fallback kalau target fan belum diset
if (targetTempFan <= 0) targetTempFan = targetTemp;
// ===== GET KALIBRASI =====
fbGetFloat("/calibration/temp_offset", tempOffset);
fbGetFloat("/calibration/hum_offset", humOffset);
// ===== CURRENT TIME =====
int hourNow = 0;
int minuteNow = 0;
bool timeOk = false;
if (rtcAvailable) {
DateTime now = rtc.now();
hourNow = now.hour();
minuteNow = now.minute();
timeOk = true;
} else {
struct tm timeinfo;
if (getLocalTime(&timeinfo, 1000)) {
hourNow = timeinfo.tm_hour;
minuteNow = timeinfo.tm_min;
timeOk = true;
}
}
int currentTime = hourNow * 60 + minuteNow;
int startTime = startHour * 60 + startMinute;
int endTime = endHour * 60 + endMinute;
bool active = false;
if (timeOk) {
active = (startTime <= endTime)
? (currentTime >= startTime &&
currentTime <= endTime)
: (currentTime >= startTime ||
currentTime <= endTime);
} else {
// kalau waktu tidak valid, fail-safe: nonaktif
Serial.println("[TIME] Tidak dapat ambil waktu (RTC/NTP). Mode AUTO akan nonaktif.");
active = false;
}
// ===== SENSOR =====
float temp = dht.readTemperature();
float hum = dht.readHumidity();
// DHT read kadang gagal (NaN) karena timing/interrupt/WiFi atau wiring.
// Coba retry sekali agar lebih stabil.
if (isnan(temp) || isnan(hum)) {
Serial.println("[SENSOR] DHT read failed, retry...");
delay(250);
temp = dht.readTemperature();
hum = dht.readHumidity();
}
// ===== VALIDASI SENSOR =====
if (isnan(temp) || isnan(hum)) {
Serial.println("[SENSOR] DHT error (NaN). Cek wiring VCC/GND/DATA, pullup 4.7k-10k ke VCC, kabel jangan panjang, pastikan sensor DHT22 & pin GPIO benar.");
delay(2000);
return;
}
// ===== APPLY KALIBRASI =====
temp += tempOffset;
hum += humOffset;
// ===== CONTROL =====
bool heaterState = false;
bool fanState = false;
if (mode == 1) {
// ===== MANUAL MODE =====
heaterState = (manualHeater != 0);
fanState = (manualFan != 0);
digitalWrite(
relayFan,
fanState ? RELAY_ON : RELAY_OFF
);
} else {
// ===== AUTO MODE =====
if (active) {
heaterState = (temp < targetTemp);
fanState = (temp > targetTempFan);
digitalWrite(
relayFan,
fanState ? RELAY_ON : RELAY_OFF
);
} else {
heaterState = false;
fanState = false;
digitalWrite(relayFan, RELAY_OFF);
}
}
// ===== APPLY HEATER =====
digitalWrite(
relayHeater,
heaterState ? RELAY_ON : RELAY_OFF
);
digitalWrite(
ledHeater,
heaterState ? HIGH : LOW
);
// ===== SEND SENSOR =====
bool okTemp = fbSetFloat("/sensor/temperature", temp);
bool okHum = fbSetFloat("/sensor/humidity", hum);
// ===== SEND RELAY =====
int heaterDb = heaterState ? 1 : 0;
int fanDb = fanState ? 1 : 0;
bool okHeater = fbSetInt("/relay/heater", heaterDb);
bool okFan = fbSetInt("/relay/fan", fanDb);
// Mirror manual state to match relay state (biar UI manual & relay selalu sama)
bool okManualHeater = fbSetInt("/manual/heater", heaterDb);
bool okManualFan = fbSetInt("/manual/fan", fanDb);
Serial.print("[FB] Upload summary: ");
Serial.println((okTemp && okHum && okHeater && okFan && okManualHeater && okManualFan) ? "OK" : "FAIL");
// ===== DEBUG =====
Serial.print("Time: ");
Serial.print(hourNow);
Serial.print(":");
Serial.print(minuteNow);
Serial.print(" (");
Serial.print(rtcAvailable ? "RTC" : "NTP");
Serial.println(")");
Serial.print("Temp: ");
Serial.println(temp);
Serial.print("Humidity: ");
Serial.println(hum);
Serial.print("Temp Offset: ");
Serial.println(tempOffset);
Serial.print("Hum Offset: ");
Serial.println(humOffset);
Serial.print("Mode: ");
Serial.println(mode == 1 ? "MANUAL" : "AUTO");
Serial.print("Active: ");
Serial.println(active);
// ===== PUSH HISTORY =====
if (ENABLE_HISTORY) {
uint32_t nowMs = millis();
if (lastHistoryPushMs == 0 || (nowMs - lastHistoryPushMs) >= HISTORY_INTERVAL_MS) {
fbPushHistory(temp, hum, heaterDb, fanDb, mode, active);
lastHistoryPushMs = nowMs;
}
}
delay(3000);
}

84
dashboard.php Normal file
View File

@ -0,0 +1,84 @@
<?php
session_start();
if(!isset($_SESSION['logged_in'])) header("Location:index.php");
$page = $_GET['page'] ?? 'home';
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard IoT</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script type="module" src="firebase-dashboard.js"></script>
<style>
* {
transition: all 0.3s ease;
}
</style>
<style>
body { font-family: 'Poppins', sans-serif; }
</style>
</head>
<body class="bg-gradient-to-br from-slate-100 to-blue-100 flex">
<!-- SIDEBAR -->
<div class="w-64 bg-gradient-to-b from-blue-600 to-indigo-700 text-white min-h-screen p-6 shadow-xl">
<h1 class="text-2xl font-bold mb-10 tracking-wide">
IoT Dryer
</h1>
<ul class="space-y-3 text-sm">
<li onclick="window.location='dashboard.php?page=home'"
class="p-3 rounded-xl cursor-pointer hover:bg-white/20 transition duration-300 flex items-center gap-2">
<i class="fas fa-chart-line"></i> Dashboard
</li>
<li onclick="window.location='dashboard.php?page=monitoring'"
class="p-3 rounded-xl cursor-pointer hover:bg-white/20 transition duration-300 flex items-center gap-2">
<i class="fas fa-thermometer-half"></i> Monitoring
</li>
<li onclick="window.location='dashboard.php?page=kontrol'"
class="p-3 rounded-xl cursor-pointer hover:bg-white/20 transition duration-300 flex items-center gap-2">
<i class="fas fa-sliders-h"></i> Kontrol
</li>
<li onclick="window.location='logout.php'"
class="p-3 rounded-xl cursor-pointer hover:bg-red-500 transition duration-300 flex items-center gap-2">
<i class="fas fa-sign-out-alt"></i> Logout
</li>
</ul>
</div>
<!-- MAIN -->
<div class="flex-1 <?= ($page == 'home') ? 'p-0' : 'p-8' ?>">
<?php
if ($page == 'home') {
include 'home.php';
} elseif ($page == 'monitoring') {
include 'monitoring.php';
} elseif ($page == 'kontrol') {
include 'kontrol.php';
} else {
echo "<h2>Halaman tidak ditemukan</h2>";
}
?>
</div>
</body>
</html>

102
firebase-auth.js Normal file
View File

@ -0,0 +1,102 @@
// 🔥 IMPORT
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
import {
getAuth,
createUserWithEmailAndPassword,
signInWithEmailAndPassword
} from "https://www.gstatic.com/firebasejs/10.12.2/firebase-auth.js";
import {
getDatabase,
ref,
set
} from "https://www.gstatic.com/firebasejs/10.12.2/firebase-database.js";
// 🔥 CONFIG (FIX SEMUA)
const firebaseConfig = {
apiKey: "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w",
authDomain: "smartoven-f9fdd.firebaseapp.com",
databaseURL: "https://smartoven-f9fdd-default-rtdb.firebaseio.com/",
projectId: "smartoven-f9fdd",
appId: "1:1005261996032:web:1bcb483b922275f08f98e5"
};
// 🔥 INIT
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getDatabase(app);
// =====================
// 🔥 REGISTER
// =====================
const registerForm = document.getElementById("registerForm");
if (registerForm) {
registerForm.addEventListener("submit", async (e) => {
e.preventDefault();
const username = document.querySelector('[name="reg_username"]').value;
const email = document.querySelector('[name="reg_email"]').value;
const password = document.querySelector('[name="reg_password"]').value;
const confirm = document.querySelector('[name="reg_confirm"]').value;
if (password !== confirm) {
alert("Password tidak sama!");
return;
}
try {
const userCred = await createUserWithEmailAndPassword(auth, email, password);
const user = userCred.user;
// 🔥 SIMPAN USER KE DATABASE
await set(ref(db, "users/" + user.uid), {
username: username,
email: email
});
alert("Registrasi berhasil!");
window.location.href = "index.php";
} catch (err) {
alert("Error: " + err.message);
}
});
}
// =====================
// 🔥 LOGIN
// =====================
const loginForm = document.getElementById("loginForm");
if (loginForm) {
loginForm.addEventListener("submit", async (e) => {
e.preventDefault();
const email = document.querySelector('[name="username"]').value;
const password = document.querySelector('[name="password"]').value;
try {
await signInWithEmailAndPassword(auth, email, password);
// 🔥 KIRIM SESSION KE PHP
await fetch("login_process.php", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `email=${email}`
});
window.location.href = "dashboard.php";
} catch (err) {
alert("Login gagal: " + err.message);
}
});
}

118
firebase-dashboard.js Normal file
View File

@ -0,0 +1,118 @@
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
import { getDatabase, ref, onValue, set, push } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-database.js";
// 🔥 CONFIG
const firebaseConfig = {
apiKey: "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w",
authDomain: "smartoven-f9fdd.firebaseapp.com",
databaseURL: "https://smartoven-f9fdd-default-rtdb.firebaseio.com/",
projectId: "smartoven-f9fdd",
appId: "1:1005261996032:web:1bcb483b922275f08f98e5"
};
const app = initializeApp(firebaseConfig);
const db = getDatabase(app);
// 🔥 ELEMENT
const suhuEl = document.getElementById("suhu");
const humEl = document.getElementById("kelembaban");
const logEl = document.getElementById("log");
const heaterEl = document.getElementById("heater");
const fanEl = document.getElementById("fan");
const historyTable = document.getElementById("historyTable");
// =======================
// 🔥 REALTIME SENSOR
// =======================
onValue(ref(db, 'sensor'), (snapshot) => {
const data = snapshot.val();
if (!data) return;
const time = new Date().toLocaleTimeString();
if (suhuEl) suhuEl.innerText = data.suhu + " °C";
if (humEl) humEl.innerText = data.kelembaban + " %";
if (logEl) {
logEl.innerHTML += `
<tr>
<td>${time}</td>
<td>${data.suhu}</td>
<td>${data.kelembaban}</td>
</tr>`;
}
// simpan ke history
push(ref(db, 'history'), {
suhu: data.suhu,
kelembaban: data.kelembaban,
waktu: time
});
});
// =======================
// 🔥 STATUS CONTROL
// =======================
onValue(ref(db, 'control'), (snap) => {
const d = snap.val();
if (!d) return;
if (heaterEl) {
heaterEl.innerText = d.heater ? "ON" : "OFF";
heaterEl.className = d.heater ? "text-green-500" : "text-red-500";
}
if (fanEl) {
fanEl.innerText = d.fan ? "ON" : "OFF";
fanEl.className = d.fan ? "text-green-500" : "text-red-500";
}
});
// =======================
// 🔥 KONTROL
// =======================
window.setHeater = (val) => {
set(ref(db, 'control/heater'), val);
};
window.setFan = (val) => {
set(ref(db, 'control/fan'), val);
};
window.setTarget = () => {
const suhu = document.getElementById("targetSuhu").value;
if (!suhu) return alert("Isi suhu!");
set(ref(db, 'control/target_suhu'), parseInt(suhu));
};
// =======================
// 🔥 HISTORY TABLE
// =======================
onValue(ref(db, 'history'), (snap) => {
const data = snap.val();
if (!data || !historyTable) return;
historyTable.innerHTML = "";
Object.values(data).reverse().forEach(d => {
let status = "Normal";
let color = "text-green-500";
if (d.suhu > 50) {
status = "Panas";
color = "text-red-500";
} else if (d.suhu < 30) {
status = "Dingin";
color = "text-blue-500";
}
historyTable.innerHTML += `
<tr class="hover:bg-gray-50 transition">
<td class="px-4 py-3">${d.waktu}</td>
<td class="px-4 py-3 font-semibold">${d.suhu} °C</td>
<td class="px-4 py-3">${d.kelembaban} %</td>
<td class="px-4 py-3 ${color} font-semibold">${status}</td>
</tr>`;
});
});

43
home.php Normal file
View File

@ -0,0 +1,43 @@
<!-- 🔥 WRAPPER KHUSUS HOME -->
<div class="relative w-full h-screen overflow-hidden">
<!-- 🔥 BACKGROUND GAMBAR -->
<div class="absolute inset-0">
<img src="assets/img/ikan.jpg"
class="w-full h-full object-cover blur-sm scale-105">
<!-- overlay biar lebih soft -->
<div class="absolute inset-0 bg-black/30"></div>
</div>
<!-- 🔥 ISI DASHBOARD -->
<div class="relative z-10 p-6 text-white">
<h2 class="text-2xl font-bold mb-6">Dashboard</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Monitoring -->
<div onclick="window.location='dashboard.php?page=monitoring'"
class="bg-white/80 backdrop-blur-md p-6 rounded-2xl shadow hover:shadow-xl cursor-pointer text-black">
<i class="fas fa-chart-line text-3xl text-blue-500 mb-3"></i>
<h3 class="font-semibold text-lg">Monitoring</h3>
<p class="text-sm text-gray-600">Lihat data suhu & kelembaban</p>
</div>
<!-- Kontrol -->
<div onclick="window.location='dashboard.php?page=kontrol'"
class="bg-white/80 backdrop-blur-md p-6 rounded-2xl shadow hover:shadow-xl cursor-pointer text-black">
<i class="fas fa-sliders-h text-3xl text-green-500 mb-3"></i>
<h3 class="font-semibold text-lg">Kontrol</h3>
<p class="text-sm text-gray-600">Atur alat pengering</p>
</div>
</div>
</div>
</div>

78
index.php Normal file
View File

@ -0,0 +1,78 @@
<?php session_start(); ?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pengering Ikan IoT</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
body {
background: url('assets/img/bg.jpg') center/cover no-repeat;
font-family: 'Inter', sans-serif;
}
.hidden-form { display: none; }
</style>
</head>
<body class="min-h-screen flex items-center justify-center p-4 relative">
<!-- 🔥 BLUR BACKGROUND -->
<div class="absolute inset-0 bg-black/40 backdrop-blur-sm z-0"></div>
<!-- 🔥 CARD UTAMA -->
<div class="relative z-10 max-w-4xl w-full flex flex-col md:flex-row
rounded-3xl overflow-hidden shadow-2xl
bg-white/20 backdrop-blur-xl border border-white/30">
<!-- 🔵 KIRI -->
<div class="md:w-1/2 bg-blue-500/60 backdrop-blur-lg
p-12 text-white flex flex-col justify-between items-center text-center">
<div>
<h1 class="text-2xl font-bold">PENGERING IKAN IoT</h1>
<div class="mt-8 overflow-hidden rounded-2xl shadow-xl">
<img src="assets/img/ikan.jpg"
class="w-full h-48 object-cover transition duration-300 hover:scale-110">
</div>
</div>
<p class="text-sm italic mt-[-20px]">Monitoring real-time</p>
</div>
<!-- KANAN -->
<div class="md:w-1/2 p-8 md:p-12 bg-white/60 backdrop-blur-lg">
<div id="login-section">
<?php include 'login_form.php'; ?>
</div>
<div id="register-section" class="hidden-form">
<?php include 'register_form.php'; ?>
</div>
</div>
</div>
<script>
function toggleForm(type) {
const login = document.getElementById('login-section');
const register = document.getElementById('register-section');
if (type === 'register') {
login.classList.add('hidden-form');
register.classList.remove('hidden-form');
} else {
register.classList.add('hidden-form');
login.classList.remove('hidden-form');
}
}
</script>
<script type="module" src="firebase-auth.js"></script>
</body>
</html>

208
kontrol.php Normal file
View File

@ -0,0 +1,208 @@
<h2 class="text-2xl font-bold mb-6 text-gray-700">Kontrol Alat</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- HEATER -->
<div class="bg-white p-6 rounded-2xl shadow">
<div class="flex justify-between items-center">
<p class="font-semibold text-gray-700 flex items-center gap-2">
🔥 Heater
</p>
<input type="checkbox" id="heaterToggle"
onchange="setHeater(this)"
class="w-6 h-6 cursor-pointer">
</div>
</div>
<!-- FAN -->
<div class="bg-white p-6 rounded-2xl shadow">
<div class="flex justify-between items-center">
<p class="font-semibold text-gray-700 flex items-center gap-2">
💨 Fan
</p>
<input type="checkbox" id="fanToggle"
onchange="setFan(this)"
class="w-6 h-6 cursor-pointer">
</div>
</div>
</div>
<!-- JADWAL -->
<div class="bg-white p-6 rounded-2xl shadow mt-6">
<p class="font-semibold mb-3 text-gray-700">
Jadwal Otomatis
</p>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="text-sm text-gray-500">Start Time</label>
<input id="startTime" type="time" class="border p-2 rounded-xl w-full">
</div>
<div>
<label class="text-sm text-gray-500">End Time</label>
<input id="endTime" type="time" class="border p-2 rounded-xl w-full">
</div>
</div>
<button onclick="setSchedule()"
class="mt-4 bg-purple-500 text-white px-6 py-2 rounded-xl">
Simpan Jadwal
</button>
</div>
<!-- MODE -->
<div class="bg-white p-4 rounded-2xl shadow mt-4">
<p class="text-sm text-gray-500">Status Mode</p>
<div class="flex gap-3 mt-2">
<span id="modeAuto"
onclick="setModeAuto()"
class="px-4 py-1 rounded-xl bg-gray-300 text-white cursor-pointer">
CONTROL
</span>
<span id="modeManual"
onclick="setModeManual()"
class="px-4 py-1 rounded-xl bg-gray-300 text-white cursor-pointer">
MANUAL
</span>
</div>
</div>
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
import { getDatabase, ref, set, update, onValue } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-database.js";
const firebaseConfig = {
apiKey: "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w",
authDomain: "smartoven-f9fdd.firebaseapp.com",
databaseURL: "https://smartoven-f9fdd-default-rtdb.firebaseio.com/",
projectId: "smartoven-f9fdd",
appId: "1:1005261996032:web:1bcb483b922275f08f98e5"
};
const app = initializeApp(firebaseConfig);
const db = getDatabase(app);
const heaterToggleEl = document.getElementById("heaterToggle");
const fanToggleEl = document.getElementById("fanToggle");
const startTimeEl = document.getElementById("startTime");
const endTimeEl = document.getElementById("endTime");
const modeAutoEl = document.getElementById("modeAuto");
const modeManualEl = document.getElementById("modeManual");
let currentMode = 0; // 0 = CONTROL/AUTO, 1 = MANUAL
// ===== UI MODE =====
const setModeUi = (mode) => {
currentMode = mode;
if (mode === 0) {
// CONTROL aktif
modeAutoEl.classList.replace("bg-gray-300", "bg-green-500");
modeManualEl.classList.replace("bg-green-500", "bg-gray-300");
heaterToggleEl.disabled = true;
fanToggleEl.disabled = true;
startTimeEl.disabled = false;
endTimeEl.disabled = false;
} else {
// MANUAL aktif
modeManualEl.classList.replace("bg-gray-300", "bg-green-500");
modeAutoEl.classList.replace("bg-green-500", "bg-gray-300");
heaterToggleEl.disabled = false;
fanToggleEl.disabled = false;
startTimeEl.disabled = true;
endTimeEl.disabled = true;
}
};
// ===== SET MODE =====
window.setModeAuto = function () {
set(ref(db, 'control/mode'), 0);
};
window.setModeManual = function () {
set(ref(db, 'control/mode'), 1);
};
// ===== MANUAL CONTROL =====
window.setHeater = function (el) {
if (currentMode !== 1) {
alert("Pakai mode MANUAL dulu!");
el.checked = !el.checked;
return;
}
set(ref(db, 'manual/heater'), el.checked ? 1 : 0);
};
window.setFan = function (el) {
if (currentMode !== 1) {
alert("Pakai mode MANUAL dulu!");
el.checked = !el.checked;
return;
}
set(ref(db, 'manual/fan'), el.checked ? 1 : 0);
};
// ===== JADWAL =====
window.setSchedule = function () {
const start = startTimeEl.value;
const end = endTimeEl.value;
if (!start || !end) {
alert("Isi waktu dulu!");
return;
}
const [sh, sm] = start.split(":").map(Number);
const [eh, em] = end.split(":").map(Number);
update(ref(db, 'control'), {
start_hour: sh,
start_minute: sm,
end_hour: eh,
end_minute: em
})
.then(() => alert(`Jadwal disimpan!\nStart: ${start}\nEnd: ${end}`))
.catch((err) => alert("Gagal simpan: " + err));
};
// ===== LISTENERS =====
// Mode
onValue(ref(db, 'control/mode'), (snap) => {
const mode = snap.val();
if (mode == null) return;
setModeUi(mode);
});
// Toggle manual (heater & fan)
onValue(ref(db, 'manual'), (snap) => {
const d = snap.val();
if (!d) return;
heaterToggleEl.checked = d.heater === 1;
fanToggleEl.checked = d.fan === 1;
});
// Jadwal — digabung dalam 1 listener
onValue(ref(db, 'control'), (snap) => {
const d = snap.val();
if (!d) return;
if (d.start_hour != null && d.start_minute != null) {
startTimeEl.value =
String(d.start_hour).padStart(2, '0') + ":" +
String(d.start_minute).padStart(2, '0');
}
if (d.end_hour != null && d.end_minute != null) {
endTimeEl.value =
String(d.end_hour).padStart(2, '0') + ":" +
String(d.end_minute).padStart(2, '0');
}
});
</script>

112
login_form.php Normal file
View File

@ -0,0 +1,112 @@
<?php ?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Pengering Ikan IoT</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
body { font-family: 'Poppins', sans-serif; }
</style>
</head>
<body style="
background-image: url('assets/img/bg.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
">
<body class="bg-[#f5f7fa] flex items-center justify-center min-h-screen">
<div class="bg-white/50 backdrop-blur-lg p-8 rounded-3xl shadow-2xl w-full max-w-md border border-white/20">
<h2 class="text-2xl font-bold text-center mb-6">Login</h2>
<form id="loginForm" class="space-y-4">
<div>
<label class="text-sm">Email</label>
<input name="username" type="email"
class="w-full mt-1 p-3 border rounded-xl"
placeholder="email@gmail.com" required>
</div>
<div>
<label class="text-sm">Password</label>
<input name="password" type="password"
class="w-full mt-1 p-3 border rounded-xl"
placeholder="••••••" required>
</div>
<button type="submit"
class="w-full bg-blue-500 text-white p-3 rounded-xl font-semibold">
MASUK
</button>
</form>
<p class="text-sm text-center mt-5">
Belum punya akun?
<a href="register.php" class="text-green-500 font-semibold">Daftar</a>
</p>
</div>
<!-- 🔥 FIREBASE LOGIN -->
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
import {
getAuth,
signInWithEmailAndPassword
} from "https://www.gstatic.com/firebasejs/10.12.2/firebase-auth.js";
// 🔥 CONFIG (ISI PUNYAMU)
const firebaseConfig = {
apiKey: "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w", // ❗ FIX (tadi double ")
authDomain: "smartoven-f9fdd.firebaseapp.com",
projectId: "smartoven-f9fdd", // 🔥 ISI BENER
appId: "1:1005261996032:web:1bcb483b922275f08f98e5"
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
// 🔥 LOGIN PROCESS
const form = document.getElementById("loginForm");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const email = document.querySelector('[name="username"]').value;
const password = document.querySelector('[name="password"]').value;
try {
await signInWithEmailAndPassword(auth, email, password);
// 🔥 KIRIM KE PHP SESSION
await fetch("login_process.php", {
method: "POST",
headers: {"Content-Type": "application/x-www-form-urlencoded"},
body: `email=${email}`
});
window.location.href = "dashboard.php";
} catch (err) {
alert("Login gagal: " + err.message);
}
});
</script>
</body>
</html>

8
login_process.php Normal file
View File

@ -0,0 +1,8 @@
<?php
session_start();
$_SESSION['logged_in'] = true;
$_SESSION['user'] = $_POST['email'];
echo "ok";
?>

6
logout.php Normal file
View File

@ -0,0 +1,6 @@
<?php
session_start();
session_destroy();
header("Location: index.php");
exit;
?>

173
monitoring.php Normal file
View File

@ -0,0 +1,173 @@
<!-- HEADER -->
<div class="mb-6 flex justify-between items-center">
<div>
<h2 class="text-2xl font-bold text-gray-700">Dashboard Monitoring</h2>
<p class="text-gray-500 text-sm">Realtime IoT Fish Dryer System</p>
</div>
</div>
<!-- KPI -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<!-- Suhu -->
<div class="bg-white p-5 rounded-2xl shadow hover:shadow-xl hover:-translate-y-1 transition">
<div class="flex items-center justify-between">
<p class="text-gray-500">Suhu</p>
<i class="fas fa-thermometer-half text-red-400"></i>
</div>
<h2 id="suhu" class="text-3xl font-bold text-gray-700 mt-2">-- °C</h2>
</div>
<!-- Kelembaban -->
<div class="bg-white p-5 rounded-2xl shadow hover:shadow-xl hover:-translate-y-1 transition">
<div class="flex items-center justify-between">
<p class="text-gray-500">Kelembaban</p>
<i class="fas fa-tint text-blue-400"></i>
</div>
<h2 id="kelembaban" class="text-3xl font-bold text-gray-700 mt-2">-- %</h2>
</div>
</div>
<!-- GRAFIK -->
<div class="grid grid-cols-1 gap-6">
<!-- GRAFIK -->
<div class="bg-white p-6 rounded-2xl shadow h-[320px] flex flex-col">
<h3 class="text-lg font-semibold mb-2 text-gray-700">
Grafik Suhu & Kelembaban
</h3>
<div class="flex-1">
<canvas id="chart"></canvas>
</div>
</div>
</div>
<!-- STATUS DEVICE -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 mb-6">
<!-- Heater -->
<div class="bg-white border rounded-lg p-4 flex justify-between items-center">
<p class="text-gray-600">Heater</p>
<span id="heaterStatus" class="font-semibold text-gray-400">OFF</span>
</div>
<!-- Fan -->
<div class="bg-white border rounded-lg p-4 flex justify-between items-center">
<p class="text-gray-600">Fan</p>
<span id="fanStatus" class="font-semibold text-gray-400">OFF</span>
</div>
</div>
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
import { getDatabase, ref, onValue } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-database.js";
const firebaseConfig = {
apiKey: "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w",
authDomain: "smartoven-f9fdd.firebaseapp.com",
databaseURL: "https://smartoven-f9fdd-default-rtdb.firebaseio.com/",
projectId: "smartoven-f9fdd",
appId: "1:1005261996032:web:1bcb483b922275f08f98e5"
};
const app = initializeApp(firebaseConfig);
const db = getDatabase(app);
// ELEMENT
const suhuEl = document.getElementById("suhu");
const humEl = document.getElementById("kelembaban");
const heaterEl = document.getElementById("heaterStatus");
const fanEl = document.getElementById("fanStatus");
// ==========================
// 📊 CHART FIX
// ==========================
const chart = new Chart(document.getElementById("chart"), {
type: 'line',
data: {
labels: [],
datasets: [
{
label: "Suhu (°C)",
data: [],
borderColor: "#ef4444",
backgroundColor: "rgba(239,68,68,0.1)",
tension: 0.4,
fill: true
},
{
label: "Kelembaban (%)",
data: [],
borderColor: "#3b82f6",
backgroundColor: "rgba(59,130,246,0.1)",
tension: 0.4,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false
}
});
// ==========================
// 🔥 SENSOR REALTIME
// ==========================
onValue(ref(db, 'sensor'), (snapshot) => {
const data = snapshot.val();
if (!data) return;
const time = new Date().toLocaleTimeString();
// 🔥 SESUAIKAN FIELD FIREBASE
const suhu = data.temperature ?? data.suhu ?? 0;
const hum = data.humidity ?? data.kelembaban ?? 0;
// tampil ke card
suhuEl.innerText = suhu + " °C";
humEl.innerText = hum + " %";
// masuk ke chart
chart.data.labels.push(time);
chart.data.datasets[0].data.push(suhu);
chart.data.datasets[1].data.push(hum);
// batasi data (biar ga numpuk)
if (chart.data.labels.length > 10) {
chart.data.labels.shift();
chart.data.datasets.forEach(ds => ds.data.shift());
}
chart.update();
});
onValue(ref(db, 'manual'), (snapshot) => {
const data = snapshot.val();
if (!data) return;
// HEATER
if (data.heater == 1) {
heaterEl.innerText = "🔥 ON";
heaterEl.classList.remove("text-gray-400");
heaterEl.classList.add("text-red-500");
} else {
heaterEl.innerText = "OFF";
heaterEl.classList.remove("text-red-500");
heaterEl.classList.add("text-gray-400");
}
// FAN
if (data.fan == 1) {
fanEl.innerText = "🌀 ON";
fanEl.classList.remove("text-gray-400");
fanEl.classList.add("text-blue-500");
} else {
fanEl.innerText = "OFF";
fanEl.classList.remove("text-blue-500");
fanEl.classList.add("text-gray-400");
}
});
</script>

141
register.php Normal file
View File

@ -0,0 +1,141 @@
<?php session_start(); ?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - Pengering Ikan IoT</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
body { font-family: 'Poppins', sans-serif; }
</style>
</head>
<body style="
background-image: url('assets/img/bg.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
"></body>
<body class="bg-[#f5f7fa] flex items-center justify-center min-h-screen">
<div class="bg-white p-8 rounded-3xl shadow-sm w-full max-w-md">
<h2 class="text-2xl font-bold text-center mb-6">Daftar Akun</h2>
<form id="registerForm" class="space-y-4">
<div>
<label class="text-sm">Username</label>
<input name="reg_username" type="text"
class="w-full mt-1 p-3 border rounded-xl"
placeholder="Masukkan username" required>
</div>
<div>
<label class="text-sm">Email</label>
<input name="reg_email" type="email"
class="w-full mt-1 p-3 border rounded-xl"
placeholder="email@gmail.com" required>
</div>
<div>
<label class="text-sm">Password</label>
<input name="reg_password" type="password"
class="w-full mt-1 p-3 border rounded-xl"
placeholder="••••••" required>
</div>
<div>
<label class="text-sm">Konfirmasi Password</label>
<input name="reg_confirm" type="password"
class="w-full mt-1 p-3 border rounded-xl"
placeholder="••••••" required>
</div>
<button type="submit"
class="w-full bg-green-500 text-white p-3 rounded-xl font-semibold">
DAFTAR
</button>
</form>
<p class="text-sm text-center mt-5">
Sudah punya akun?
<a href="index.php" class="text-blue-500 font-semibold">Login</a>
</p>
</div>
<!-- 🔥 FIREBASE REGISTER -->
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
import {
getAuth,
createUserWithEmailAndPassword
} from "https://www.gstatic.com/firebasejs/10.12.2/firebase-auth.js";
import {
getDatabase,
ref,
set
} from "https://www.gstatic.com/firebasejs/10.12.2/firebase-database.js";
// 🔥 CONFIG (ISI PUNYAMU)
const firebaseConfig = {
apiKey: "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w",
authDomain: "smartoven-f9fdd.firebaseapp.com",
databaseURL: "https://smartoven-f9fdd-default-rtdb.firebaseio.com/",
projectId: "smartoven-f9fdd",
appId: "1:1005261996032:web:1bcb483b922275f08f98e5"
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getDatabase(app);
// 🔥 REGISTER PROCESS
const form = document.getElementById("registerForm");
form.addEventListener("submit", async (e) => {
e.preventDefault();
const username = document.querySelector('[name="reg_username"]').value;
const email = document.querySelector('[name="reg_email"]').value;
const password = document.querySelector('[name="reg_password"]').value;
const confirm = document.querySelector('[name="reg_confirm"]').value;
if (password !== confirm) {
alert("Password tidak sama!");
return;
}
try {
const userCred = await createUserWithEmailAndPassword(auth, email, password);
const user = userCred.user;
// 🔥 SIMPAN KE DATABASE
await set(ref(db, "users/" + user.uid), {
username: username,
email: email
});
alert("Registrasi berhasil!");
window.location.href = "index.php";
} catch (err) {
alert(err.message);
}
});
</script>
</body>
</html>

113
riwayat.php Normal file
View File

@ -0,0 +1,113 @@
<h2 class="text-2xl font-bold mb-6 text-gray-700">Riwayat Sensor</h2>
<div class="bg-white p-6 rounded-2xl shadow hover:shadow-xl transition">
<!-- HEADER -->
<div class="flex flex-col md:flex-row md:justify-between md:items-center gap-3 mb-4">
<div>
<p class="text-gray-700 font-semibold">Data Sensor</p>
<p class="text-gray-400 text-sm">Realtime dari perangkat IoT</p>
</div>
<input type="text" placeholder="🔍 Cari data..."
class="border border-gray-200 px-4 py-2 rounded-xl text-sm focus:ring-2 focus:ring-blue-400 outline-none w-full md:w-64">
</div>
<!-- TABLE -->
<div class="overflow-y-auto max-h-72 rounded-xl border border-gray-100">
<table class="w-full text-sm">
<thead class="bg-gray-50 text-gray-500 uppercase text-xs sticky top-0">
<tr>
<th>Waktu</th>
<th>Suhu</th>
<th>Kelembaban</th>
<th>Heater</th>
<th>Fan</th>
<th>Mode</th>
<th>Active</th>
</tr>
</thead>
<tbody id="historyTable" class="divide-y text-gray-600">
</tbody>
</table>
</div>
</div>
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-app.js";
import { getDatabase, ref, onValue } from "https://www.gstatic.com/firebasejs/10.12.2/firebase-database.js";
const firebaseConfig = {
apiKey: "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w",
authDomain: "smartoven-f9fdd.firebaseapp.com",
databaseURL: "https://smartoven-f9fdd-default-rtdb.firebaseio.com/",
projectId: "smartoven-f9fdd",
appId: "1:1005261996032:web:1bcb483b922275f08f98e5"
};
const app = initializeApp(firebaseConfig);
const db = getDatabase(app);
const table = document.getElementById("historyTable");
function fmtDateTime(epochSec) {
const d = new Date(Number(epochSec) * 1000);
// contoh: 20/05/2026 14.32.10 (format lokal)
return d.toLocaleString('id-ID');
}
function fmtNum(n, digits = 1) {
const x = Number(n);
if (!Number.isFinite(x)) return '--';
return x.toFixed(digits);
}
// ==========================
// 🔥 LOAD HISTORY
// ==========================
onValue(ref(db, 'history'), (snapshot) => {
const data = snapshot.val();
if (!data) {
table.innerHTML = `
<tr>
<td colspan="7" class="text-center py-4 text-gray-400">
Belum ada data
</td>
</tr>
`;
return;
}
table.innerHTML = "";
const rows = Object.keys(data)
.map((key) => ({ key, ...data[key] }))
.filter((r) => r && r.ts != null)
.sort((a, b) => Number(b.ts) - Number(a.ts));
rows.forEach((r) => {
const modeText = Number(r.mode) === 1 ? 'MANUAL' : 'AUTO';
const heaterText = Number(r.heater) === 1 ? 'ON' : 'OFF';
const fanText = Number(r.fan) === 1 ? 'ON' : 'OFF';
const activeText = Number(r.active) === 1 ? 'YA' : 'TIDAK';
table.innerHTML += `
<tr class="hover:bg-gray-50">
<td class="px-4 py-2 whitespace-nowrap">${fmtDateTime(r.ts)}</td>
<td class="px-4 py-2 whitespace-nowrap">${fmtNum(r.temp)} °C</td>
<td class="px-4 py-2 whitespace-nowrap">${fmtNum(r.hum)} %</td>
<td class="px-4 py-2 whitespace-nowrap">${heaterText}</td>
<td class="px-4 py-2 whitespace-nowrap">${fanText}</td>
<td class="px-4 py-2 whitespace-nowrap">${modeText}</td>
<td class="px-4 py-2 whitespace-nowrap">${activeText}</td>
</tr>
`;
});
});
</script>