From c85f4c6b2db44df34f304ae51f22b13cd1184ab7 Mon Sep 17 00:00:00 2001 From: "[Valentino Heman Budiarto]" <[hemanvalentino@gmail.com]> Date: Mon, 15 Jun 2026 17:32:43 +0700 Subject: [PATCH] hardware n monitoring --- backend/cmd/main.go | 2 + backend/controllers/hardwarecontroller.go | 95 ++++-- frontend/app/admin/monitoring/page.tsx | 368 +++++++++++----------- 3 files changed, 257 insertions(+), 208 deletions(-) diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 73590b5..eb22e4f 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -74,6 +74,8 @@ func main() { r.GET("/api/hardware/power-status", controllers.GetPowerStatus) + r.POST("/api/cutoff", controllers.CutOffAllPower) + r.Run(":8080") } diff --git a/backend/controllers/hardwarecontroller.go b/backend/controllers/hardwarecontroller.go index b8c79ff..25ef30f 100644 --- a/backend/controllers/hardwarecontroller.go +++ b/backend/controllers/hardwarecontroller.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "os" + "strconv" // Digunakan untuk mengubah teks Watt menjadi angka "time" "s-class-backend/config" // Import database @@ -170,7 +171,7 @@ func ControlHardware(c *gin.Context) { return } - // 🌟 BLOK 1: KONTROL GEMBOK / OTORISASI RELAY (SANGAT PENTING) + // 🌟 BLOK 1: KONTROL GEMBOK / OTORISASI RELAY if req.Device == "auth" { broker := os.Getenv("MQTT_BROKER") user := os.Getenv("MQTT_USER") @@ -179,7 +180,6 @@ func ControlHardware(c *gin.Context) { opts := mqtt.NewClientOptions().AddBroker(broker).SetUsername(user).SetPassword(pass).SetClientID(fmt.Sprintf("Golang-Lock-%d", time.Now().Unix())) client := mqtt.NewClient(opts) if token := client.Connect(); token.Wait() && token.Error() == nil { - // Retain = true untuk menimpa status UNLOCK lama di server MQTT client.Publish("sclass/d101/auth", 0, true, req.Action).Wait() client.Disconnect(250) fmt.Println("[MQTT] Status Gembok Relay diubah menjadi:", req.Action) @@ -263,45 +263,100 @@ func ControlHardware(c *gin.Context) { } // ========================================================================= -// FUNGSI 3: MENDAPATKAN STATUS DAYA DARI HOME ASSISTANT +// HELPER: MENGAMBIL STATE DARI HOME ASSISTANT // ========================================================================= -func GetPowerStatus(c *gin.Context) { - haURL := os.Getenv("HA_URL") - haToken := os.Getenv("HA_TOKEN") - - entityID := "sensor.kwh_meter_power" +func fetchHAState(haURL string, haToken string, entityID string) float64 { apiURL := fmt.Sprintf("%s/api/states/%s", haURL, entityID) req, _ := http.NewRequest("GET", apiURL, nil) req.Header.Set("Authorization", "Bearer "+haToken) req.Header.Set("Content-Type", "application/json") - client := &http.Client{} + client := &http.Client{Timeout: 3 * time.Second} resp, err := client.Do(req) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"power": 0}) - return + return 0 } defer resp.Body.Close() var result map[string]interface{} - json.NewDecoder(resp.Body).Decode(&result) - - powerStr, ok := result["state"].(string) - if !ok { - c.JSON(http.StatusOK, gin.H{"power": 0}) - return + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return 0 } - c.JSON(http.StatusOK, gin.H{"power": powerStr}) + stateStr, ok := result["state"].(string) + if !ok { + return 0 + } + + // Ubah string angka menjadi float + val, _ := strconv.ParseFloat(stateStr, 64) + return val } // ========================================================================= -// FUNGSI 4: MENGIRIM STATUS REAL-TIME KE WEB FRONTEND +// FUNGSI 3: MENDAPATKAN STATUS DAYA DARI 3 MCB SEKALIGUS +// ========================================================================= +func GetPowerStatus(c *gin.Context) { + haURL := os.Getenv("HA_URL") + haToken := os.Getenv("HA_TOKEN") + + powerUmum := fetchHAState(haURL, haToken, "sensor.wifi_smart_meter_pro_power") + powerAC1 := fetchHAState(haURL, haToken, "sensor.wifi_smart_meter_pro_2_power") + powerAC2 := fetchHAState(haURL, haToken, "sensor.wifi_smart_meter_pro_3_power") + + c.JSON(http.StatusOK, gin.H{ + "umum": powerUmum, + "ac1": powerAC1, + "ac2": powerAC2, + }) +} + +// ========================================================================= +// FUNGSI 4: CUT OFF SEMUA POWER (MATIKAN 3 MCB) +// ========================================================================= +func CutOffAllPower(c *gin.Context) { + haURL := os.Getenv("HA_URL") + haToken := os.Getenv("HA_TOKEN") + + apiURL := fmt.Sprintf("%s/api/services/switch/turn_off", haURL) + + 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", + }, + } + + 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") + + 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 + } + defer resp.Body.Close() + + // Update cache + 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"}) +} + +// ========================================================================= +// FUNGSI 5: MENGIRIM STATUS REAL-TIME KE WEB FRONTEND // ========================================================================= func GetHardwareStatus(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "success", "data": DeviceStatusCache, }) -} +} \ No newline at end of file diff --git a/frontend/app/admin/monitoring/page.tsx b/frontend/app/admin/monitoring/page.tsx index 5b5c173..dd36ed2 100644 --- a/frontend/app/admin/monitoring/page.tsx +++ b/frontend/app/admin/monitoring/page.tsx @@ -1,34 +1,48 @@ "use client"; import { useState, useEffect } from "react"; -import { Activity, Power, ZapOff, AlertTriangle, Lightbulb, Wind, Projector } from "lucide-react"; +import { Activity, Power, AlertTriangle, Lightbulb, Wind, Projector, Zap } from "lucide-react"; export default function PowerMonitoringPage() { // ========================================================================= - // 1. DEKLARASI STATE + // 1. DEKLARASI STATE UNTUK KELAS D101 // ========================================================================= - const [rooms, setRooms] = useState([]); - const [roomDeviceStatus, setRoomDeviceStatus] = useState<{ [roomId: string]: { [deviceName: string]: boolean } }>({}); + const [powerData, setPowerData] = useState({ umum: 0, ac1: 0, ac2: 0 }); + const [deviceStatus, setDeviceStatus] = useState({ + "Lampu 1": false, + "Lampu 2": false, + "AC": false, + "Proyektor": false, + }); + const [isRelayOn, setIsRelayOn] = useState(true); // Status apakah ruangan terkunci/terbuka + const [lastUpdate, setLastUpdate] = useState("Menunggu data..."); // ========================================================================= - // 2. FUNGSI FETCH DATA DARI BACKEND + // 2. FUNGSI FETCH DATA DARI BACKEND GOLANG // ========================================================================= - // A. Tarik Data Daya (Power / Watt) + // A. Tarik Data Daya dari 3 MCB (Umum, AC1, AC2) const fetchPowerStatus = async () => { try { + // Pastikan route ini sesuai dengan router.GET di main.go Golang kamu const response = await fetch("http://172.17.172.17:8080/api/hardware/power-status"); - const data = await response.json(); - - setRooms(prev => prev.map(room => - room.name === "Kelas D101" ? { ...room, power: parseFloat(data.power) || 0 } : room - )); + if (response.ok) { + const data = await response.json(); + setPowerData({ + umum: parseFloat(data.umum) || 0, + ac1: parseFloat(data.ac1) || 0, + ac2: parseFloat(data.ac2) || 0, + }); + + const now = new Date(); + setLastUpdate(`${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`); + } } catch (err) { console.error("Gagal mengambil data daya:", err); } }; - // B. Tarik Data Status Perangkat (Sinkronisasi Real-time Antar Admin) + // B. Tarik Data Status Perangkat IoT const fetchDeviceStatus = async () => { try { const response = await fetch("http://172.17.172.17:8080/api/hardware/status"); @@ -36,18 +50,14 @@ export default function PowerMonitoringPage() { if (result.status === "success") { const backendData = result.data; - - // Kita terjemahkan data Golang ("on"/"off") menjadi boolean (true/false) - setRoomDeviceStatus(prev => ({ - ...prev, - "room_1": { - ...prev["room_1"], - "Lampu 1": backendData.lampu1 === "on", - "Lampu 2": backendData.lampu2 === "on", - "AC": backendData.ac === "on", - "Proyektor": backendData.projector === "on", - } - })); + setDeviceStatus({ + "Lampu 1": backendData.lampu1 === "on", + "Lampu 2": backendData.lampu2 === "on", + "AC": backendData.ac === "on", + "Proyektor": backendData.projector === "on", + }); + // Jika semua mati dan gemboknya aktif, bisa kita asumsikan ruangan off + // Tergantung bagaimana logika "auth" kamu di frontend } } catch (err) { console.error("Gagal sinkronisasi status perangkat:", err); @@ -55,75 +65,42 @@ export default function PowerMonitoringPage() { }; // ========================================================================= - // 3. INISIALISASI & POLLING (AUTO-REFRESH) + // 3. INISIALISASI & POLLING (AUTO-REFRESH 3 DETIK) // ========================================================================= useEffect(() => { - // Simulasi Data Ruangan - const dummyRooms = [ - { id: 1, name: "Kelas D101", power: 0, isRelayOn: true, lastUpdate: "Real-time" }, - { id: 2, name: "Kelas D102", power: 0, isRelayOn: false, lastUpdate: "2 mnt lalu" }, - { id: 3, name: "Kelas D103", power: 45, isRelayOn: true, lastUpdate: "Baru saja" }, - { id: 4, name: "Kelas D104", power: 0, isRelayOn: false, lastUpdate: "10 mnt lalu" }, - ]; - setRooms(dummyRooms); - - // Inisialisasi status default semua device OFF - const initialStatus: { [roomId: string]: { [deviceName: string]: boolean } } = {}; - dummyRooms.forEach(room => { - initialStatus[`room_${room.id}`] = { - "Lampu 1": false, - "Lampu 2": false, - "AC": false, - "Proyektor": false, - }; - }); - setRoomDeviceStatus(initialStatus); - - // Tarik data pertama kali & set Interval 2 detik fetchPowerStatus(); fetchDeviceStatus(); const interval = setInterval(() => { fetchPowerStatus(); fetchDeviceStatus(); - }, 2000); + }, 3000); return () => clearInterval(interval); }, []); // ========================================================================= - // 4. FUNGSI KONTROL DEVICE (TOGGLE ON/OFF) + // 4. FUNGSI KONTROL DEVICE & CUT OFF // ========================================================================= - const handleDeviceToggle = async (roomId: number, roomName: string, deviceName: string) => { - const roomIdKey = `room_${roomId}`; - const currentStatus = roomDeviceStatus[roomIdKey]?.[deviceName] || false; - - // Logika Pintar: Jika sedang ON, maka klik selanjutnya adalah OFF. Sebaliknya. + const handleDeviceToggle = async (deviceName: string) => { + // Tentukan kunci array state yang benar + const key = deviceName as keyof typeof deviceStatus; + const currentStatus = deviceStatus[key]; const actionType = currentStatus ? "off" : "on"; - // Konfirmasi mematikan (Biar admin gak salah pencet) if (currentStatus) { - const confirmMsg = `Apakah Anda yakin ingin mematikan ${deviceName} di ${roomName}?`; - if (!window.confirm(confirmMsg)) return; // Jika di-cancel, berhenti di sini + if (!window.confirm(`Apakah Anda yakin ingin mematikan ${deviceName}?`)) return; } - // Mapping nama device untuk backend let backendDevice = ""; if (deviceName === 'AC') backendDevice = "ac"; else if (deviceName === 'Proyektor') backendDevice = "projector"; else if (deviceName === 'Lampu 1') backendDevice = "lampu1"; else if (deviceName === 'Lampu 2') backendDevice = "lampu2"; - // 1. UBAH UI SEKETIKA (Optimistic UI) agar layar merespons tanpa delay - setRoomDeviceStatus(prev => ({ - ...prev, - [roomIdKey]: { - ...prev[roomIdKey], - [deviceName]: !currentStatus, - }, - })); + // Optimistic UI (Berubah cepat di layar) + setDeviceStatus(prev => ({ ...prev, [key]: !currentStatus })); - // 2. KIRIM KE GOLANG try { const response = await fetch("http://172.17.172.17:8080/api/hardware/control", { method: "POST", @@ -131,153 +108,168 @@ export default function PowerMonitoringPage() { body: JSON.stringify({ device: backendDevice, action: actionType }), }); - // 3. JIKA GAGAL, KEMBALIKAN UI KE POSISI SEMULA if (!response.ok) { - setRoomDeviceStatus(prev => ({ - ...prev, - [roomIdKey]: { - ...prev[roomIdKey], - [deviceName]: currentStatus, - }, - })); + setDeviceStatus(prev => ({ ...prev, [key]: currentStatus })); const errorData = await response.json(); alert(`GAGAL: ${errorData.error || "Server menolak perintah"}`); } } catch (error) { console.error("Error API:", error); - // Rollback UI jika koneksi mati - setRoomDeviceStatus(prev => ({ - ...prev, - [roomIdKey]: { - ...prev[roomIdKey], - [deviceName]: currentStatus, - }, - })); + setDeviceStatus(prev => ({ ...prev, [key]: currentStatus })); alert("GAGAL: Tidak dapat terhubung ke Server Golang."); } }; - const handleCutOff = (roomName: string) => { - if (window.confirm(`PERINGATAN: Anda yakin ingin mematikan daya secara paksa di ${roomName}?`)) { - alert(`Sinyal pemutusan daya dikirim ke Relay Master ${roomName}.`); + // FUNGSI CUT OFF 3 MCB SEKALIGUS + const handleCutOff = async () => { + if (!window.confirm(`PERINGATAN FATAL: Anda yakin ingin memutus 3 MCB Daya di Kelas D101 secara paksa?`)) return; + + try { + const response = await fetch("http://172.17.172.17:8080/api/cutoff", { + method: "POST", + }); + + if (response.ok) { + alert(`Daya di Kelas D101 berhasil diputus!`); + setIsRelayOn(false); + fetchPowerStatus(); // Paksa update layar agar jadi 0 Watt + fetchDeviceStatus(); // Paksa update icon perangkat mati + } else { + alert("Gagal memutus daya. Cek koneksi server."); + } + } catch (error) { + console.error(error); + alert("Terjadi kesalahan jaringan saat memutus daya."); } }; + // Hitung total daya + const totalPower = powerData.umum + powerData.ac1 + powerData.ac2; + // ========================================================================= // 5. TAMPILAN UI (RENDER) // ========================================================================= return ( -
-
-
- +
+ {/* HEADER SECTION */} +
+
+
+ +
+
+

