mynodes / presence-detector / presence-detector.js
Newer Older
208 lines | 7.445kb
Bogdan Timofte authored 2 weeks ago
1
module.exports = function(RED) {
2
  function PresenceDetectorNode(config) {
3
    RED.nodes.createNode(this, config);
4
    var node = this;
5

            
6
    // Configuration
7
    node.motionTimeout = Number(config.motionTimeout) || 30000; // ms
8
    node.delayBetween = Number(config.delayBetween) || 3000; // ms between payload #1 and #2
9
    node.payload1Text = (config.payload1Text || "").trim();
10
    node.payload2Text = (config.payload2Text || "").trim();
11
    node.treatMissingDoorAsOpen = config.treatMissingDoorAsOpen === "true" || config.treatMissingDoorAsOpen === true;
12
    node.resetOnMotion = config.resetOnMotion === "true" || config.resetOnMotion === true;
13
    node.resetOnDoorOpen = config.resetOnDoorOpen === "true" || config.resetOnDoorOpen === true;
14

            
15
    // Internal state
16
    node.presence = false;
17
    node.doorOpen = node.treatMissingDoorAsOpen; // default initial
18
    node.lastMotion = false;
19
    node.timer = null;
20
    node.timerType = null; // "door" or "motion"
21

            
22
    function parsePayloadText(text) {
23
      if (!text) return null;
24
      try {
25
        return JSON.parse(text);
26
      } catch (e) {
27
        return text;
28
      }
29
    }
30

            
31
    node.payload1 = parsePayloadText(node.payload1Text);
32
    node.payload2 = parsePayloadText(node.payload2Text);
33

            
34
    // Helper: format milliseconds to HH:MM:SS
35
    function formatMs(ms) {
36
      ms = Math.max(0, Math.floor(ms/1000));
37
      var hrs = Math.floor(ms / 3600);
38
      var mins = Math.floor((ms % 3600) / 60);
39
      var secs = ms % 60;
40
      return [hrs, mins, secs].map(function(n){ return String(n).padStart(2, '0'); }).join(':');
41
    }
42

            
43
    function sendPresence(p) {
44
      if (node.presence === p) return; // only send on change
45
      node.presence = p;
46
      node.status({fill: p ? "green" : "grey", shape: "dot", text: p ? "present" : "absent"});
47
      var out = {payload: {presenceDetected: p}};
48
      node.send([out, null]);
49
    }
50

            
51
    function sendControl(payload) {
52
      node.send([null, {payload: payload}]);
53
    }
54

            
55
    function clearTimer() {
56
      if (node.timer) {
57
        clearTimeout(node.timer);
58
        node.timer = null;
59
      }
60
      if (node.updateInterval) {
61
        clearInterval(node.updateInterval);
62
        node.updateInterval = null;
63
      }
64
      node.timerType = null;
65
      node.timerEnd = null;
66
      node._lastStatusUpdate = null;
67
      node.status({fill: node.presence ? "green" : "grey", shape: "dot", text: node.presence ? "present" : "idle"});
68
    }
69

            
70
    function startTimer(type, timeoutMs) {
71
      clearTimer();
72
      node.timerType = type;
73
      node.timerEnd = Date.now() + timeoutMs;
74
      node._lastStatusUpdate = 0;
75

            
76
      function statusUpdater() {
77
        var now = Date.now();
78
        var remaining = Math.max(0, node.timerEnd - now);
79
        // update every 10s when remaining > 10s, otherwise every second
80
        var shouldUpdate = false;
81
        if (remaining <= 10000) {
82
          shouldUpdate = true;
83
        } else {
84
          if (!node._lastStatusUpdate || (now - node._lastStatusUpdate) >= 10000) shouldUpdate = true;
85
        }
86
        if (shouldUpdate) {
87
          node._lastStatusUpdate = now;
88
          node.status({fill: "yellow", shape: "ring", text: `waiting (${type}) ${formatMs(remaining)}`});
89
        }
90
      }
91

            
92
      // initial status
93
      statusUpdater();
94
      node.updateInterval = setInterval(statusUpdater, 1000);
95

            
96
      node.timer = setTimeout(function() {
97
        // clear interval and timers
98
        if (node.updateInterval) { clearInterval(node.updateInterval); node.updateInterval = null; }
99
        node.timer = null;
100
        node.timerType = null;
101
        node.timerEnd = null;
102
        node._lastStatusUpdate = null;
103

            
104
        // timeout fires -> absence
105
        sendPresence(false);
106
        // control payload sequence
107
        if (node.payload1 !== null) sendControl(node.payload1);
108
        if (node.payload2 !== null) {
109
          setTimeout(function() {
110
            sendControl(node.payload2);
111
          }, node.delayBetween);
112
        }
113
      }, timeoutMs);
114
    }
115

            
116
    function handleMotion(val) {
117
      var motion = (val === true || val === 1 || String(val) === "1" || String(val).toLowerCase() === "true");
118
      node.lastMotion = motion;
119
      if (motion) {
120
        // someone detected
121
        sendPresence(true);
122
        if (node.payload2 !== null) sendControl(node.payload2);
123
        if (node.resetOnMotion) clearTimer();
124
      } else {
125
        // no motion -> only start timer if door is open (or no door sensor)
126
        if (node.doorOpen) {
127
          // If the door is open and motion stops, start motion timeout
128
          startTimer("motion", node.motionTimeout);
129
        } else {
130
          // door closed — keep presence as-is until door opens
131
          node.status({fill: node.presence ? "green" : "grey", shape: "dot", text: node.presence ? "present (door closed)" : "idle"});
132
        }
133
      }
134
    }
135

            
136
    function handleDoor(val) {
137
      var open = null;
138
      if (typeof val === 'string') {
139
        var v = val.toLowerCase();
140
        if (v === 'open' || v === '1' || v === 'true') open = true;
141
        if (v === 'closed' || v === '0' || v === 'false') open = false;
142
      } else if (typeof val === 'number') {
143
        open = (val === 1);
144
      } else if (typeof val === 'boolean') {
145
        open = val;
146
      } else if (val && typeof val === 'object') {
147
        if ('ContactSensorState' in val) {
148
          open = (val.ContactSensorState === 1);
149
        } else if ('doorOpen' in val) {
150
          open = !!val.doorOpen;
151
        }
152
      }
153
      if (open === null) return; // unknown message
154

            
155
      node.doorOpen = open;
156
      if (open) {
157
        // door opened -> start deducing occupancy
158
        if (node.resetOnDoorOpen) clearTimer();
159
        if (node.lastMotion) {
160
          // motion present now -> confirm presence
161
          sendPresence(true);
162
          if (node.payload2 !== null) sendControl(node.payload2);
163
        } else {
164
          // no motion now -> start motion timeout to decide absence
165
          startTimer("motion", node.motionTimeout);
166
        }
167
      } else {
168
        // door closed -> stop any running timeout and keep current presence until door opens
169
        clearTimer();
170
        // presence remains as-is
171
      }
172
    }
173

            
174
    node.on('input', function(msg) {
175
      try {
176
        // Only accept explicit object payloads with fields: motionDetected, doorOpen, presenceDetected
177
        if (!msg.payload || typeof msg.payload !== 'object') return;
178

            
179
        var p = msg.payload;
180

            
181
        // Explicit presence override
182
        if ('presenceDetected' in p) {
183
          var pres = !!p.presenceDetected;
184
          sendPresence(pres);
185
          if (pres && node.payload2 !== null) sendControl(node.payload2);
186
          return;
187
        }
188

            
189
        var didSomething = false;
190
        if ('motionDetected' in p) { handleMotion(p.motionDetected); didSomething = true; }
191
        if ('motionDtected' in p) { handleMotion(p.motionDtected); didSomething = true; }
192
        if ('doorOpen' in p) { handleDoor(p.doorOpen); didSomething = true; }
193
        if ('ContactSensorState' in p) { handleDoor(p); didSomething = true; }
194
        // ignore other payloads
195
      } catch (err) {
196
        node.error(err.message);
197
      }
198
    });
199

            
200
    node.on('close', function() {
201
      clearTimer();
202
    });
203

            
204
    // initialize status
205
    node.status({fill: node.presence ? "green" : "grey", shape: "dot", text: node.presence ? "present" : "idle"});
206
  }
207
  RED.nodes.registerType("presence-detector", PresenceDetectorNode);
208
};