module.exports = function(RED) { function PresenceDetectorNode(config) { RED.nodes.createNode(this, config); var node = this; // Configuration node.motionTimeout = Number(config.motionTimeout) || 30000; // ms node.delayBetween = Number(config.delayBetween) || 3000; // ms between payload #1 and #2 node.payload1Text = (config.payload1Text || "").trim(); node.payload2Text = (config.payload2Text || "").trim(); node.treatMissingDoorAsOpen = config.treatMissingDoorAsOpen === "true" || config.treatMissingDoorAsOpen === true; node.resetOnMotion = config.resetOnMotion === "true" || config.resetOnMotion === true; node.resetOnDoorOpen = config.resetOnDoorOpen === "true" || config.resetOnDoorOpen === true; // Internal state node.presence = false; node.doorOpen = node.treatMissingDoorAsOpen; // default initial node.lastMotion = false; node.timer = null; node.timerType = null; // "door" or "motion" function parsePayloadText(text) { if (!text) return null; try { return JSON.parse(text); } catch (e) { return text; } } node.payload1 = parsePayloadText(node.payload1Text); node.payload2 = parsePayloadText(node.payload2Text); // Helper: format milliseconds to HH:MM:SS function formatMs(ms) { ms = Math.max(0, Math.floor(ms/1000)); var hrs = Math.floor(ms / 3600); var mins = Math.floor((ms % 3600) / 60); var secs = ms % 60; return [hrs, mins, secs].map(function(n){ return String(n).padStart(2, '0'); }).join(':'); } function sendPresence(p) { if (node.presence === p) return; // only send on change node.presence = p; node.status({fill: p ? "green" : "grey", shape: "dot", text: p ? "present" : "absent"}); var out = {payload: {presenceDetected: p}}; node.send([out, null]); } function sendControl(payload) { node.send([null, {payload: payload}]); } function clearTimer() { if (node.timer) { clearTimeout(node.timer); node.timer = null; } if (node.updateInterval) { clearInterval(node.updateInterval); node.updateInterval = null; } node.timerType = null; node.timerEnd = null; node._lastStatusUpdate = null; node.status({fill: node.presence ? "green" : "grey", shape: "dot", text: node.presence ? "present" : "idle"}); } function startTimer(type, timeoutMs) { clearTimer(); node.timerType = type; node.timerEnd = Date.now() + timeoutMs; node._lastStatusUpdate = 0; function statusUpdater() { var now = Date.now(); var remaining = Math.max(0, node.timerEnd - now); // update every 10s when remaining > 10s, otherwise every second var shouldUpdate = false; if (remaining <= 10000) { shouldUpdate = true; } else { if (!node._lastStatusUpdate || (now - node._lastStatusUpdate) >= 10000) shouldUpdate = true; } if (shouldUpdate) { node._lastStatusUpdate = now; node.status({fill: "yellow", shape: "ring", text: `waiting (${type}) ${formatMs(remaining)}`}); } } // initial status statusUpdater(); node.updateInterval = setInterval(statusUpdater, 1000); node.timer = setTimeout(function() { // clear interval and timers if (node.updateInterval) { clearInterval(node.updateInterval); node.updateInterval = null; } node.timer = null; node.timerType = null; node.timerEnd = null; node._lastStatusUpdate = null; // timeout fires -> absence sendPresence(false); // control payload sequence if (node.payload1 !== null) sendControl(node.payload1); if (node.payload2 !== null) { setTimeout(function() { sendControl(node.payload2); }, node.delayBetween); } }, timeoutMs); } function handleMotion(val) { var motion = (val === true || val === 1 || String(val) === "1" || String(val).toLowerCase() === "true"); node.lastMotion = motion; if (motion) { // someone detected sendPresence(true); if (node.payload2 !== null) sendControl(node.payload2); if (node.resetOnMotion) clearTimer(); } else { // no motion -> only start timer if door is open (or no door sensor) if (node.doorOpen) { // If the door is open and motion stops, start motion timeout startTimer("motion", node.motionTimeout); } else { // door closed — keep presence as-is until door opens node.status({fill: node.presence ? "green" : "grey", shape: "dot", text: node.presence ? "present (door closed)" : "idle"}); } } } function handleDoor(val) { var open = null; if (typeof val === 'string') { var v = val.toLowerCase(); if (v === 'open' || v === '1' || v === 'true') open = true; if (v === 'closed' || v === '0' || v === 'false') open = false; } else if (typeof val === 'number') { open = (val === 1); } else if (typeof val === 'boolean') { open = val; } else if (val && typeof val === 'object') { if ('ContactSensorState' in val) { open = (val.ContactSensorState === 1); } else if ('doorOpen' in val) { open = !!val.doorOpen; } } if (open === null) return; // unknown message node.doorOpen = open; if (open) { // door opened -> start deducing occupancy if (node.resetOnDoorOpen) clearTimer(); if (node.lastMotion) { // motion present now -> confirm presence sendPresence(true); if (node.payload2 !== null) sendControl(node.payload2); } else { // no motion now -> start motion timeout to decide absence startTimer("motion", node.motionTimeout); } } else { // door closed -> stop any running timeout and keep current presence until door opens clearTimer(); // presence remains as-is } } node.on('input', function(msg) { try { // Only accept explicit object payloads with fields: motionDetected, doorOpen, presenceDetected if (!msg.payload || typeof msg.payload !== 'object') return; var p = msg.payload; // Explicit presence override if ('presenceDetected' in p) { var pres = !!p.presenceDetected; sendPresence(pres); if (pres && node.payload2 !== null) sendControl(node.payload2); return; } var didSomething = false; if ('motionDetected' in p) { handleMotion(p.motionDetected); didSomething = true; } if ('motionDtected' in p) { handleMotion(p.motionDtected); didSomething = true; } if ('doorOpen' in p) { handleDoor(p.doorOpen); didSomething = true; } if ('ContactSensorState' in p) { handleDoor(p); didSomething = true; } // ignore other payloads } catch (err) { node.error(err.message); } }); node.on('close', function() { clearTimer(); }); // initialize status node.status({fill: node.presence ? "green" : "grey", shape: "dot", text: node.presence ? "present" : "idle"}); } RED.nodes.registerType("presence-detector", PresenceDetectorNode); };