S-CLASS Power Monitoring

+

Pantau beban 3 MCB utama & kendalikan perangkat Kelas D101.

+
-
-

Power Monitoring & Control

-

Pantau konsumsi daya kWh meter dan kendalikan relay sirkuit ruangan.

+ + +
+ + {/* TOTAL POWER SUMMARY */} +
+
+
+ +
+
+

Total Konsumsi Daya (D101)

+

Update terakhir: {lastUpdate}

+
+
+
+ 3000 ? 'text-red-400' : 'text-yellow-400'}`}> + {totalPower.toFixed(1)} + + Watts
-
- {[...rooms].sort((a, b) => a.name.localeCompare(b.name)).map((room) => { + {/* 3 MCB MONITORING CARDS */} +
+ {/* MCB UMUM */} +
+
MCB 1
+

Jalur Umum

+

Lampu, Proyektor & Stop Kontak

+
+ {powerData.umum.toFixed(1)} + W +
+
+ + {/* MCB AC 1 */} +
+
MCB 2
+

Pendingin 1

+

Air Conditioner Unit 1

+
+ {powerData.ac1.toFixed(1)} + W +
+
+ + {/* MCB AC 2 */} +
+
MCB 3
+

Pendingin 2

+

Air Conditioner Unit 2

+
+ {powerData.ac2.toFixed(1)} + W +
+
+
+ + {totalPower > 3500 && ( +
+ PERINGATAN: Beban Kelas D101 melebihi batas aman (3500W)! +
+ )} + + {/* IOT DEVICE CONTROLS */} +
+

Kontrol Perangkat Kelas D101

+
- const roomIdKey = `room_${room.id}`; - const currentRoomStatus = roomDeviceStatus[roomIdKey] || {}; + - return ( -
- -
- {room.isRelayOn ? 'Sirkuit Aktif' : 'Sirkuit Terputus'} -
+ -

{room.name}

- -
- 1000 ? 'text-orange-500' : 'text-gray-800'}`}> - {room.power} - - Watts -
+ - {room.power > 1000 && ( -
- Beban Tinggi Terdeteksi! -
- )} + -
-

IoT Device Control

-
- - {/* LAMPU 1 */} - - - {/* LAMPU 2 */} - - - {/* AC */} - - - {/* PROYEKTOR */} - - -
-
- -
- Update: {room.lastUpdate} - -
-
- ); - })} +
);