@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.
- package/nodes/accumulate-block.html +18 -8
- package/nodes/accumulate-block.js +39 -44
- package/nodes/add-block.html +1 -1
- package/nodes/add-block.js +18 -11
- package/nodes/alarm-collector.html +260 -0
- package/nodes/alarm-collector.js +292 -0
- package/nodes/alarm-config.html +129 -0
- package/nodes/alarm-config.js +126 -0
- package/nodes/alarm-service.html +96 -0
- package/nodes/alarm-service.js +142 -0
- package/nodes/analog-switch-block.js +25 -36
- package/nodes/and-block.js +44 -15
- package/nodes/average-block.js +46 -41
- package/nodes/boolean-switch-block.js +10 -28
- package/nodes/boolean-to-number-block.html +18 -5
- package/nodes/boolean-to-number-block.js +24 -16
- package/nodes/cache-block.js +24 -37
- package/nodes/call-status-block.html +91 -32
- package/nodes/call-status-block.js +398 -115
- package/nodes/changeover-block.html +5 -0
- package/nodes/changeover-block.js +167 -162
- package/nodes/comment-block.html +1 -1
- package/nodes/comment-block.js +14 -9
- package/nodes/compare-block.html +14 -4
- package/nodes/compare-block.js +23 -18
- package/nodes/contextual-label-block.html +5 -0
- package/nodes/contextual-label-block.js +6 -16
- package/nodes/convert-block.html +25 -39
- package/nodes/convert-block.js +31 -16
- package/nodes/count-block.html +11 -5
- package/nodes/count-block.js +34 -32
- package/nodes/delay-block.js +58 -53
- package/nodes/divide-block.js +43 -45
- package/nodes/edge-block.html +17 -10
- package/nodes/edge-block.js +43 -41
- package/nodes/enum-switch-block.js +6 -6
- package/nodes/frequency-block.html +6 -1
- package/nodes/frequency-block.js +64 -74
- package/nodes/global-getter.html +51 -15
- package/nodes/global-getter.js +43 -13
- package/nodes/global-setter.html +1 -1
- package/nodes/global-setter.js +40 -12
- package/nodes/history-buffer.html +96 -0
- package/nodes/history-buffer.js +461 -0
- package/nodes/history-collector.html +29 -1
- package/nodes/history-collector.js +37 -16
- package/nodes/history-config.html +13 -1
- package/nodes/history-service.html +84 -0
- package/nodes/history-service.js +52 -0
- package/nodes/hysteresis-block.html +5 -0
- package/nodes/hysteresis-block.js +13 -16
- package/nodes/interpolate-block.html +20 -2
- package/nodes/interpolate-block.js +39 -50
- package/nodes/join.html +78 -0
- package/nodes/join.js +78 -0
- package/nodes/latch-block.js +12 -14
- package/nodes/load-sequence-block.js +102 -110
- package/nodes/max-block.js +26 -26
- package/nodes/memory-block.js +57 -58
- package/nodes/min-block.js +26 -25
- package/nodes/minmax-block.js +35 -34
- package/nodes/modulo-block.js +45 -43
- package/nodes/multiply-block.js +43 -41
- package/nodes/negate-block.html +17 -7
- package/nodes/negate-block.js +25 -19
- package/nodes/network-point-read.html +128 -0
- package/nodes/network-point-read.js +230 -0
- package/nodes/{network-register.html → network-point-register.html} +94 -7
- package/nodes/{network-register.js → network-point-register.js} +18 -4
- package/nodes/network-point-write.html +149 -0
- package/nodes/network-point-write.js +222 -0
- package/nodes/network-service-bridge.html +131 -0
- package/nodes/network-service-bridge.js +376 -0
- package/nodes/network-service-read.html +81 -0
- package/nodes/{network-read.js → network-service-read.js} +4 -3
- package/nodes/{network-point-registry.html → network-service-registry.html} +19 -4
- package/nodes/{network-point-registry.js → network-service-registry.js} +7 -2
- package/nodes/network-service-write.html +89 -0
- package/nodes/{network-write.js → network-service-write.js} +3 -3
- package/nodes/nullify-block.js +13 -15
- package/nodes/on-change-block.html +17 -9
- package/nodes/on-change-block.js +49 -46
- package/nodes/oneshot-block.html +13 -10
- package/nodes/oneshot-block.js +57 -75
- package/nodes/or-block.js +44 -15
- package/nodes/pid-block.html +54 -4
- package/nodes/pid-block.js +459 -248
- package/nodes/priority-block.js +24 -35
- package/nodes/rate-limit-block.js +70 -72
- package/nodes/rate-of-change-block.html +33 -14
- package/nodes/rate-of-change-block.js +74 -62
- package/nodes/round-block.html +14 -9
- package/nodes/round-block.js +32 -25
- package/nodes/saw-tooth-wave-block.js +49 -76
- package/nodes/scale-range-block.html +12 -6
- package/nodes/scale-range-block.js +46 -39
- package/nodes/sine-wave-block.js +49 -57
- package/nodes/string-builder-block.js +6 -6
- package/nodes/subtract-block.js +38 -34
- package/nodes/thermistor-block.js +44 -44
- package/nodes/tick-tock-block.js +32 -32
- package/nodes/time-sequence-block.js +30 -42
- package/nodes/triangle-wave-block.js +49 -69
- package/nodes/tstat-block.js +34 -44
- package/nodes/units-block.html +90 -69
- package/nodes/units-block.js +22 -30
- package/nodes/utils.js +206 -3
- package/package.json +14 -6
- package/nodes/network-read.html +0 -56
- 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
|
-
//
|
|
7
|
-
node.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
//
|
|
67
|
-
if (
|
|
68
|
-
node
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
send(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
node.
|
|
97
|
-
node.
|
|
316
|
+
// No delay, clear immediately
|
|
317
|
+
node.actualState = false;
|
|
318
|
+
node.alarm = false;
|
|
319
|
+
node.alarmMessage = "";
|
|
320
|
+
node.neverReceivedStatus = true;
|
|
98
321
|
}
|
|
99
|
-
|
|
100
|
-
|
|
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(
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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" },
|