@bldgblocks/node-red-contrib-control 0.1.34 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/nodes/accumulate-block.html +18 -8
  2. package/nodes/accumulate-block.js +39 -44
  3. package/nodes/add-block.html +1 -1
  4. package/nodes/add-block.js +18 -11
  5. package/nodes/alarm-collector.html +260 -0
  6. package/nodes/alarm-collector.js +292 -0
  7. package/nodes/alarm-config.html +129 -0
  8. package/nodes/alarm-config.js +126 -0
  9. package/nodes/alarm-service.html +96 -0
  10. package/nodes/alarm-service.js +142 -0
  11. package/nodes/analog-switch-block.js +25 -36
  12. package/nodes/and-block.js +44 -15
  13. package/nodes/average-block.js +46 -41
  14. package/nodes/boolean-switch-block.js +10 -28
  15. package/nodes/boolean-to-number-block.html +18 -5
  16. package/nodes/boolean-to-number-block.js +24 -16
  17. package/nodes/cache-block.js +24 -37
  18. package/nodes/call-status-block.html +91 -32
  19. package/nodes/call-status-block.js +398 -115
  20. package/nodes/changeover-block.html +5 -0
  21. package/nodes/changeover-block.js +167 -162
  22. package/nodes/comment-block.html +1 -1
  23. package/nodes/comment-block.js +14 -9
  24. package/nodes/compare-block.html +14 -4
  25. package/nodes/compare-block.js +23 -18
  26. package/nodes/contextual-label-block.html +5 -0
  27. package/nodes/contextual-label-block.js +6 -16
  28. package/nodes/convert-block.html +25 -39
  29. package/nodes/convert-block.js +31 -16
  30. package/nodes/count-block.html +11 -5
  31. package/nodes/count-block.js +34 -32
  32. package/nodes/delay-block.js +58 -53
  33. package/nodes/divide-block.js +43 -45
  34. package/nodes/edge-block.html +17 -10
  35. package/nodes/edge-block.js +43 -41
  36. package/nodes/enum-switch-block.js +6 -6
  37. package/nodes/frequency-block.html +6 -1
  38. package/nodes/frequency-block.js +64 -74
  39. package/nodes/global-getter.html +51 -15
  40. package/nodes/global-getter.js +43 -13
  41. package/nodes/global-setter.html +1 -1
  42. package/nodes/global-setter.js +40 -12
  43. package/nodes/history-buffer.html +96 -0
  44. package/nodes/history-buffer.js +461 -0
  45. package/nodes/history-collector.html +29 -1
  46. package/nodes/history-collector.js +37 -16
  47. package/nodes/history-config.html +13 -1
  48. package/nodes/history-service.html +84 -0
  49. package/nodes/history-service.js +52 -0
  50. package/nodes/hysteresis-block.html +5 -0
  51. package/nodes/hysteresis-block.js +13 -16
  52. package/nodes/interpolate-block.html +20 -2
  53. package/nodes/interpolate-block.js +39 -50
  54. package/nodes/join.html +78 -0
  55. package/nodes/join.js +78 -0
  56. package/nodes/latch-block.js +12 -14
  57. package/nodes/load-sequence-block.js +102 -110
  58. package/nodes/max-block.js +26 -26
  59. package/nodes/memory-block.js +57 -58
  60. package/nodes/min-block.js +26 -25
  61. package/nodes/minmax-block.js +35 -34
  62. package/nodes/modulo-block.js +45 -43
  63. package/nodes/multiply-block.js +43 -41
  64. package/nodes/negate-block.html +17 -7
  65. package/nodes/negate-block.js +25 -19
  66. package/nodes/network-point-read.html +128 -0
  67. package/nodes/network-point-read.js +230 -0
  68. package/nodes/{network-register.html → network-point-register.html} +94 -7
  69. package/nodes/{network-register.js → network-point-register.js} +18 -4
  70. package/nodes/network-point-write.html +149 -0
  71. package/nodes/network-point-write.js +222 -0
  72. package/nodes/network-service-bridge.html +131 -0
  73. package/nodes/network-service-bridge.js +376 -0
  74. package/nodes/network-service-read.html +81 -0
  75. package/nodes/{network-read.js → network-service-read.js} +4 -3
  76. package/nodes/{network-point-registry.html → network-service-registry.html} +19 -4
  77. package/nodes/{network-point-registry.js → network-service-registry.js} +7 -2
  78. package/nodes/network-service-write.html +89 -0
  79. package/nodes/{network-write.js → network-service-write.js} +3 -3
  80. package/nodes/nullify-block.js +13 -15
  81. package/nodes/on-change-block.html +17 -9
  82. package/nodes/on-change-block.js +49 -46
  83. package/nodes/oneshot-block.html +13 -10
  84. package/nodes/oneshot-block.js +57 -75
  85. package/nodes/or-block.js +44 -15
  86. package/nodes/pid-block.html +54 -4
  87. package/nodes/pid-block.js +459 -248
  88. package/nodes/priority-block.js +24 -35
  89. package/nodes/rate-limit-block.js +70 -72
  90. package/nodes/rate-of-change-block.html +33 -14
  91. package/nodes/rate-of-change-block.js +74 -62
  92. package/nodes/round-block.html +14 -9
  93. package/nodes/round-block.js +32 -25
  94. package/nodes/saw-tooth-wave-block.js +49 -76
  95. package/nodes/scale-range-block.html +12 -6
  96. package/nodes/scale-range-block.js +46 -39
  97. package/nodes/sine-wave-block.js +49 -57
  98. package/nodes/string-builder-block.js +6 -6
  99. package/nodes/subtract-block.js +38 -34
  100. package/nodes/thermistor-block.js +44 -44
  101. package/nodes/tick-tock-block.js +32 -32
  102. package/nodes/time-sequence-block.js +30 -42
  103. package/nodes/triangle-wave-block.js +49 -69
  104. package/nodes/tstat-block.js +34 -44
  105. package/nodes/units-block.html +90 -69
  106. package/nodes/units-block.js +22 -30
  107. package/nodes/utils.js +206 -3
  108. package/package.json +14 -6
  109. package/nodes/network-read.html +0 -56
  110. package/nodes/network-write.html +0 -65
