1 contributor
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);
};