From 5e38ec30bc44ca9f63f067386834511865e3d181 Mon Sep 17 00:00:00 2001 From: "[Valentino Heman Budiarto]" <[hemanvalentino@gmail.com]> Date: Mon, 15 Jun 2026 17:57:30 +0700 Subject: [PATCH] hardware n monitoring --- backend/cmd/main.go | 2 +- backend/controllers/hardwarecontroller.go | 82 ++++++++++++++++------- frontend/app/admin/monitoring/page.tsx | 46 ++++++++----- 3 files changed, 89 insertions(+), 41 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index eb22e4f..d51398b 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -74,7 +74,7 @@ func main() { r.GET("/api/hardware/power-status", controllers.GetPowerStatus) - r.POST("/api/cutoff", controllers.CutOffAllPower) + r.POST("/api/power/global", controllers.GlobalPowerControl) r.Run(":8080") } diff --git a/backend/controllers/hardwarecontroller.go b/backend/controllers/hardwarecontroller.go index 25ef30f..4950823 100644 --- a/backend/controllers/hardwarecontroller.go +++ b/backend/controllers/hardwarecontroller.go @@ -25,6 +25,11 @@ type VerifyRequest struct { Token string `json:"token"` } +// Tambahan untuk kontrol daya global (ON/OFF) +type GlobalPowerRequest struct { + Action string `json:"action"` // "on" atau "off" +} + // ========================================================================= // CACHE STATUS HARDWARE (Mengingat status terakhir perangkat untuk Web Admin) // ========================================================================= @@ -313,42 +318,69 @@ func GetPowerStatus(c *gin.Context) { } // ========================================================================= -// FUNGSI 4: CUT OFF SEMUA POWER (MATIKAN 3 MCB) +// FUNGSI 4: GLOBAL POWER CONTROL (Sequential ON/OFF dengan Jeda 1 Detik) // ========================================================================= -func CutOffAllPower(c *gin.Context) { +func GlobalPowerControl(c *gin.Context) { + var req GlobalPowerRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Format data tidak valid"}) + return + } + haURL := os.Getenv("HA_URL") haToken := os.Getenv("HA_TOKEN") - apiURL := fmt.Sprintf("%s/api/services/switch/turn_off", haURL) + // Tentukan service HA berdasarkan request + service := "turn_off" + if req.Action == "on" { + service = "turn_on" + } + apiURL := fmt.Sprintf("%s/api/services/switch/%s", haURL, service) - payload := map[string]interface{}{ - "entity_id": []string{ - "switch.wifi_smart_meter_pro_switch", - "switch.wifi_smart_meter_pro_2_switch", - "switch.wifi_smart_meter_pro_3_switch", - }, + // Daftar MCB yang akan dieksekusi secara berurutan + mcbs := []string{ + "switch.wifi_smart_meter_pro_switch", // Umum + "switch.wifi_smart_meter_pro_2_switch", // AC1 + "switch.wifi_smart_meter_pro_3_switch", // AC2 } - jsonPayload, _ := json.Marshal(payload) - req, _ := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonPayload)) - req.Header.Set("Authorization", "Bearer "+haToken) - req.Header.Set("Content-Type", "application/json") + // Looping untuk eksekusi satu per satu + for i, mcb := range mcbs { + payload := map[string]string{"entity_id": mcb} + jsonPayload, _ := json.Marshal(payload) - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Do(req) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Gagal menghubungi Home Assistant"}) - return + reqHA, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonPayload)) + if err == nil { + reqHA.Header.Set("Authorization", "Bearer "+haToken) + reqHA.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Do(reqHA) + if err == nil && resp != nil { + resp.Body.Close() + } + } + + fmt.Printf("[MCB] Sinyal %s dikirim ke %s\n", req.Action, mcb) + + // Berikan jeda 1 detik, KECUALI untuk iterasi terakhir + if i < len(mcbs)-1 { + time.Sleep(1 * time.Second) + } } - defer resp.Body.Close() - // Update cache - DeviceStatusCache["lampu1"] = "off" - DeviceStatusCache["lampu2"] = "off" - DeviceStatusCache["ac"] = "off" - DeviceStatusCache["projector"] = "off" + // Jika dimatikan paksa, update cache perangkat IoT menjadi mati semua + if req.Action == "off" { + DeviceStatusCache["lampu1"] = "off" + DeviceStatusCache["lampu2"] = "off" + DeviceStatusCache["ac"] = "off" + DeviceStatusCache["projector"] = "off" + } - c.JSON(http.StatusOK, gin.H{"status": "success", "message": "Semua daya ruangan (MCB) berhasil diputus"}) + c.JSON(http.StatusOK, gin.H{ + "status": "success", + "message": fmt.Sprintf("Seluruh daya ruangan berhasil di-%s secara berurutan", req.Action), + }) } // ========================================================================= diff --git a/frontend/app/admin/monitoring/page.tsx b/frontend/app/admin/monitoring/page.tsx index 09c6aed..b632435 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, AlertTriangle, Lightbulb, Wind, Projector } from "lucide-react"; +import { Activity, Power, ZapOff, Zap, AlertTriangle, Lightbulb, Wind, Projector } from "lucide-react"; export default function PowerMonitoringPage() { // ========================================================================= @@ -147,27 +147,44 @@ export default function PowerMonitoringPage() { } }; - // FUNGSI CUT OFF DAYA KHUSUS D101 - const handleCutOff = async (roomName: string, roomId: number) => { + // FUNGSI GLOBAL POWER (ON/OFF KHUSUS D101) + const handleGlobalPower = async (roomName: string, roomId: number, currentRelayStatus: boolean) => { if (roomName === "Kelas D101") { - if (!window.confirm(`PERINGATAN FATAL: Anda yakin ingin memutus 3 MCB Daya di ${roomName} secara paksa?`)) return; + const actionType = currentRelayStatus ? "off" : "on"; + const confirmMessage = currentRelayStatus + ? `PERINGATAN FATAL: Anda yakin ingin MEMUTUS 3 MCB Daya di ${roomName}? (Proses berurutan ~3 detik)` + : `RESTORE POWER: Anda yakin ingin MENYALAKAN KEMBALI 3 MCB Daya di ${roomName}? (Proses berurutan ~3 detik)`; + + if (!window.confirm(confirmMessage)) return; + // Ubah UI seketika + setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: !currentRelayStatus } : r)); + try { - const response = await fetch("http://172.17.172.17:8080/api/cutoff", { method: "POST" }); + const response = await fetch("http://172.17.172.17:8080/api/power/global", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: actionType }), + }); + if (response.ok) { - alert(`Daya di ${roomName} berhasil diputus!`); - setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: false, power: 0 } : r)); + alert(`Daya di ${roomName} berhasil di-${actionType.toUpperCase()}!`); fetchPowerStatus(); + fetchDeviceStatus(); } else { - alert("Gagal memutus daya. Cek koneksi server."); + // Rollback UI jika gagal + setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: currentRelayStatus } : r)); + alert("Gagal mengontrol MCB utama. Cek koneksi server."); } } catch (error) { - alert("Terjadi kesalahan jaringan saat memutus daya."); + setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: currentRelayStatus } : r)); + alert("Terjadi kesalahan jaringan saat menghubungi server."); } } else { // Dummy untuk kelas lain - if (window.confirm(`Simulasi mematikan daya di ${roomName}?`)) { - setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: false, power: 0 } : r)); + 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)); } } }; @@ -275,14 +292,13 @@ export default function PowerMonitoringPage() {
Update: {room.lastUpdate}