@@ -1,157 +1,440 @@
1
1
  module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
3
+
2
4
  function CallStatusBlockNode(config) {
3
5
  RED.nodes.createNode(this, config);
4
6
  const node = this;
5
7
 
6
- // Simplified runtime state
7
- node.runtime = {
8
- call: false,
9
- status: false,
10
- alarm: false,
11
- alarmMessage: "",
12
- statusTimer: null,
13
- neverReceivedStatus: true // Track if we've ever gotten status during this call
8
+ // State management
9
+ node.requestedState = false; // What we want equipment to do (call)
10
+ node.actualState = false; // What equipment is actually doing (status)
11
+ node.alarm = false;
12
+ node.alarmMessage = "";
13
+ node.lastStatusTime = null;
14
+ node.neverReceivedStatus = true; // Track if status arrived during this call
15
+
16
+ // Timer management
17
+ node.initialStatusTimer = null; // Initial timeout waiting for first status response
18
+ node.heartbeatTimer = null; // Continuous heartbeat verification timer
19
+ node.statusLostTimer = null; // Hysteresis timer for status lost alarm
20
+ node.inactiveStatusTimer = null; // Timer to verify status goes inactive when call=false
21
+ node.clearTimer = null; // Timer to clear state after call ends
22
+ node.debounceTimer = null; // Debounce status flicker
23
+
24
+ // State machine states
25
+ const STATES = {
26
+ IDLE: "IDLE",
27
+ WAITING_FOR_STATUS: "WAITING_FOR_STATUS",
28
+ RUNNING: "RUNNING",
29
+ STATUS_LOST: "STATUS_LOST",
30
+ SHUTDOWN: "SHUTDOWN"
14
31
  };
15
32
 
16
- // Configuration with validation
33
+ // Configuration with defaults and validation
17
34
  node.config = {
35
+ inputProperty: config.inputProperty || "payload",
36
+ statusInputProperty: config.statusInputProperty || "status",
18
37
  statusTimeout: Math.max(parseFloat(config.statusTimeout) || 30, 0.01),
38
+ heartbeatTimeout: Math.max(parseFloat(config.heartbeatTimeout) || 30, 0), // 0 = disabled
39
+ clearDelay: Math.max(parseFloat(config.clearDelay) || 10, 0),
40
+ debounce: Math.max(parseFloat(config.debounce) || 100, 0), // ms
19
41
  runLostStatus: config.runLostStatus === true,
20
42
  noStatusOnRun: config.noStatusOnRun === true,
21
- runLostStatusMessage: config.runLostStatusMessage,
22
- noStatusOnRunMessage: config.noStatusOnRunMessage
43
+ runLostStatusMessage: config.runLostStatusMessage || "Status lost during run",
44
+ noStatusOnRunMessage: config.noStatusOnRunMessage || "No status received during run"
23
45
  };
24
46
 
25
- node.on("input", function(msg, send, done) {
26
- send = send || function() { node.send.apply(node, arguments); };
47
+ /**
48
+ * Get the current state machine state
49
+ */
50
+ function getCurrentState() {
51
+ if (!node.requestedState) {
52
+ return STATES.IDLE;
53
+ }
54
+ if (node.requestedState && node.neverReceivedStatus && node.initialStatusTimer) {
55
+ return STATES.WAITING_FOR_STATUS;
56
+ }
57
+ if (node.requestedState && node.actualState) {
58
+ return STATES.RUNNING;
59
+ }
60
+ if (node.requestedState && !node.actualState && !node.neverReceivedStatus) {
61
+ return STATES.STATUS_LOST;
62
+ }
63
+ return STATES.IDLE;
64
+ }
27
65
 
28
- // Handle status input
29
- if (msg.hasOwnProperty("context") && msg.context === "status") {
30
- if (typeof msg.payload !== "boolean") {
31
- node.status({ fill: "red", shape: "ring", text: "invalid status" });
32
- if (done) done();
33
- return;
66
+ /**
67
+ * Build the output message
68
+ */
69
+ function buildOutput() {
70
+ return {
71
+ payload: node.requestedState,
72
+ status: {
73
+ call: node.requestedState,
74
+ status: node.actualState,
75
+ alarm: node.alarm,
76
+ alarmMessage: node.alarmMessage
77
+ },
78
+ diagnostics: {
79
+ state: getCurrentState(),
80
+ initialTimeout: !!node.initialStatusTimer,
81
+ heartbeatActive: !!node.heartbeatTimer,
82
+ neverReceivedStatus: node.neverReceivedStatus,
83
+ lastStatusTime: node.lastStatusTime,
84
+ timeSinceLastStatus: node.lastStatusTime ? Date.now() - node.lastStatusTime : null
34
85
  }
86
+ };
87
+ }
35
88
 
36
- if (msg.context === node.runtime.status) {
37
- if (done) done();
38
- return;
39
- }
40
-
41
- node.runtime.status = msg.payload;
42
- node.runtime.neverReceivedStatus = false;
89
+ /**
90
+ * Update node status indicator
91
+ */
92
+ function updateNodeStatus() {
93
+ const state = getCurrentState();
94
+ const timeSince = node.lastStatusTime ? Math.round((Date.now() - node.lastStatusTime) / 1000) : '-';
95
+ let text;
96
+
97
+ if (node.alarm) {
98
+ text = `${state} | ALARM: ${node.alarmMessage}`;
99
+ utils.setStatusError(node, text);
100
+ } else if (node.heartbeatTimer && node.requestedState && node.actualState) {
101
+ text = `${state} | call:ON status:ON heartbeat:${timeSince}s | monitoring`;
102
+ utils.setStatusBusy(node, text);
103
+ } else if (node.inactiveStatusTimer && !node.requestedState && node.actualState) {
104
+ text = `${state} | call:OFF status:ON | waiting for deactivation`;
105
+ utils.setStatusBusy(node, text);
106
+ } else if (node.initialStatusTimer) {
107
+ text = `${state} | call:ON status:WAITING | initial timeout`;
108
+ utils.setStatusBusy(node, text);
109
+ } else if (node.requestedState && node.actualState) {
110
+ text = `${state} | call:ON status:ON heartbeat:${timeSince}s | running`;
111
+ utils.setStatusOK(node, text);
112
+ } else if (node.requestedState && !node.actualState) {
113
+ text = `${state} | call:ON status:OFF | off`;
114
+ utils.setStatusUnchanged(node, text);
115
+ } else if (!node.requestedState && !node.actualState) {
116
+ text = `${state} | call:OFF status:OFF | idle`;
117
+ utils.setStatusUnchanged(node, text);
118
+ } else {
119
+ text = `${state} | call:${node.requestedState} status:${node.actualState}`;
120
+ utils.setStatusUnchanged(node, text);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Clear all timers
126
+ */
127
+ function clearAllTimers() {
128
+ if (node.initialStatusTimer) {
129
+ clearTimeout(node.initialStatusTimer);
130
+ node.initialStatusTimer = null;
131
+ }
132
+ if (node.heartbeatTimer) {
133
+ clearTimeout(node.heartbeatTimer);
134
+ node.heartbeatTimer = null;
135
+ }
136
+ if (node.statusLostTimer) {
137
+ clearTimeout(node.statusLostTimer);
138
+ node.statusLostTimer = null;
139
+ }
140
+ if (node.inactiveStatusTimer) {
141
+ clearTimeout(node.inactiveStatusTimer);
142
+ node.inactiveStatusTimer = null;
143
+ }
144
+ if (node.clearTimer) {
145
+ clearTimeout(node.clearTimer);
146
+ node.clearTimer = null;
147
+ }
148
+ if (node.debounceTimer) {
149
+ clearTimeout(node.debounceTimer);
150
+ node.debounceTimer = null;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Start heartbeat verification timer
156
+ */
157
+ function startHeartbeatMonitoring(send) {
158
+ if (!node.config.heartbeatTimeout || node.config.heartbeatTimeout <= 0) {
159
+ return; // Heartbeat monitoring disabled
160
+ }
161
+
162
+ if (node.heartbeatTimer) {
163
+ clearTimeout(node.heartbeatTimer);
164
+ }
165
+
166
+ node.heartbeatTimer = setTimeout(() => {
167
+ // Check if status has been updated within the heartbeat window
168
+ const timeSinceLastUpdate = node.lastStatusTime ? Date.now() - node.lastStatusTime : Infinity;
43
169
 
44
- // Clear any pending status timeout
45
- if (node.runtime.statusTimer) {
46
- clearTimeout(node.runtime.statusTimer);
47
- node.runtime.statusTimer = null;
170
+ if (node.requestedState && node.actualState && timeSinceLastUpdate > node.config.heartbeatTimeout * 1000) {
171
+ // Status hasn't been updated within heartbeat window - arm the alarm
172
+ if (!node.statusLostTimer && node.config.runLostStatus) {
173
+ // Start hysteresis timer before alarming
174
+ node.statusLostTimer = setTimeout(() => {
175
+ if (node.requestedState && !node.actualState && node.lastStatusTime &&
176
+ (Date.now() - node.lastStatusTime > node.config.heartbeatTimeout * 1000)) {
177
+ node.alarm = true;
178
+ node.alarmMessage = node.config.runLostStatusMessage;
179
+ send(buildOutput());
180
+ updateNodeStatus();
181
+ }
182
+ node.statusLostTimer = null;
183
+ }, 500); // 500ms hysteresis
184
+ }
185
+ }
186
+
187
+ // Restart heartbeat timer
188
+ node.heartbeatTimer = null;
189
+ startHeartbeatMonitoring(send);
190
+ }, node.config.heartbeatTimeout * 1000);
191
+ }
192
+
193
+ /**
194
+ * Start timer to verify status goes inactive when call is inactive
195
+ */
196
+ function startInactiveStatusMonitoring(send) {
197
+ if (node.inactiveStatusTimer) {
198
+ clearTimeout(node.inactiveStatusTimer);
199
+ }
200
+
201
+ // When call=false but status=true, monitor with hysteresis
202
+ node.inactiveStatusTimer = setTimeout(() => {
203
+ if (!node.requestedState && node.actualState) {
204
+ // Status should have gone false by now
205
+ if (!node.statusLostTimer) {
206
+ node.statusLostTimer = setTimeout(() => {
207
+ if (!node.requestedState && node.actualState) {
208
+ node.alarm = true;
209
+ node.alarmMessage = "Status not clearing after call deactivated";
210
+ send(buildOutput());
211
+ updateNodeStatus();
212
+ }
213
+ node.statusLostTimer = null;
214
+ }, 500); // 500ms hysteresis
215
+ }
216
+ }
217
+ node.inactiveStatusTimer = null;
218
+ }, (node.config.clearDelay + 1) * 1000); // Check after clear delay passes
219
+ }
220
+
221
+ /**
222
+ * Check alarm conditions and set alarm state
223
+ */
224
+ function checkAlarmConditions() {
225
+ // Condition 1: Status active without a call (with hysteresis)
226
+ if (node.actualState && !node.requestedState) {
227
+ if (!node.statusLostTimer) {
228
+ node.statusLostTimer = setTimeout(() => {
229
+ if (node.actualState && !node.requestedState) {
230
+ node.alarm = true;
231
+ node.alarmMessage = "Status active without call";
232
+ }
233
+ node.statusLostTimer = null;
234
+ }, 500); // 500ms hysteresis to prevent false alarms
48
235
  }
49
-
50
- // Check alarm conditions
51
- checkAlarmConditions();
52
- send(sendOutputs());
53
- updateStatus();
54
-
55
- if (done) done();
56
236
  return;
57
237
  }
58
238
 
59
- // Handle call input (must be boolean)
60
- if (typeof msg.payload !== "boolean") {
61
- node.status({ fill: "red", shape: "ring", text: "invalid call payload" });
62
- if (done) done();
239
+ // Condition 2: Status lost during active call (checked by heartbeat/status update)
240
+ // This is handled by heartbeat monitoring and status timeout handlers
241
+
242
+ // If no conditions met and no timers running, clear alarm
243
+ if (!node.heartbeatTimer && !node.statusLostTimer && !node.inactiveStatusTimer && !node.initialStatusTimer) {
244
+ node.alarm = false;
245
+ node.alarmMessage = "";
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Process a requested state (call) change
251
+ */
252
+ function processRequestedState(value, send) {
253
+ const { valid, value: boolValue, error } = utils.validateBoolean(value);
254
+
255
+ if (!valid) {
256
+ utils.setStatusError(node, error || "invalid requested state");
63
257
  return;
64
258
  }
65
259
 
66
- // Process call state change
67
- if (msg.payload !== node.runtime.call) {
68
- node.runtime.call = msg.payload;
69
-
70
- // Clear existing timer
71
- if (node.runtime.statusTimer) {
72
- clearTimeout(node.runtime.statusTimer);
73
- node.runtime.statusTimer = null;
74
- }
75
-
76
- if (node.runtime.call) {
77
- // Call activated - reset tracking, set timeout if needed
78
- node.runtime.neverReceivedStatus = true;
79
- node.runtime.alarm = false;
80
- node.runtime.alarmMessage = "";
81
-
82
- if (node.config.noStatusOnRun) {
83
- // Set timer for "never got status" condition
84
- node.runtime.statusTimer = setTimeout(() => {
85
- if (node.runtime.neverReceivedStatus) {
86
- node.runtime.alarm = true;
87
- node.runtime.alarmMessage = node.config.noStatusOnRunMessage;
88
- }
89
- send(sendOutputs());
90
- updateStatus();
91
- node.runtime.statusTimer = null;
92
- }, node.config.statusTimeout * 1000);
93
- }
260
+ // No change
261
+ if (boolValue === node.requestedState) {
262
+ utils.setStatusUnchanged(node, "no change");
263
+ return;
264
+ }
265
+
266
+ node.requestedState = boolValue;
267
+
268
+ if (node.requestedState) {
269
+ // Call activated - expect status to arrive and be maintained
270
+ node.neverReceivedStatus = true;
271
+ node.alarm = false;
272
+ node.alarmMessage = "";
273
+
274
+ // Clear any existing timers
275
+ clearAllTimers();
276
+
277
+ // Set timeout waiting for initial status response
278
+ if (node.config.noStatusOnRun) {
279
+ node.initialStatusTimer = setTimeout(() => {
280
+ if (node.neverReceivedStatus && node.requestedState) {
281
+ node.alarm = true;
282
+ node.alarmMessage = node.config.noStatusOnRunMessage;
283
+ send(buildOutput());
284
+ updateNodeStatus();
285
+ }
286
+ node.initialStatusTimer = null;
287
+ }, node.config.statusTimeout * 1000);
288
+ }
289
+ } else {
290
+ // Call deactivated - start monitoring for status to go false
291
+ if (node.initialStatusTimer) {
292
+ clearTimeout(node.initialStatusTimer);
293
+ node.initialStatusTimer = null;
294
+ }
295
+ if (node.heartbeatTimer) {
296
+ clearTimeout(node.heartbeatTimer);
297
+ node.heartbeatTimer = null;
298
+ }
299
+
300
+ // Monitor that status goes inactive
301
+ if (node.actualState) {
302
+ startInactiveStatusMonitoring(send);
303
+ }
304
+
305
+ if (node.config.clearDelay > 0) {
306
+ node.clearTimer = setTimeout(() => {
307
+ node.actualState = false;
308
+ node.alarm = false;
309
+ node.alarmMessage = "";
310
+ node.neverReceivedStatus = true;
311
+ send(buildOutput());
312
+ updateNodeStatus();
313
+ node.clearTimer = null;
314
+ }, node.config.clearDelay * 1000);
94
315
  } else {
95
- node.runtime.status = false;
96
- node.runtime.alarm = false;
97
- node.runtime.alarmMessage = "";
316
+ // No delay, clear immediately
317
+ node.actualState = false;
318
+ node.alarm = false;
319
+ node.alarmMessage = "";
320
+ node.neverReceivedStatus = true;
98
321
  }
99
-
100
- // Check alarm conditions
322
+ }
323
+
324
+ checkAlarmConditions();
325
+ send(buildOutput());
326
+ updateNodeStatus();
327
+ }
328
+
329
+ /**
330
+ * Process a status update with debounce
331
+ */
332
+ function processStatus(value, send) {
333
+ const { valid, value: boolValue, error } = utils.validateBoolean(value);
334
+
335
+ if (!valid) {
336
+ utils.setStatusError(node, error || "invalid status");
337
+ return;
338
+ }
339
+
340
+ // Debounce rapid status changes
341
+ if (node.debounceTimer) {
342
+ clearTimeout(node.debounceTimer);
343
+ }
344
+
345
+ node.debounceTimer = setTimeout(() => {
346
+ // Check if status actually changed
347
+ if (boolValue === node.actualState) {
348
+ utils.setStatusUnchanged(node, "status unchanged");
349
+ node.debounceTimer = null;
350
+ return;
351
+ }
352
+
353
+ node.actualState = boolValue;
354
+ node.lastStatusTime = Date.now();
355
+ node.neverReceivedStatus = false;
356
+
357
+ // Clear initial timeout if we finally got status
358
+ if (node.initialStatusTimer) {
359
+ clearTimeout(node.initialStatusTimer);
360
+ node.initialStatusTimer = null;
361
+ }
362
+
363
+ // Clear status lost hysteresis timer on successful update
364
+ if (node.statusLostTimer && boolValue === true) {
365
+ clearTimeout(node.statusLostTimer);
366
+ node.statusLostTimer = null;
367
+ }
368
+
369
+ // If call is active and status is true, start heartbeat monitoring
370
+ if (node.requestedState && boolValue) {
371
+ startHeartbeatMonitoring(send);
372
+ }
373
+
374
+ // If call is inactive and status goes false, clear inactiveStatusTimer
375
+ if (!node.requestedState && !boolValue && node.inactiveStatusTimer) {
376
+ clearTimeout(node.inactiveStatusTimer);
377
+ node.inactiveStatusTimer = null;
378
+ node.alarm = false;
379
+ node.alarmMessage = "";
380
+ }
381
+
382
+ // Re-evaluate alarm conditions
101
383
  checkAlarmConditions();
102
- send(sendOutputs());
103
- updateStatus();
384
+ send(buildOutput());
385
+ updateNodeStatus();
386
+ node.debounceTimer = null;
387
+ }, node.config.debounce);
388
+ }
389
+
390
+ node.on("input", function(msg, send, done) {
391
+ send = send || function() { node.send.apply(node, arguments); };
392
+
393
+ // Validate message exists
394
+ if (!msg || typeof msg !== 'object') {
395
+ utils.setStatusError(node, "invalid message");
396
+ if (done) done();
397
+ return;
104
398
  }
105
399
 
106
- if (done) done();
400
+ try {
401
+ // ===== STATUS UPDATE (Dedicated Property Priority) =====
402
+ // 1. Check dedicated status property first (msg.status)
403
+ if (msg.hasOwnProperty(node.config.statusInputProperty) &&
404
+ typeof msg[node.config.statusInputProperty] === 'boolean') {
405
+ processStatus(msg[node.config.statusInputProperty], send);
406
+ if (done) done();
407
+ return;
408
+ }
107
409
 
108
- function checkAlarmConditions() {
109
- if (node.runtime.status && !node.runtime.call) {
110
- node.runtime.alarm = true;
111
- node.runtime.alarmMessage = "Status active without call";
410
+ // 2. Fallback to context tagging (msg.context === "status")
411
+ if (msg.hasOwnProperty("context") && msg.context === "status") {
412
+ processStatus(msg.payload, send);
413
+ if (done) done();
112
414
  return;
113
415
  }
114
-
115
- if (node.runtime.call && !node.runtime.status && !node.runtime.neverReceivedStatus && node.config.runLostStatus) {
116
- node.runtime.alarm = true;
117
- node.runtime.alarmMessage = node.config.runLostStatusMessage;
416
+
417
+ // ===== REQUESTED STATE (Call) =====
418
+ // Check configured input property
419
+ if (msg.hasOwnProperty(node.config.inputProperty)) {
420
+ processRequestedState(msg[node.config.inputProperty], send);
421
+ if (done) done();
118
422
  return;
119
423
  }
120
-
121
- // No alarm conditions met. Don't clear alarm if timer is still running
122
- if (!node.runtime.statusTimer) {
123
- node.runtime.alarm = false;
124
- node.runtime.alarmMessage = "";
125
- }
126
- }
127
-
128
- function sendOutputs() {
129
- return {
130
- payload: node.runtime.call,
131
- status: {
132
- call: node.runtime.call,
133
- status: node.runtime.status,
134
- alarm: node.runtime.alarm,
135
- alarmMessage: node.runtime.alarmMessage,
136
- timeout: !!node.runtime.statusTimer,
137
- neverReceivedStatus: node.runtime.neverReceivedStatus
138
- }
139
- };
140
- }
141
424
 
142
- function updateStatus() {
143
- node.status({
144
- fill: node.runtime.alarm ? "red" : "blue",
145
- shape: "dot",
146
- text: `call: ${node.runtime.call}, status: ${node.runtime.status}, alarm: ${node.runtime.alarm}`
147
- });
425
+ // Default: no recognized command
426
+ utils.setStatusWarn(node, "unrecognized input");
427
+ if (done) done();
428
+
429
+ } catch (err) {
430
+ node.error(`Error processing message: ${err.message}`);
431
+ utils.setStatusError(node, `error: ${err.message}`);
432
+ if (done) done();
148
433
  }
149
434
  });
150
435
 
151
436
  node.on("close", function(done) {
152
- if (node.runtime.statusTimer) {
153
- clearTimeout(node.runtime.statusTimer);
154
- }
437
+ clearAllTimers();
155
438
  done();
156
439
  });
157
440
  }
@@ -3,6 +3,10 @@
3
3
  <label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
4
4
  <input type="text" id="node-input-name" placeholder="Name">
5
5
  </div>
6
+ <div class="form-row">
7
+ <label for="node-input-inputProperty" title="Message property to read temperature input from"><i class="fa fa-folder-open"></i> Input Property</label>
8
+ <input type="text" id="node-input-inputProperty" placeholder="payload">
9
+ </div>
6
10
  <div class="form-row">
7
11
  <label for="node-input-algorithm" title="Control algorithm type"><i class="fa fa-cog"></i> Algorithm</label>
8
12
  <input type="text" id="node-input-algorithm" class="node-input-typed" placeholder="single">
@@ -97,6 +101,7 @@
97
101
  color: "#301934",
98
102
  defaults: {
99
103
  name: { value: "" },
104
+ inputProperty: { value: "payload" },
100
105
  algorithm: { value: "single" },
101
106
  algorithmType: { value: "dropdown" },
102
107
  setpoint: { value: "70" },