@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 +10 -0
- package/nodes/alarm-collector.html +11 -0
- package/nodes/alarm-collector.js +13 -0
- package/nodes/alarm-service.js +2 -2
- package/nodes/and-block.js +1 -1
- package/nodes/call-status-block.html +83 -56
- package/nodes/call-status-block.js +335 -248
- package/nodes/changeover-block.html +30 -31
- package/nodes/changeover-block.js +287 -389
- package/nodes/contextual-label-block.js +3 -3
- package/nodes/delay-block.js +74 -13
- package/nodes/global-getter.js +29 -14
- package/nodes/global-setter.js +8 -5
- package/nodes/history-buffer.js +32 -27
- package/nodes/history-collector.js +4 -4
- package/nodes/network-point-read.js +5 -0
- package/nodes/network-service-bridge.js +43 -11
- package/nodes/or-block.js +1 -1
- package/nodes/priority-block.js +1 -1
- package/nodes/tstat-block.html +34 -79
- package/nodes/tstat-block.js +223 -345
- package/nodes/utils.js +1 -1
- package/package.json +90 -75
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();
|
package/nodes/alarm-collector.js
CHANGED
|
@@ -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}`);
|
package/nodes/alarm-service.js
CHANGED
|
@@ -48,7 +48,7 @@ module.exports = function(RED) {
|
|
|
48
48
|
|
|
49
49
|
// Send alarm message with status
|
|
50
50
|
const msg = {
|
|
51
|
-
|
|
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
|
-
|
|
74
|
+
alarm: eventData,
|
|
75
75
|
status: { state: "cleared", transition: eventData.transition },
|
|
76
76
|
activeAlarmCount: activeCount,
|
|
77
77
|
alarmKey: key
|
package/nodes/and-block.js
CHANGED
|
@@ -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 = `
|
|
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-
|
|
9
|
-
<input type="text" id="node-input-
|
|
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-
|
|
13
|
-
<input type="text" id="node-input-
|
|
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,
|
|
17
|
-
<input type="number" id="node-input-statusTimeout" placeholder="30" min="0
|
|
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="
|
|
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-
|
|
29
|
-
<input type="
|
|
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-
|
|
37
|
-
<input type="checkbox" id="node-input-
|
|
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-
|
|
45
|
-
<input type="text" id="node-input-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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:
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
+
1. **No Status Response** (hysteresis: statusTimeout seconds) — Triggered when call=true but no status arrives within initial timeout.
|
|
127
157
|
|
|
128
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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):
|
|
146
|
-
- Blue (
|
|
147
|
-
-
|
|
148
|
-
- Red (ring):
|
|
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
|
|