@bldgblocks/node-red-contrib-control 0.1.37 → 0.1.38

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/README.md CHANGED
@@ -43,3 +43,13 @@ Search for the package name and add to your project.
43
43
  - $ npm install node-red-contrib-buildingblocks-control
44
44
  # then restart node-red
45
45
  ```
46
+
47
+ ## Testing
48
+ Tests use [Mocha](https://mochajs.org/) and [node-red-node-test-helper](https://github.com/node-red/node-red-node-test-helper) to run nodes in an isolated, in-memory Node-RED runtime. Your live Node-RED instance is never touched.
49
+
50
+ ```bash
51
+ npm install # install dev dependencies (mocha, test helper)
52
+ npm test # run all tests
53
+ ```
54
+
55
+ Test files live in `test/` and follow the naming convention `*_spec.js`. Shared utilities are in `test/test-helpers.js`.
@@ -96,6 +96,7 @@
96
96
  <div class="form-row">
97
97
  <label for="node-input-message"><i class="fa fa-comment"></i> Message</label>
98
98
  <input type="text" id="node-input-message" placeholder="Zone exceeds setpoint">
99
+ <input type="hidden" id="node-input-messageType">
99
100
  </div>
100
101
 
101
102
  <div class="form-row">
@@ -131,6 +132,7 @@
131
132
  topic: { value: "Alarms_Default" },
132
133
  title: { value: "Alarm" },
133
134
  message: { value: "Condition triggered" },
135
+ messageType: { value: "str" },
134
136
  tags: { value: "" },
135
137
  units: { value: "°F" }
136
138
  },
@@ -197,6 +199,15 @@
197
199
  typeField: "#node-input-inputFieldType"
198
200
  }).typedInput("type", node.inputFieldType || "msg").typedInput("value", node.inputField || "payload");
199
201
 
202
+ // Setup message typedInput: static string or from msg property
203
+ $("#node-input-message").typedInput({
204
+ types: [
205
+ "str",
206
+ "msg"
207
+ ],
208
+ typeField: "#node-input-messageType"
209
+ }).typedInput("type", node.messageType || "str").typedInput("value", node.message || "Condition triggered");
210
+
200
211
  // Show/hide sections based on inputMode
201
212
  const updateDisplay = () => {
202
213
  let mode = $("#node-input-inputMode").val();
@@ -20,6 +20,7 @@ module.exports = function(RED) {
20
20
  node.topic = config.topic || "Alarms_Default";
21
21
  node.title = config.title || "Alarm";
22
22
  node.message = config.message || "Condition triggered";
23
+ node.messageType = config.messageType || "str";
23
24
  node.tags = config.tags || "";
24
25
  node.units = config.units || "";
25
26
 
@@ -257,6 +258,18 @@ module.exports = function(RED) {
257
258
  }
258
259
  }
259
260
 
261
+ // Resolve message dynamically if configured as msg property
262
+ if (node.messageType === "msg") {
263
+ try {
264
+ const resolved = await utils.evaluateNodeProperty(config.message, "msg", node, msg);
265
+ if (resolved !== undefined && resolved !== null) {
266
+ node.message = String(resolved);
267
+ }
268
+ } catch (e) {
269
+ // Keep existing message on error
270
+ }
271
+ }
272
+
260
273
  evaluateAndEmit(inputValue);
261
274
  } catch (err) {
262
275
  utils.setStatusError(node, `Error reading input: ${err.message}`);
@@ -48,7 +48,7 @@ module.exports = function(RED) {
48
48
 
49
49
  // Send alarm message with status
50
50
  const msg = {
51
- payload: eventData,
51
+ alarm: eventData,
52
52
  status: { state: "triggered", transition: eventData.transition },
53
53
  activeAlarmCount: activeCount,
54
54
  alarmKey: key
@@ -71,7 +71,7 @@ module.exports = function(RED) {
71
71
 
72
72
  // Send clear message with status
73
73
  const msg = {
74
- payload: eventData,
74
+ alarm: eventData,
75
75
  status: { state: "cleared", transition: eventData.transition },
76
76
  activeAlarmCount: activeCount,
77
77
  alarmKey: key
@@ -48,7 +48,7 @@ module.exports = function(RED) {
48
48
  node.inputs[slotVal.index - 1] = Boolean(msg.payload);
49
49
  const result = node.inputs.every(v => v === true);
50
50
  const isUnchanged = result === lastResult && node.inputs.every((v, i) => v === lastInputs[i]);
51
- const statusText = `in: [${node.inputs.join(", ")}], out: ${result}`;
51
+ const statusText = `[${node.inputs.join(", ")}] -> ${result}`;
52
52
 
53
53
  // ================================================================
54
54
  // Debounce: Suppress consecutive same outputs within 500ms
@@ -5,16 +5,22 @@
5
5
  <input type="text" id="node-input-name" placeholder="Name">
6
6
  </div>
7
7
  <div class="form-row">
8
- <label for="node-input-inputProperty" title="Message property for call (request) signal"><i class="fa fa-sign-in"></i> Call Property</label>
9
- <input type="text" id="node-input-inputProperty" placeholder="payload">
8
+ <label for="node-input-callValue" title="Call (request) signal typed input (bool, msg, flow, global)"><i class="fa fa-sign-in"></i> Call Value</label>
9
+ <input type="text" id="node-input-callValue" placeholder="payload">
10
+ <input type="hidden" id="node-input-callValueType">
10
11
  </div>
11
12
  <div class="form-row">
12
- <label for="node-input-statusInputProperty" title="Message property for status (response) signal"><i class="fa fa-sign-out"></i> Status Property</label>
13
- <input type="text" id="node-input-statusInputProperty" placeholder="status">
13
+ <label for="node-input-statusValue" title="Status (response) signal typed input (bool, msg, flow, global)"><i class="fa fa-sign-out"></i> Status Value</label>
14
+ <input type="text" id="node-input-statusValue" placeholder="status">
15
+ <input type="hidden" id="node-input-statusValueType">
14
16
  </div>
15
17
  <div class="form-row">
16
- <label for="node-input-statusTimeout" title="Time to wait for status response (seconds, positive number)"><i class="fa fa-clock-o"></i> Status Timeout</label>
17
- <input type="number" id="node-input-statusTimeout" placeholder="30" min="0.01" step="any">
18
+ <label for="node-input-statusTimeout" title="Time to wait for initial status response after call activates (seconds, 0=disabled)"><i class="fa fa-clock-o"></i> Status Timeout</label>
19
+ <input type="number" id="node-input-statusTimeout" placeholder="30" min="0" step="any">
20
+ </div>
21
+ <div class="form-row">
22
+ <label for="node-input-heartbeatTimeout" title="Heartbeat window for continuous status monitoring (seconds, 0=disabled)"><i class="fa fa-heartbeat"></i> Heartbeat Timeout</label>
23
+ <input type="number" id="node-input-heartbeatTimeout" placeholder="0" min="0" step="any">
18
24
  </div>
19
25
  <div class="form-row">
20
26
  <label for="node-input-clearDelay" title="Delay before clearing status and alarm after call ends (seconds, 0=immediate)"><i class="fa fa-hourglass"></i> Clear Delay</label>
@@ -22,27 +28,31 @@
22
28
  </div>
23
29
  <div class="form-row">
24
30
  <label for="node-input-debounce" title="Debounce status flicker (milliseconds, 0=disabled)"><i class="fa fa-filter"></i> Debounce</label>
25
- <input type="number" id="node-input-debounce" placeholder="100" min="0" step="any">
31
+ <input type="number" id="node-input-debounce" placeholder="0" min="0" step="any">
26
32
  </div>
27
33
  <div class="form-row">
28
- <label for="node-input-heartbeatTimeout" title="Heartbeat window for continuous status monitoring (seconds, 0=disabled)"><i class="fa fa-pulse"></i> Heartbeat Timeout</label>
29
- <input type="number" id="node-input-heartbeatTimeout" placeholder="30" min="0" step="any">
34
+ <label for="node-input-noStatusOnRun" title="Alarm if no status received during call (boolean)"><i class="fa fa-exclamation-triangle"></i> No Status On Run</label>
35
+ <input type="checkbox" id="node-input-noStatusOnRun" style="width: auto; vertical-align: middle;">
30
36
  </div>
31
37
  <div class="form-row">
32
38
  <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>
33
39
  <input type="checkbox" id="node-input-runLostStatus" style="width: auto; vertical-align: middle;">
34
40
  </div>
35
41
  <div class="form-row">
36
- <label for="node-input-noStatusOnRun" title="Alarm if no status received during call (boolean)"><i class="fa fa-exclamation-triangle"></i> No Status On Run</label>
37
- <input type="checkbox" id="node-input-noStatusOnRun" style="width: auto; vertical-align: middle;">
42
+ <label for="node-input-statusWithoutCall" title="Alarm if equipment status is active without a call signal (boolean)"><i class="fa fa-exclamation-triangle"></i> Status Without Call</label>
43
+ <input type="checkbox" id="node-input-statusWithoutCall" style="width: auto; vertical-align: middle;">
44
+ </div>
45
+ <div class="form-row">
46
+ <label for="node-input-noStatusOnRunMessage" title="Message for no status on run alarm (string)"><i class="fa fa-comment"></i> No Status Message</label>
47
+ <input type="text" id="node-input-noStatusOnRunMessage" placeholder="No status received during run">
38
48
  </div>
39
49
  <div class="form-row">
40
50
  <label for="node-input-runLostStatusMessage" title="Message for run lost status alarm (string)"><i class="fa fa-comment"></i> Run Lost Message</label>
41
51
  <input type="text" id="node-input-runLostStatusMessage" placeholder="Status lost during run">
42
52
  </div>
43
53
  <div class="form-row">
44
- <label for="node-input-noStatusOnRunMessage" title="Message for no status on run alarm (string)"><i class="fa fa-comment"></i> No Status Message</label>
45
- <input type="text" id="node-input-noStatusOnRunMessage" placeholder="No status received during run">
54
+ <label for="node-input-statusWithoutCallMessage" title="Message for status without call alarm (string)"><i class="fa fa-comment"></i> Without Call Message</label>
55
+ <input type="text" id="node-input-statusWithoutCallMessage" placeholder="Status active without call">
46
56
  </div>
47
57
  </script>
48
58
 
@@ -53,16 +63,20 @@
53
63
  color: "#301934",
54
64
  defaults: {
55
65
  name: { value: "" },
56
- inputProperty: { value: "payload" },
57
- statusInputProperty: { value: "status" },
58
- statusTimeout: { value: 30, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) > 0; } },
66
+ callValue: { value: "payload" },
67
+ callValueType: { value: "msg" },
68
+ statusValue: { value: "status" },
69
+ statusValueType: { value: "msg" },
70
+ statusTimeout: { value: 30, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0; } },
71
+ heartbeatTimeout: { value: 0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0; } },
59
72
  clearDelay: { value: 10, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0; } },
60
- debounce: { value: 100, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0; } },
61
- heartbeatTimeout: { value: 30, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0; } },
73
+ debounce: { value: 0, required: true, validate: function(v) { return !isNaN(parseFloat(v)) && parseFloat(v) >= 0; } },
62
74
  runLostStatus: { value: false },
63
75
  noStatusOnRun: { value: true },
76
+ statusWithoutCall: { value: true },
64
77
  runLostStatusMessage: { value: "Status lost during run" },
65
- noStatusOnRunMessage: { value: "No status received during run" }
78
+ noStatusOnRunMessage: { value: "No status received during run" },
79
+ statusWithoutCallMessage: { value: "Status active without call" }
66
80
  },
67
81
  inputs: 1,
68
82
  outputs: 1,
@@ -72,6 +86,24 @@
72
86
  paletteLabel: "call status",
73
87
  label: function() {
74
88
  return this.name || "call status";
89
+ },
90
+ oneditprepare: function() {
91
+ const node = this;
92
+ try {
93
+ $("#node-input-callValue").typedInput({
94
+ default: "msg",
95
+ types: ["msg", "flow", "global", "bool"],
96
+ typeField: "#node-input-callValueType"
97
+ }).typedInput("type", node.callValueType || "msg").typedInput("value", node.callValue || "payload");
98
+
99
+ $("#node-input-statusValue").typedInput({
100
+ default: "msg",
101
+ types: ["msg", "flow", "global", "bool"],
102
+ typeField: "#node-input-statusValueType"
103
+ }).typedInput("type", node.statusValueType || "msg").typedInput("value", node.statusValue || "status");
104
+ } catch(e) {
105
+ console.error("Error preparing call-status-block editor:", e);
106
+ }
75
107
  }
76
108
  });
77
109
  </script>
@@ -82,8 +114,11 @@ Monitors call and status signals to detect equipment faults, communication losse
82
114
 
83
115
  ### Inputs
84
116
 
85
- : payload (boolean) : Requested equipment state (call signal). Default property name for sending call commands. When true, block expects equipment to respond with status=true within timeout period. Property name configurable via inputProperty setting.
86
- : status (boolean) : Equipment response signal. Updates actual equipment state and triggers state transitions. Default property name `msg.status`. Accepts `msg.context="status"` with `msg.payload` as fallback routing.
117
+ Both **Call Value** and **Status Value** are **typed inputs** they can read from `msg` properties, `flow` variables, `global` variables, or static `bool` values.
118
+
119
+ Every incoming message triggers the node to evaluate both values and process state transitions.
120
+
121
+ : context (string) : Optional. Send `msg.context = "reset"` with `msg.payload = true` to reset all state and clear alarms.
87
122
 
88
123
  ### Outputs
89
124
 
@@ -93,60 +128,52 @@ Monitors call and status signals to detect equipment faults, communication losse
93
128
 
94
129
  ### Properties
95
130
 
96
- - name (string) - Display name in editor. Default: `"call status"`.
97
- - inputProperty (string) - Input message property for call signal. Default: `"payload"`.
98
- - statusInputProperty (string) - Input message property for status signal. Default: `"status"`.
99
- - statusTimeout (number) - Seconds to wait for initial status response (0.01–3600s). Default: `30`. If heartbeat monitoring is disabled, this is the only timeout check.
100
- - heartbeatTimeout (number) - Seconds for continuous status heartbeat monitoring while call=true (0–3600s). Default: `30`. Set to 0 to disable heartbeat monitoring.
101
- - clearDelay (number) - Seconds to hold actual state after call deactivated (0–3600s). Default: `10`.
102
- - debounce (number) - Milliseconds to filter status flicker (0–10000ms). Default: `100`.
103
- - runLostStatus (checkbox) - Alarm if equipment status becomes false while call is true. Default: checked.
104
- - noStatusOnRun (checkbox) - Alarm if no status response within initial timeout. Default: checked.
105
- - runLostStatusMessage (string) - Alarm text for status lost. Default: `"Status lost during run"`.
106
- - noStatusOnRunMessage (string) - Alarm text for no status. Default: `"No status received during run"`.
131
+ - **Call Value** (typed input) Source of the call (request) signal. Default: `msg.payload`. Can be `msg`, `flow`, `global`, or `bool`.
132
+ - **Status Value** (typed input) Source of the status (response) signal. Default: `msg.status`. Can be `msg`, `flow`, `global`, or `bool`.
133
+ - **Status Timeout** (number) Seconds to wait for the *first* status=true after call activates (0=disabled). This is a one-shot timer for initial equipment response. Default: `30`.
134
+ - **Heartbeat Timeout** (number) Seconds for ongoing status freshness monitoring while running. Each status=true resets the timer. If it expires without a refresh, status is considered lost (0=disabled). Default: `0`.
135
+ - **Clear Delay** (number) Seconds to hold actual state after call deactivated (0=immediate). Default: `10`.
136
+ - **Debounce** (number) Milliseconds to filter status value changes (0=disabled). Default: `0`.
137
+ - **No Status On Run** (checkbox) Alarm if no status response within initial timeout. Default: checked.
138
+ - **Run Lost Status** (checkbox) Alarm if equipment status becomes false while call is true. Default: unchecked.
139
+ - **Status Without Call** (checkbox) Alarm if equipment status is active without a call signal. Default: checked.
140
+ - **No Status Message** (string) Alarm text for no status. Default: `"No status received during run"`.
141
+ - **Run Lost Message** (string) Alarm text for status lost. Default: `"Status lost during run"`.
142
+ - **Without Call Message** (string) — Alarm text for status without call. Default: `"Status active without call"`.
107
143
 
108
144
  ### Details
109
145
 
110
- The block implements a 4-state controller: IDLE (call=false), WAITING_FOR_STATUS (call=true, status pending), RUNNING (call=true, status=true), and STATUS_LOST (call=true, status=false).
146
+ The block implements a 4-state controller: **IDLE** (call=false), **WAITING_FOR_STATUS** (call=true, status pending), **RUNNING** (call=true, status=true), and **STATUS_LOST** (call=true, status=false after previously being true).
111
147
 
112
- When call=true, the block expects two things: (1) Initial status arrival within statusTimeout seconds, and (2) if heartbeatTimeout is enabled, continuous status updates at least once every heartbeatTimeout seconds. If either requirement is violated, an alarm is triggered.
148
+ On every incoming message, the node evaluates both the call and status typed inputs simultaneously. This means a single message can carry both signals (e.g., `msg.payload` for call and `msg.status` for equipment feedback).
113
149
 
114
- When call=false but status=true, the block monitors that status goes false within clearDelay + 1 second. If status remains true beyond this window, an alarm is triggered indicating the equipment did not respond to the deactivation.
150
+ **Heartbeat refresh**: Repeated status=true values refresh the heartbeat timer even though the value hasn't changed. This is critical equipment that continuously reports `status: true` must keep the heartbeat alive.
115
151
 
116
- Input routing processes messages in priority order: Status Update (from `msg.status` or `msg.context="status"`), Call Request (from configured input property).
117
-
118
- Debounce collapses rapid status changes within the configured window into a single state transition, preventing false alarms from sensor noise. All alarm conditions include 100ms hysteresis to prevent false triggers.
119
-
120
- #### Heartbeat Monitoring
121
-
122
- When call=true and heartbeatTimeout > 0, the block continuously verifies that status updates arrive within the heartbeat window. This detects communication loss or equipment failures mid-cycle. Set heartbeatTimeout=0 to disable this feature.
152
+ **Debounce** only delays processing of status *value changes*. Same-value status updates always refresh the heartbeat immediately.
123
153
 
124
154
  #### Alarm Conditions
125
155
 
126
- Status Active Without Call (Hysteresis 100ms): Triggered when status=true AND call=false, indicating potential equipment fault, check valve failure, or external signal interference.
156
+ 1. **No Status Response** (hysteresis: statusTimeout seconds) Triggered when call=true but no status arrives within initial timeout.
127
157
 
128
- No Status Response on Run (Hysteresis: statusTimeout seconds): Triggered when call=true but no status arrives within initial timeout, indicating equipment not responding or wiring failure at call initiation.
158
+ 2. **Status Lost During Run** (hysteresis: 100ms) Triggered when call=true, status was true, but status goes false mid-cycle. Also triggered by heartbeat timeout if enabled.
129
159
 
130
- Status Lost During Run (Hysteresis 100ms): Triggered when call=true, status=true initially, but either: (1) status goes false mid-cycle, or (2) heartbeat monitoring detects no status update within heartbeatTimeout window. Indicates mid-cycle shutdown or communication loss.
160
+ 3. **Status Active Without Call** (hysteresis: 100ms) Triggered when status=true but call=false (and no clearTimer running), indicating unexpected equipment activity.
131
161
 
132
- Status Not Clearing (Hysteresis 100ms): Triggered when call=false, status=true initially, but status does not go false within clearDelay + 1 second window. Indicates equipment failed to deactivate.
162
+ 4. **Status Not Clearing** (hysteresis: clearDelay+1 seconds) Triggered when call=false but status remains true beyond the clear delay window.
133
163
 
134
164
  #### HVAC Scenarios
135
165
 
136
- Scenario 1: Chiller with unloader valve feedback. Send `{payload: true}` to request call. Chiller responds with `{context: "status", payload: true}` within 30s. As long as call=true and status updates arrive every 30 seconds, no alarm. If 31 seconds elapse without update, heartbeat alarm triggers. Configuration: Status Timeout=30s, Heartbeat Timeout=30s, Run Lost Status=true.
137
-
138
- Scenario 2: VAV box with wireless controller. Send `{payload: true}` to activate damper. If wireless module not responding, No Status alarm triggers after 10s. Configuration: Status Timeout=10s, Heartbeat Timeout=0 (heartbeat disabled), No Status On Run=true.
166
+ **Chiller with feedback**: Call Value = `msg.payload`, Status Value = `flow.chillerStatus`. Send `{payload: true}` to request. Chiller writes status to flow variable. Config: Status Timeout=30s, Heartbeat=30s, Run Lost Status=checked.
139
167
 
140
- Scenario 3: Pump with 5-second startup lag. Send `{payload: true}` at t=0. Pump motor spins up for 5 seconds, status arrives at t=5s. Send `{payload: false}` at t=10s. Clear Delay holds actual state for 3 seconds. At t=13s, state fully cleared. Configuration: Status Timeout=8s, Heartbeat Timeout=15s, Clear Delay=3s, Debounce=200ms.
168
+ **VAV box wireless**: Call Value = `msg.payload`, Status Value = `msg.status`. Config: Status Timeout=10s, Heartbeat=0 (disabled), No Status On Run=checked.
141
169
 
142
- Scenario 4: Equipment with 60-second status heartbeat. Enable heartbeat monitoring with 60-second window. Configuration: Status Timeout=30s, Heartbeat Timeout=60s. Status must arrive at least once every 60 seconds to avoid "Status Lost" alarm.
170
+ **Pump with startup lag**: Call Value = `msg.payload`, Status Value = `global.pumpRunning`. Config: Status Timeout=8s, Heartbeat=15s, Clear Delay=3s, Debounce=200ms.
143
171
 
144
172
  ### Status
145
- - Green (dot): Configuration update
146
- - Blue (dot): State changed
147
- - Blue (ring): State unchanged
148
- - Red (ring): Error
149
- - Yellow (ring): Warning
173
+ - Green (dot): Running normally (call=ON, status=ON)
174
+ - Blue (ring): Idle (call=OFF, status=OFF)
175
+ - Yellow (ring): Waiting/warning state
176
+ - Red (ring): Alarm active
150
177
 
151
178
  ### References
152
179