From 122115b5e9be5afd399757219d384b34fbfd58d6 Mon Sep 17 00:00:00 2001 From: "[Valentino Heman Budiarto]" <[hemanvalentino@gmail.com]> Date: Mon, 22 Jun 2026 23:45:01 +0700 Subject: [PATCH] update timer auto cut off mcb --- backend/cmd/main.go | 9 ++- backend/controllers/timercontroller.go | 66 +++++++++++++++ frontend/app/admin/monitoring/page.tsx | 108 +++++++++++++++++++++++-- 3 files changed, 174 insertions(+), 9 deletions(-) create mode 100644 backend/controllers/timercontroller.go diff --git a/backend/cmd/main.go b/backend/cmd/main.go index a638c3d..b253d2c 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -14,6 +14,9 @@ func main() { // 1. Konek Database config.ConnectDatabase() + // 🌟 TAMBAHAN UNTUK TIMER: Nyalakan mesin Cron Timer di background + controllers.StartPowerCronWorker() + // 2. AutoMigrate config.DB.AutoMigrate(&models.User{}, &models.Room{}, &models.Booking{}, &models.ClassSchedule{}) @@ -65,7 +68,7 @@ func main() { protected.DELETE("/schedules/:id", controllers.DeleteSchedule) } - // 5. Jalur IoT ESP32 + // 5. Jalur IoT ESP32 & Kontrol Daya r.POST("/api/sensor/energy", controllers.UpdateRoomPower) r.POST("/api/hardware/verify", controllers.VerifyHardwareCode) r.POST("/api/hardware/control", controllers.ControlHardware) @@ -73,6 +76,10 @@ func main() { r.GET("/api/hardware/power-status", controllers.GetPowerStatus) r.POST("/api/power/global", controllers.GlobalPowerControl) + // 🌟 RUTE BARU UNTUK TIMER AUTO-CUTOFF + r.GET("/api/power/timer", controllers.GetTimerConfig) + r.POST("/api/power/timer", controllers.SetTimerConfig) + r.Run(":8080") } diff --git a/backend/controllers/timercontroller.go b/backend/controllers/timercontroller.go new file mode 100644 index 0000000..f9fb2d7 --- /dev/null +++ b/backend/controllers/timercontroller.go @@ -0,0 +1,66 @@ +package controllers + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +// Struktur Data Timer +type AutoTimer struct { + IsActive bool `json:"is_active"` + OffTime string `json:"off_time"` + OnTime string `json:"on_time"` +} + +// Disimpan di RAM sementara agar cepat (reset saat server mati) +var AppTimer = AutoTimer{IsActive: false, OffTime: "22:00", OnTime: "05:00"} + +// API: Ambil Pengaturan Timer +func GetTimerConfig(c *gin.Context) { + c.JSON(http.StatusOK, AppTimer) +} + +// API: Simpan Pengaturan Timer +func SetTimerConfig(c *gin.Context) { + if err := c.ShouldBindJSON(&AppTimer); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Data tidak valid"}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "Jadwal Auto-Cutoff berhasil disimpan", "data": AppTimer}) +} + +// MESIN CRON: Berjalan di latar belakang mengecek jam setiap menit +func StartPowerCronWorker() { + go func() { + ticker := time.NewTicker(1 * time.Minute) + for range ticker.C { + if !AppTimer.IsActive { + continue + } + + loc, _ := time.LoadLocation("Asia/Jakarta") + now := time.Now().In(loc).Format("15:04") // Format 24 Jam (HH:MM) + + if now == AppTimer.OffTime { + fmt.Println("🕰️ [TIMER] Waktunya OFF! Memutus daya...") + triggerGlobalPower("off") + } else if now == AppTimer.OnTime { + fmt.Println("🕰️ [TIMER] Waktunya ON! Menyalakan daya...") + triggerGlobalPower("on") + } + } + }() +} + +// Helper untuk menembak API Global Power yang sudah kamu miliki +func triggerGlobalPower(action string) { + payload := map[string]string{"action": action} + jsonPayload, _ := json.Marshal(payload) + // Memanggil API lokal kita sendiri + http.Post("http://127.0.0.1:8080/api/power/global", "application/json", bytes.NewBuffer(jsonPayload)) +} \ No newline at end of file diff --git a/frontend/app/admin/monitoring/page.tsx b/frontend/app/admin/monitoring/page.tsx index b632435..a4bbab2 100644 --- a/frontend/app/admin/monitoring/page.tsx +++ b/frontend/app/admin/monitoring/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect } from "react"; -import { Activity, Power, ZapOff, Zap, AlertTriangle, Lightbulb, Wind, Projector } from "lucide-react"; +import { Activity, Power, ZapOff, Zap, AlertTriangle, Lightbulb, Wind, Projector, Clock } from "lucide-react"; export default function PowerMonitoringPage() { // ========================================================================= @@ -13,6 +13,10 @@ export default function PowerMonitoringPage() { // State khusus untuk menampung rincian 3 MCB di D101 const [powerDataD101, setPowerDataD101] = useState({ umum: 0, ac1: 0, ac2: 0 }); + // 🌟 TAMBAHAN STATE UNTUK TIMER AUTO-CUTOFF + const [timer, setTimer] = useState({ is_active: false, off_time: "22:00", on_time: "05:00" }); + const [isSavingTimer, setIsSavingTimer] = useState(false); + // ========================================================================= // 2. FUNGSI FETCH DATA DARI BACKEND // ========================================================================= @@ -66,6 +70,17 @@ export default function PowerMonitoringPage() { } }; + // 🌟 C. Tarik Data Timer dari Golang + const fetchTimer = async () => { + try { + const res = await fetch("http://172.17.172.17:8080/api/power/timer"); + const data = await res.json(); + setTimer(data); + } catch (e) { + console.error("Gagal mengambil data timer:", e); + } + }; + // ========================================================================= // 3. INISIALISASI & POLLING (AUTO-REFRESH) // ========================================================================= @@ -91,6 +106,8 @@ export default function PowerMonitoringPage() { }); setRoomDeviceStatus(initialStatus); + // Panggil fungsi awal + fetchTimer(); fetchPowerStatus(); fetchDeviceStatus(); @@ -103,8 +120,26 @@ export default function PowerMonitoringPage() { }, []); // ========================================================================= - // 4. FUNGSI KONTROL DEVICE & CUT OFF + // 4. FUNGSI KONTROL DEVICE, GLOBAL POWER & SIMPAN TIMER // ========================================================================= + + // 🌟 D. Simpan Jadwal Timer ke Golang + const saveTimer = async () => { + setIsSavingTimer(true); + try { + const res = await fetch("http://172.17.172.17:8080/api/power/timer", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(timer) + }); + if (res.ok) alert("Jadwal Auto-Cutoff Berhasil Disimpan!"); + } catch (e) { + alert("Gagal menyimpan jadwal timer."); + } finally { + setIsSavingTimer(false); + } + }; + const handleDeviceToggle = async (roomId: number, roomName: string, deviceName: string) => { const roomIdKey = `room_${roomId}`; const currentStatus = roomDeviceStatus[roomIdKey]?.[deviceName] || false; @@ -115,13 +150,11 @@ export default function PowerMonitoringPage() { if (!window.confirm(confirmMsg)) return; } - // Jika yang ditekan bukan D101, ubah UI saja (simulasi) if (roomName !== "Kelas D101") { setRoomDeviceStatus(prev => ({ ...prev, [roomIdKey]: { ...prev[roomIdKey], [deviceName]: !currentStatus } })); return; } - // Logika khusus D101 (Tembak ke Golang) let backendDevice = ""; if (deviceName === 'AC') backendDevice = "ac"; else if (deviceName === 'Proyektor') backendDevice = "projector"; @@ -147,7 +180,6 @@ export default function PowerMonitoringPage() { } }; - // FUNGSI GLOBAL POWER (ON/OFF KHUSUS D101) const handleGlobalPower = async (roomName: string, roomId: number, currentRelayStatus: boolean) => { if (roomName === "Kelas D101") { const actionType = currentRelayStatus ? "off" : "on"; @@ -157,7 +189,6 @@ export default function PowerMonitoringPage() { if (!window.confirm(confirmMessage)) return; - // Ubah UI seketika setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: !currentRelayStatus } : r)); try { @@ -172,7 +203,6 @@ export default function PowerMonitoringPage() { fetchPowerStatus(); fetchDeviceStatus(); } else { - // Rollback UI jika gagal setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: currentRelayStatus } : r)); alert("Gagal mengontrol MCB utama. Cek koneksi server."); } @@ -181,7 +211,6 @@ export default function PowerMonitoringPage() { alert("Terjadi kesalahan jaringan saat menghubungi server."); } } else { - // Dummy untuk kelas lain const actionType = currentRelayStatus ? "Mematikan" : "Menyalakan"; if (window.confirm(`Simulasi ${actionType} daya di ${roomName}?`)) { setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: !currentRelayStatus, power: 0 } : r)); @@ -204,6 +233,69 @@ export default function PowerMonitoringPage() { + {/* 🌟 PANEL BARU: NIGHT MODE (AUTO-CUTOFF TIMER) */} +
+ +
+
+ +
+
+

Global Night Mode Auto-Cutoff

+

Otomatis putus daya listrik (MCB) seluruh kelas pada jam malam.

+
+
+ +
+ {/* Input Jam */} +
+
+ + setTimer({...timer, off_time: e.target.value})} + className="bg-slate-900 border border-slate-600 rounded px-2 py-1 text-sm outline-none focus:border-blue-500 transition-colors" + disabled={timer.is_active} + /> +
+ - +
+ + setTimer({...timer, on_time: e.target.value})} + className="bg-slate-900 border border-slate-600 rounded px-2 py-1 text-sm outline-none focus:border-blue-500 transition-colors" + disabled={timer.is_active} + /> +
+
+ + {/* Tombol Aksi */} +
+ + + +
+
+
+ {/* 🌟 AKHIR PANEL NIGHT MODE */} +
{[...rooms].sort((a, b) => a.name.localeCompare(b.name)).map((room) => {