(function () { "use strict"; const POLL_MS = 3000; const DATA_URL = (window.NODERED_URL || "").replace(/\/$/, "") + "/get-data"; const SENSORS = { suhu: { valId:"val-suhu", barId:"bar-suhu", unit:"", decimals:1, max:45, color: v=>(v>32||v<18)?"#f59e0b":"#22c55e" }, ph: { valId:"val-ph", barId:"bar-ph", unit:" pH", decimals:2, max:14, color: v=>(v<6.5||v>8.5)?"#ef4444":"#22c55e" }, amonia: { valId:"val-amonia", barId:"bar-amonia", unit:" ppm", decimals:2, max:10, color: v=>v>2?"#ef4444":v>1?"#f59e0b":"#22c55e" }, kekeruhan: { valId:"val-kekeruhan", barId:"bar-kekeruhan", unit:" NTU", decimals:1, max:150, color: v=>v>80?"#ef4444":v>40?"#f59e0b":"#22c55e" }, distance: { valId:"val-distance", barId:"bar-distance", unit:" cm", decimals:1, max:100, color: v=>v>70?"#ef4444":v>50?"#f59e0b":"#22c55e" }, }; function updateSensor(key, raw) { const cfg = SENSORS[key]; if (!cfg || raw===null || raw===undefined) return; const v = parseFloat(raw); if (isNaN(v)) return; const valEl = document.getElementById(cfg.valId); const barEl = document.getElementById(cfg.barId); if (valEl) valEl.textContent = v.toFixed(cfg.decimals) + cfg.unit; if (barEl) { barEl.style.width = Math.min(100, Math.max(0, v/cfg.max*100)) + "%"; barEl.style.background = cfg.color(v); } } function updateDevice(badgeId, valId, raw) { const valEl = document.getElementById(valId); const badgeEl = document.getElementById(badgeId); if (!valEl || !badgeEl || raw===null || raw===undefined) return; const on = (raw==1 || String(raw).toUpperCase()==="ON"); valEl.textContent = on ? "ON" : "OFF"; badgeEl.classList.toggle("on", on); badgeEl.classList.toggle("off", !on); } const RULES = [ {key:"amonia", label:"Amonia", dangerHi:2, warnHi:1 }, {key:"suhu", label:"Suhu", dangerHi:33, warnHi:30, dangerLo:17, warnLo:20 }, {key:"ph", label:"pH", dangerLo:6.0, warnLo:6.5,dangerHi:9.0,warnHi:8.5}, {key:"kekeruhan", label:"Kekeruhan", dangerHi:80, warnHi:40 }, {key:"distance", label:"Distance", dangerHi:70, warnHi:50 } ]; function checkThresholds(data) { const notifs = []; RULES.forEach(r => { const v = parseFloat(data[r.key]); if (isNaN(v)) return; if ((r.dangerHi!==undefined&&v>=r.dangerHi)||(r.dangerLo!==undefined&&v<=r.dangerLo)) notifs.push({level:"danger", msg:`⚠️ ${r.label} KRITIS: ${v.toFixed(2)}`}); else if ((r.warnHi!==undefined&&v>=r.warnHi)||(r.warnLo!==undefined&&v<=r.warnLo)) notifs.push({level:"warn", msg:`🔔 ${r.label} Abnormal: ${v.toFixed(2)}`}); }); const badge = document.querySelector(".notif-badge"); const listEl = document.querySelector("#notifDropdown ul"); if (!listEl) return; if (!notifs.length) { if (badge) badge.textContent = "0"; listEl.innerHTML = "
  • Semua normal ✔
  • "; } else { if (badge) badge.textContent = notifs.length; listEl.innerHTML = notifs.map(n=> `
  • ${n.msg}
  • ` ).join(""); } } async function fetchData() { try { const res = await fetch(DATA_URL, {cache:"no-store"}); if (!res.ok) throw new Error("HTTP "+res.status); const data = await res.json(); // normalize: turbidity → kekeruhan, pH → ph const d = { suhu: data.suhu ?? data.temperature ?? null, ph: data.ph ?? data.pH ?? null, amonia: data.amonia ?? null, kekeruhan: data.kekeruhan ?? data.turbidity ?? null, relay: data.relay ?? null, lampu: data.lampu ?? null, distance: data.distance ?? null }; Object.keys(SENSORS).forEach(k => updateSensor(k, d[k])); updateDevice("badge-relay", "val-relay", d.relay); updateDevice("badge-lampu", "val-lampu", d.lampu); checkThresholds(d); if (typeof window.onSensorUpdate==="function") window.onSensorUpdate(true); } catch(err) { console.warn("[node-red.js]", err.message); if (typeof window.onSensorUpdate==="function") window.onSensorUpdate(false); } } if (!window.NODERED_URL) { console.error("[node-red.js] window.NODERED_URL belum diset di monitoring.php!"); return; } fetchData(); setInterval(fetchData, POLL_MS); })();