@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
package/nodes/or-block.js CHANGED
@@ -1,4 +1,6 @@
1
1
  module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
3
+
2
4
  function OrBlockNode(config) {
3
5
  RED.nodes.createNode(this, config);
4
6
  const node = this;
@@ -7,60 +9,87 @@ module.exports = function(RED) {
7
9
  node.inputs = Array(parseInt(config.slots) || 2).fill(false)
8
10
  node.slots = parseInt(config.slots);
9
11
 
10
- node.status({ fill: "green", shape: "dot", text: `slots: ${node.slots}` });
12
+ utils.setStatusOK(node, `slots: ${node.slots}`);
11
13
 
12
14
  // Initialize logic fields
13
15
  let lastResult = null;
14
16
  let lastInputs = node.inputs.slice();
17
+ let lastOutputTime = 0; // Track last output timestamp for debounce
18
+ let lastOutputValue = undefined; // Track last output value for duplicate suppression
19
+ const DEBOUNCE_MS = 500; // Debounce period in milliseconds
15
20
 
16
21
  node.on("input", function(msg, send, done) {
17
22
  send = send || function() { node.send.apply(node, arguments); };
18
23
 
19
24
  // Guard against invalid msg
20
25
  if (!msg) {
21
- node.status({ fill: "red", shape: "ring", text: "invalid message" });
26
+ utils.setStatusError(node, "invalid message");
22
27
  if (done) done();
23
28
  return;
24
29
  }
25
30
 
26
31
  // Check required properties
27
32
  if (!msg.hasOwnProperty("context")) {
28
- node.status({ fill: "red", shape: "ring", text: "missing context" });
33
+ utils.setStatusError(node, "missing context");
29
34
  if (done) done();
30
35
  return;
31
36
  }
32
37
 
33
38
  if (!msg.hasOwnProperty("payload")) {
34
- node.status({ fill: "red", shape: "ring", text: "missing payload" });
39
+ utils.setStatusError(node, "missing payload");
35
40
  if (done) done();
36
41
  return;
37
42
  }
38
43
 
39
44
  // Process input slot
40
45
  if (msg.context.startsWith("in")) {
41
- let index = parseInt(msg.context.slice(2), 10);
42
- if (!isNaN(index) && index >= 1 && index <= node.slots) {
43
- node.inputs[index - 1] = Boolean(msg.payload);
46
+ const slotVal = utils.validateSlotIndex(msg.context, node.slots);
47
+ if (slotVal.valid) {
48
+ node.inputs[slotVal.index - 1] = Boolean(msg.payload);
44
49
  const result = node.inputs.some(v => v === true);
45
50
  const isUnchanged = result === lastResult && node.inputs.every((v, i) => v === lastInputs[i]);
46
- node.status({
47
- fill: "blue",
48
- shape: isUnchanged ? "ring" : "dot",
49
- text: `in: [${node.inputs.join(", ")}], out: ${result}`
50
- });
51
+ const statusText = `in: [${node.inputs.join(", ")}], out: ${result}`;
52
+
53
+ // ================================================================
54
+ // Debounce: Suppress consecutive same outputs within 500ms
55
+ // But always output if value is different or debounce time expired
56
+ // ================================================================
57
+ const now = Date.now();
58
+ const timeSinceLastOutput = now - lastOutputTime;
59
+ const isSameOutput = result === lastOutputValue;
60
+ const shouldSuppress = isSameOutput && timeSinceLastOutput < DEBOUNCE_MS;
61
+
62
+ if (shouldSuppress) {
63
+ // Same output within debounce window - don't send, just update status
64
+ utils.setStatusUnchanged(node, statusText);
65
+ } else {
66
+ // Different output or debounce period expired - send it
67
+ if (isUnchanged) {
68
+ utils.setStatusUnchanged(node, statusText);
69
+ } else {
70
+ utils.setStatusChanged(node, statusText);
71
+ }
72
+
73
+ // Record output for next debounce comparison
74
+ lastOutputTime = now;
75
+ lastOutputValue = result;
76
+
77
+ // Send output to allow all downstream branches to update
78
+ send({ payload: result });
79
+ }
80
+
51
81
  lastResult = result;
52
82
  lastInputs = node.inputs.slice();
53
- send({ payload: result });
54
83
  if (done) done();
55
84
  return;
56
85
  } else {
57
- node.status({ fill: "red", shape: "ring", text: `invalid input index ${index || "NaN"}` });
86
+ utils.setStatusError(node, slotVal.error);
58
87
  if (done) done();
59
88
  return;
60
89
  }
61
90
  }
62
91
 
63
- node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
92
+ utils.setStatusWarn(node, "unknown context");
64
93
  if (done) done();
65
94
  });
66
95
 
@@ -4,6 +4,10 @@
4
4
  <label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
5
5
  <input type="text" id="node-input-name" placeholder="Name">
6
6
  </div>
7
+ <div class="form-row">
8
+ <label for="node-input-inputProperty" title="Message property to read input from"><i class="fa fa-folder-open"></i> Input Property</label>
9
+ <input type="text" id="node-input-inputProperty" placeholder="payload">
10
+ </div>
7
11
  <div class="form-row">
8
12
  <label for="node-input-kp" title="Proportional gain (number)"><i class="fa fa-sliders"></i> Kp</label>
9
13
  <input type="text" id="node-input-kp" placeholder="0" step="any">
@@ -29,6 +33,11 @@
29
33
  <input type="text" id="node-input-deadband" placeholder="0" step="any" min="0">
30
34
  <input type="hidden" id="node-input-deadbandType">
31
35
  </div>
36
+ <div class="form-row">
37
+ <label for="node-input-setpointRateLimit" title="Maximum setpoint change per second (non-negative number, 0 = no limit)"><i class="fa fa-arrow-right"></i> Setpoint Rate Limit</label>
38
+ <input type="text" id="node-input-setpointRateLimit" placeholder="0" step="any" min="0">
39
+ <input type="hidden" id="node-input-setpointRateLimitType">
40
+ </div>
32
41
  <div class="form-row">
33
42
  <label for="node-input-dbBehavior" title="Deadband behavior: ReturnToZero or HoldLastResult"><i class="fa fa-cog"></i> Deadband Behavior</label>
34
43
  <select id="node-input-dbBehavior">
@@ -69,6 +78,7 @@
69
78
  color: "#301934",
70
79
  defaults: {
71
80
  name: { value: "" },
81
+ inputProperty: { value: "payload" },
72
82
  kp: { value: 0, required: true },
73
83
  kpType: { value: "num" },
74
84
  ki: { value: 0, required: true },
@@ -79,6 +89,8 @@
79
89
  setpointType: { value: "num" },
80
90
  deadband: { value: 0, required: true },
81
91
  deadbandType: { value: "num" },
92
+ setpointRateLimit: { value: 0, required: true },
93
+ setpointRateLimitType: { value: "num" },
82
94
  dbBehavior: { value: "ReturnToZero" },
83
95
  outMin: { value: null },
84
96
  outMinType: { value: "num" },
@@ -134,6 +146,12 @@
134
146
  typeField: "#node-input-deadbandType"
135
147
  }).typedInput("type", node.deadbandType || "num").typedInput("value", node.deadband);
136
148
 
149
+ $("#node-input-setpointRateLimit").typedInput({
150
+ default: "num",
151
+ types: ["num", "msg", "flow", "global"],
152
+ typeField: "#node-input-setpointRateLimitType"
153
+ }).typedInput("type", node.setpointRateLimitType || "num").typedInput("value", node.setpointRateLimit);
154
+
137
155
  $("#node-input-outMin").typedInput({
138
156
  default: "num",
139
157
  types: ["num", "msg", "flow", "global"],
@@ -185,6 +203,7 @@ Implements a PID controller with deadband, output limits, and tuning.
185
203
  : kd (number) : Derivative gain. Default: 0.
186
204
  : setpoint (number) : Target setpoint. Default: 0.
187
205
  : deadband (number) : Deadband range around setpoint (non-negative). Default: 0.
206
+ : setpointRateLimit (number) : Maximum setpoint change per second (non-negative, 0 = no limit). Default: 0.
188
207
  : dbBehavior (string) : Deadband behavior (`"ReturnToZero"`, `"HoldLastResult"`). Default: `"ReturnToZero"`.
189
208
  : outMin (number | null) : Minimum output limit (less than outMax). Default: null.
190
209
  : outMax (number | null) : Maximum output limit (greater than outMin). Default: null.
@@ -193,13 +212,44 @@ Implements a PID controller with deadband, output limits, and tuning.
193
212
  : run (boolean) : Enable (true) or disable (false) PID calculation. Default: true.
194
213
 
195
214
  ### Details
196
- Calculates PID control output based on numeric `msg.payload`, setpoint, and gains (`kp`, `ki`, `kd`).
197
215
 
198
- Supports deadband, output limits, rate of change limit, direct/reverse action, integral clamping, and Ziegler-Nichols tuning.
216
+ **Core Algorithm**
217
+ Calculates PID control output every cycle based on `msg.payload` (input), setpoint, and configurable gains (`kp`, `ki`, `kd`). Supports both **Direct Action** (cooling: error↑ → output↑) and **Reverse Action** (heating: error↑ → output↓) modes.
218
+
219
+ **Error Calculation**
220
+ - Reverse Action (heating): `error = setpoint - input`
221
+ - Direct Action (cooling): `error = input - setpoint`
222
+
223
+ **PID Terms**
224
+ - **P (Proportional)**: Immediate response to error. `pGain = Kp × error`
225
+ - **I (Integral)**: Removes steady-state offset by accumulating error over time with anti-windup clamping to prevent excessive accumulation when output limits are active.
226
+ - **D (Derivative)**: Low-pass filtered to prevent noise amplification. Uses exponential smoothing (0.1 new + 0.9 old).
227
+
228
+ **Setpoint Rate Limiting**
229
+ Smoothly ramps setpoint changes at configured rate (units per second) to prevent integrator wind-up and thermal shock. With `setpointRateLimit = 0`, setpoint changes immediately.
230
+
231
+ **Deadband**
232
+ No output generated when input is within ±deadband of setpoint. Behavior controlled by `dbBehavior`:
233
+ - `ReturnToZero`: Output becomes 0
234
+ - `HoldLastResult`: Output holds previous value
235
+
236
+ **Output Limiting**
237
+ Hard limits applied before output transmission. `maxChange` further limits rate of change to prevent abrupt jumps (units per second).
238
+
239
+ **Relay Auto-Tuning (Ziegler-Nichols)**
240
+ Send `{context: "tune", payload: true}` to start automatic tuning. Uses bang-bang relay control to oscillate the system and measure:
241
+ - **Tu** (Ultimate Period): Time for one complete oscillation cycle
242
+ - **Ku** (Ultimate Gain): Peak oscillation amplitude
243
+
244
+ Calculates conservative "no overshoot" gains:
245
+ - `Kp = 0.2 × Ku`
246
+ - `Ki = 0.4 × Kp / Tu`
247
+ - `Kd = 0.066 × Kp × Tu`
199
248
 
200
- Outputs `{ payload: number, diagnostics: object }` when output changes, or `{ tuneResult: object }` on tuning completion.
249
+ Tuning data persists across input messages but resets on flow restart. Progress shown in status bar during measurement.
201
250
 
202
- Ziegler-Nichols tuning sets `kp = 0.6*Ku`, `ki = 2*kp/Tu`, `kd = kp*Tu/8` after detecting oscillations. Outputs only on change.
251
+ **Output**
252
+ Sends every calculation cycle: `{ payload: number, diagnostics: {...} }` showing P/I/D breakdown and current state. On tuning completion: `{ tuneResult: {...} }` with calculated gains and oscillation count.
203
253
 
204
254
  ### Error Handling
205
255
  - Missing `msg`: No output, red status (`invalid message`).