@bldgblocks/node-red-contrib-control 0.1.27 → 0.1.28

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.
@@ -47,7 +47,7 @@ module.exports = function(RED) {
47
47
  // Acceptable fallbacks
48
48
  if (isNaN(node.runtime.maxValues) || node.runtime.maxValues < 1) {
49
49
  node.runtime.maxValues = 10;
50
- node.status({ fill: "red", shape: "ring", text: "invalid window size, using 10" });
50
+ node.status({ fill: "yellow", shape: "ring", text: "invalid window size, using 10" });
51
51
  }
52
52
 
53
53
  // Validate values
@@ -12,7 +12,8 @@
12
12
  category: "control",
13
13
  color: "#301934",
14
14
  defaults: {
15
- name: { value: "" }
15
+ name: { value: "" },
16
+ state: { value: false }
16
17
  },
17
18
  inputs: 1,
18
19
  outputs: 3,
@@ -4,7 +4,7 @@ module.exports = function(RED) {
4
4
  const node = this;
5
5
 
6
6
  // Initialize state from config
7
- node.state = config.state !== undefined ? Boolean(config.state) : false;
7
+ node.state = config.state;
8
8
 
9
9
  // Set initial status
10
10
  node.status({
@@ -79,7 +79,6 @@ module.exports = function(RED) {
79
79
  });
80
80
 
81
81
  node.on("close", function(done) {
82
- node.status({});
83
82
  done();
84
83
  });
85
84
  }
@@ -13,15 +13,7 @@
13
13
  <input type="number" id="node-input-clearDelay" placeholder="10" min="0.01" step="any">
14
14
  </div>
15
15
  <div class="form-row">
16
- <label for="node-input-normalOff" title="Expected status when call is off (boolean)"><i class="fa fa-power-off"></i> Normal Off</label>
17
- <input type="checkbox" id="node-input-normalOff" style="width: auto; vertical-align: middle;">
18
- </div>
19
- <div class="form-row">
20
- <label for="node-input-normalOn" title="Expected status when call is on (boolean)"><i class="fa fa-power-off"></i> Normal On</label>
21
- <input type="checkbox" id="node-input-normalOn" style="width: auto; vertical-align: middle;">
22
- </div>
23
- <div class="form-row">
24
- <label for="node-input-runLostStatus" title="Alarm if status doesn't match normalOn during call (boolean)"><i class="fa fa-exclamation-triangle"></i> Run Lost Status</label>
16
+ <label for="node-input-runLostStatus" title="Alarm if status is lost during call (boolean)"><i class="fa fa-exclamation-triangle"></i> Run Lost Status</label>
25
17
  <input type="checkbox" id="node-input-runLostStatus" style="width: auto; vertical-align: middle;">
26
18
  </div>
27
19
  <div class="form-row">
@@ -47,17 +39,15 @@
47
39
  name: { value: "" },
48
40
  statusTimeout: { value: 30, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) > 0; } },
49
41
  clearDelay: { value: 10, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) > 0; } },
50
- normalOff: { value: false },
51
- normalOn: { value: true },
52
42
  runLostStatus: { value: false },
53
43
  noStatusOnRun: { value: true },
54
44
  runLostStatusMessage: { value: "Status lost during run" },
55
45
  noStatusOnRunMessage: { value: "No status received during run" }
56
46
  },
57
47
  inputs: 1,
58
- outputs: 2,
48
+ outputs: 1,
59
49
  inputLabels: ["input"],
60
- outputLabels: ["call", "diagnostics"],
50
+ outputLabels: ["output"],
61
51
  icon: "font-awesome/fa-phone",
62
52
  paletteLabel: "call status",
63
53
  label: function() {
@@ -68,44 +58,39 @@
68
58
 
69
59
  <!-- Help Section -->
70
60
  <script type="text/markdown" data-help-name="call-status-block">
71
- Issues calls to equipment with status and alarm diagnostics.
61
+ Monitors call and status signals with alarm diagnostics.
72
62
 
73
63
  ### Inputs
74
- : context (string) : Configuration commands - (`"statusTimeout"`, `"clearDelay"`, `"normalOff"`, `"normalOn"`, `"runLostStatus"`, `"noStatusOnRun"`, `"runLostStatusMessage"`, `"noStatusOnRunMessage"`) or sets status (`"status"`). Unmatched values trigger error.
75
- : payload (boolean | number | string) : Boolean for call, status, or boolean configs; number for timeouts; string for messages.
64
+ - payload (boolean): Call activation/deactivation signal
65
+ - status (boolean): Status response from equipment (only processed when call is active)
76
66
 
77
67
  ### Outputs
78
- : call (boolean) : Current call state.
79
- : diagnostics (object) : `{ call; boolean, status; boolean, alarm; boolean, alarmMessage; string, timeout; boolean }`.
68
+ - call (boolean): Current call state
69
+ - status (object): `{ call: boolean, status: boolean, alarm: boolean, alarmMessage: string, timeout: boolean, neverReceivedStatus: boolean }`
80
70
 
81
71
  ### Properties
82
- : name (string) : Display name in editor.
83
- : statusTimeout (number) : Time to wait for status response (seconds, > 0).
84
- : clearDelay (number) : Delay before clearing status and alarm after call ends (seconds, > 0).
85
- : normalOff (boolean) : Expected status when call is off.
86
- : normalOn (boolean) : Expected status when call is on.
87
- : runLostStatus (boolean) : Alarm if status doesn't match `normalOn` during call.
88
- : noStatusOnRun (boolean) : Alarm if no status received during call.
89
- : runLostStatusMessage (string) : Message for run lost status alarm.
90
- : noStatusOnRunMessage (string) : Message for no status on run alarm.
72
+ - name (string): Display name in editor
73
+ - statusTimeout (number): Time to wait for status response (seconds, > 0)
74
+ - clearDelay (number): Delay before clearing status and alarm after call ends (seconds, > 0)
75
+ - runLostStatus (boolean): Alarm if status is lost during call
76
+ - noStatusOnRun (boolean): Alarm if no status received during call
77
+ - runLostStatusMessage (string): Message for status lost alarm
78
+ - noStatusOnRunMessage (string): Message for no status alarm
91
79
 
92
- ### Details
93
- Issues a call (`true`) to equipment via output 1 and expects a status response matching `normalOn` within `statusTimeout` seconds.
80
+ ### Alarm Conditions
81
+ 1. Status without call: Status=true when call=false (immediate alarm)
82
+ 2. No status during call: No status received within timeout period (after statusTimeout)
83
+ 3. Lost status during call: Status=false when call=true and status was previously received (immediate alarm)
94
84
 
95
- Clears status and alarm after `clearDelay` seconds when call ends (`false`), checking against `normalOff`.
85
+ ### Behavior
86
+ - When call is activated: Expects status=true response
87
+ - Status is only processed when call is active
88
+ - Clears all state after clearDelay when call ends
89
+ - Timer acts as a blocking gate for status reception
96
90
 
97
- Generates alarms for `runLostStatus` (status mismatch during call) or `noStatusOnRun` (no status within `statusTimeout`) with custom messages.
98
-
99
- Ignores status inputs without an active call.
100
-
101
- ### Status
102
- - Green (dot): Configuration
103
- - Blue (dot): Output, no alarm
104
- - Red (dot): Output with alarm
105
- - Red (ring): Errors
106
- - Yellow (ring): Unknown context
107
-
108
- ### References
109
- - [Node-RED Documentation](https://nodered.org/docs/)
110
- - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
111
- </script>
91
+ ### Status Indicators
92
+ - Green (ring): Configuration errors
93
+ - Blue (dot): Normal operation, no alarms
94
+ - Red (dot): Active alarm condition
95
+ - Yellow (ring): Invalid input
96
+ </script>
@@ -3,255 +3,140 @@ module.exports = function(RED) {
3
3
  RED.nodes.createNode(this, config);
4
4
  const node = this;
5
5
 
6
- // Initialize runtime state
6
+ // Simplified runtime state
7
7
  node.runtime = {
8
- name: config.name || "",
9
- statusTimeout: parseFloat(config.statusTimeout),
10
- clearDelay: parseFloat(config.clearDelay),
11
- normalOff: config.normalOff === true,
12
- normalOn: config.normalOn === true,
13
- runLostStatus: config.runLostStatus === true,
14
- noStatusOnRun: config.noStatusOnRun === true,
15
- runLostStatusMessage: config.runLostStatusMessage,
16
- noStatusOnRunMessage: config.noStatusOnRunMessage,
17
8
  call: false,
18
9
  status: false,
19
10
  alarm: false,
20
11
  alarmMessage: "",
21
12
  statusTimer: null,
22
- clearTimer: null
13
+ neverReceivedStatus: true // Track if we've ever gotten status during this call
23
14
  };
24
15
 
25
- // Validate initial config
26
- if (isNaN(node.runtime.statusTimeout) || node.runtime.statusTimeout <= 0) {
27
- node.runtime.statusTimeout = 30;
28
- node.status({ fill: "red", shape: "ring", text: "invalid statusTimeout" });
29
- }
30
- if (isNaN(node.runtime.clearDelay) || node.runtime.clearDelay <= 0) {
31
- node.runtime.clearDelay = 10;
32
- node.status({ fill: "red", shape: "ring", text: "invalid clearDelay" });
33
- }
16
+ // Configuration with validation
17
+ node.config = {
18
+ statusTimeout: Math.max(parseFloat(config.statusTimeout) || 30, 0.01),
19
+ runLostStatus: config.runLostStatus === true,
20
+ noStatusOnRun: config.noStatusOnRun === true,
21
+ runLostStatusMessage: config.runLostStatusMessage,
22
+ noStatusOnRunMessage: config.noStatusOnRunMessage
23
+ };
34
24
 
35
25
  node.on("input", function(msg, send, done) {
36
26
  send = send || function() { node.send.apply(node, arguments); };
37
27
 
38
- // Guard against invalid message
39
- if (!msg) {
40
- node.status({ fill: "red", shape: "ring", text: "invalid message" });
41
- if (done) done();
42
- return;
43
- }
44
-
45
- // Handle context updates
46
- if (msg.hasOwnProperty("context")) {
47
- if (!msg.hasOwnProperty("payload")) {
48
- node.status({ fill: "red", shape: "ring", text: `missing payload for ${msg.context}` });
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" });
49
32
  if (done) done();
50
33
  return;
51
34
  }
52
35
 
53
- switch (msg.context) {
54
- case "statusTimeout":
55
- case "clearDelay":
56
- const value = parseFloat(msg.payload);
57
- if (isNaN(value) || value <= 0) {
58
- node.status({ fill: "red", shape: "ring", text: `invalid ${msg.context}` });
59
- if (done) done();
60
- return;
61
- }
62
- node.runtime[msg.context] = value;
63
- if (node.runtime.statusTimer) {
64
- clearTimeout(node.runtime.statusTimer);
65
- node.runtime.statusTimer = null;
66
- if (node.runtime.noStatusOnRun && node.runtime.call) {
67
- node.runtime.statusTimer = setTimeout(() => {
68
- if (!node.runtime.status) {
69
- node.runtime.alarm = true;
70
- node.runtime.alarmMessage = node.runtime.noStatusOnRunMessage;
71
- node.status({ fill: "red", shape: "dot", text: `no status on run, alarm: true` });
72
- send(sendOutputs());
73
- }
74
- node.runtime.statusTimer = null;
75
- }, node.runtime.statusTimeout * 1000);
76
- }
77
- }
78
- node.status({
79
- fill: "green",
80
- shape: "dot",
81
- text: `${msg.context}: ${value.toFixed(2)}`
82
- });
83
- if (done) done();
84
- return;
85
- case "normalOff":
86
- case "normalOn":
87
- case "runLostStatus":
88
- case "noStatusOnRun":
89
- if (typeof msg.payload !== "boolean") {
90
- node.status({ fill: "red", shape: "ring", text: `invalid ${msg.context}` });
91
- if (done) done();
92
- return;
93
- }
94
- node.runtime[msg.context] = msg.payload;
95
- if (msg.context === "noStatusOnRun" && !msg.payload && node.runtime.statusTimer) {
96
- clearTimeout(node.runtime.statusTimer);
97
- node.runtime.statusTimer = null;
98
- } else if (msg.context === "noStatusOnRun" && msg.payload && node.runtime.call && !node.runtime.status) {
99
- node.runtime.statusTimer = setTimeout(() => {
100
- if (!node.runtime.status) {
101
- node.runtime.alarm = true;
102
- node.runtime.alarmMessage = node.runtime.noStatusOnRunMessage;
103
- node.status({ fill: "red", shape: "dot", text: `no status on run, alarm: true` });
104
- send(sendOutputs());
105
- }
106
- node.runtime.statusTimer = null;
107
- }, node.runtime.statusTimeout * 1000);
108
- }
109
- node.status({
110
- fill: "green",
111
- shape: "dot",
112
- text: `${msg.context}: ${msg.payload}`
113
- });
114
- send(checkStatusConditions());
115
- if (done) done();
116
- return;
117
- case "runLostStatusMessage":
118
- case "noStatusOnRunMessage":
119
- if (typeof msg.payload !== "string") {
120
- node.status({ fill: "red", shape: "ring", text: `invalid ${msg.context}` });
121
- if (done) done();
122
- return;
123
- }
124
- node.runtime[msg.context] = msg.payload;
125
- node.status({
126
- fill: "green",
127
- shape: "dot",
128
- text: `${msg.context} set`
129
- });
130
- if (node.runtime.alarm && node.runtime.alarmMessage === (msg.context === "runLostStatusMessage" ? node.runtime.noStatusOnRunMessage : node.runtime.runLostStatusMessage)) {
131
- node.runtime.alarmMessage = msg.payload;
132
- send(sendOutputs());
133
- }
134
- if (done) done();
135
- return;
136
- case "status":
137
- if (typeof msg.payload !== "boolean") {
138
- node.status({ fill: "red", shape: "ring", text: "invalid status" });
139
- if (done) done();
140
- return;
141
- }
142
- if (!node.runtime.call) {
143
- node.status({ fill: "red", shape: "ring", text: "status ignored" });
144
- if (done) done();
145
- return;
146
- }
147
- node.runtime.status = msg.payload;
148
- if (node.runtime.status && node.runtime.statusTimer) {
149
- clearTimeout(node.runtime.statusTimer);
150
- node.runtime.statusTimer = null;
151
- node.runtime.alarm = false;
152
- node.runtime.alarmMessage = "";
153
- }
154
- send(checkStatusConditions());
155
- if (done) done();
156
- return;
157
- default:
158
- node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
159
- if (done) done("Unknown context");
160
- return;
36
+ if (msg.context === node.runtime.status) {
37
+ if (done) done();
38
+ return;
161
39
  }
40
+
41
+ node.runtime.status = msg.payload;
42
+ node.runtime.neverReceivedStatus = false;
43
+
44
+ // Clear any pending status timeout
45
+ if (node.runtime.statusTimer) {
46
+ clearTimeout(node.runtime.statusTimer);
47
+ node.runtime.statusTimer = null;
48
+ }
49
+
50
+ // Check alarm conditions
51
+ checkAlarmConditions();
52
+ send(sendOutputs());
53
+ updateStatus();
54
+
55
+ if (done) done();
56
+ return;
162
57
  }
163
58
 
164
- // Validate call input
59
+ // Handle call input (must be boolean)
165
60
  if (typeof msg.payload !== "boolean") {
166
- node.status({ fill: "red", shape: "ring", text: "invalid call" });
61
+ node.status({ fill: "red", shape: "ring", text: "invalid call payload" });
167
62
  if (done) done();
168
63
  return;
169
64
  }
170
65
 
171
- // Process call change
66
+ // Process call state change
172
67
  if (msg.payload !== node.runtime.call) {
173
68
  node.runtime.call = msg.payload;
174
69
 
175
- // Clear existing timers
70
+ // Clear existing timer
176
71
  if (node.runtime.statusTimer) {
177
72
  clearTimeout(node.runtime.statusTimer);
178
73
  node.runtime.statusTimer = null;
179
74
  }
180
- if (node.runtime.clearTimer) {
181
- clearTimeout(node.runtime.clearTimer);
182
- node.runtime.clearTimer = null;
183
- }
184
75
 
185
76
  if (node.runtime.call) {
186
- // Start call: reset status and alarm, set timeout
187
- node.runtime.status = false;
77
+ // Call activated - reset tracking, set timeout if needed
78
+ node.runtime.neverReceivedStatus = true;
188
79
  node.runtime.alarm = false;
189
80
  node.runtime.alarmMessage = "";
190
- if (node.runtime.noStatusOnRun) {
81
+
82
+ if (node.config.noStatusOnRun) {
83
+ // Set timer for "never got status" condition
191
84
  node.runtime.statusTimer = setTimeout(() => {
192
- if (!node.runtime.status) {
85
+ if (node.runtime.neverReceivedStatus) {
193
86
  node.runtime.alarm = true;
194
- node.runtime.alarmMessage = node.runtime.noStatusOnRunMessage;
195
- node.status({ fill: "red", shape: "dot", text: `no status on run, alarm: true` });
196
- send(sendOutputs());
87
+ node.runtime.alarmMessage = node.config.noStatusOnRunMessage;
197
88
  }
89
+ send(sendOutputs());
90
+ updateStatus();
198
91
  node.runtime.statusTimer = null;
199
- }, node.runtime.statusTimeout * 1000);
92
+ }, node.config.statusTimeout * 1000);
200
93
  }
201
94
  } else {
202
- // Stop call: schedule status and alarm clear
203
- node.runtime.clearTimer = setTimeout(() => {
204
- node.runtime.status = false;
205
- node.runtime.alarm = false;
206
- node.runtime.alarmMessage = "";
207
- send(sendOutputs());
208
- updateStatus();
209
- node.runtime.clearTimer = null;
210
- }, node.runtime.clearDelay * 1000);
95
+ node.runtime.status = false;
96
+ node.runtime.alarm = false;
97
+ node.runtime.alarmMessage = "";
211
98
  }
212
- send(checkStatusConditions());
213
- } else {
99
+
100
+ // Check alarm conditions
101
+ checkAlarmConditions();
102
+ send(sendOutputs());
214
103
  updateStatus();
215
104
  }
216
105
 
217
106
  if (done) done();
218
107
 
219
- function checkStatusConditions() {
220
- if (node.runtime.call && node.runtime.runLostStatus) {
221
- if (node.runtime.status !== node.runtime.normalOn) {
222
- node.runtime.alarm = true;
223
- node.runtime.alarmMessage = node.runtime.runLostStatusMessage;
224
- node.status({ fill: "red", shape: "dot", text: `run lost, alarm: true` });
225
- return sendOutputs();
226
- }
108
+ function checkAlarmConditions() {
109
+ if (node.runtime.status && !node.runtime.call) {
110
+ node.runtime.alarm = true;
111
+ node.runtime.alarmMessage = "Status active without call";
112
+ return;
227
113
  }
228
- if (!node.runtime.call && node.runtime.status !== node.runtime.normalOff) {
114
+
115
+ if (node.runtime.call && !node.runtime.status && !node.runtime.neverReceivedStatus && node.config.runLostStatus) {
229
116
  node.runtime.alarm = true;
230
- node.runtime.alarmMessage = "Status mismatch when off";
231
- node.status({ fill: "red", shape: "dot", text: `off mismatch, alarm: true` });
232
- return sendOutputs();
117
+ node.runtime.alarmMessage = node.config.runLostStatusMessage;
118
+ return;
233
119
  }
234
- if (!node.runtime.alarm || (node.runtime.status === node.runtime.normalOn && node.runtime.call) || (node.runtime.status === node.runtime.normalOff && !node.runtime.call)) {
120
+
121
+ // No alarm conditions met. Don't clear alarm if timer is still running
122
+ if (!node.runtime.statusTimer) {
235
123
  node.runtime.alarm = false;
236
124
  node.runtime.alarmMessage = "";
237
125
  }
238
- updateStatus();
239
- return sendOutputs();
240
126
  }
241
127
 
242
128
  function sendOutputs() {
243
- return [
244
- { payload: node.runtime.call },
245
- {
246
- payload: {
247
- call: node.runtime.call,
248
- status: node.runtime.status,
249
- alarm: node.runtime.alarm,
250
- alarmMessage: node.runtime.alarmMessage,
251
- timeout: !!node.runtime.statusTimer
252
- }
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
253
138
  }
254
- ];
139
+ };
255
140
  }
256
141
 
257
142
  function updateStatus() {
@@ -264,11 +149,12 @@ module.exports = function(RED) {
264
149
  });
265
150
 
266
151
  node.on("close", function(done) {
267
- if (node.runtime.statusTimer) clearTimeout(node.runtime.statusTimer);
268
- if (node.runtime.clearTimer) clearTimeout(node.runtime.clearTimer);
152
+ if (node.runtime.statusTimer) {
153
+ clearTimeout(node.runtime.statusTimer);
154
+ }
269
155
  done();
270
156
  });
271
157
  }
272
158
 
273
159
  RED.nodes.registerType("call-status-block", CallStatusBlockNode);
274
- };
160
+ };