@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
@@ -1,13 +1,14 @@
1
1
  module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
3
+
2
4
  function PriorityBlockNode(config) {
3
5
  RED.nodes.createNode(this, config);
4
6
  const node = this;
5
7
  const context = this.context();
6
8
 
7
9
  // Initialize runtime state
8
- node.runtime = {
9
- name: config.name
10
- };
10
+ // Initialize state
11
+ node.name = config.name;
11
12
 
12
13
  // Initialize state from context or defaults
13
14
  let priorities = context.get("priorities") || {
@@ -37,14 +38,14 @@ module.exports = function(RED) {
37
38
 
38
39
  // Guard against invalid message
39
40
  if (!msg) {
40
- node.status({ fill: "red", shape: "ring", text: "invalid message" });
41
+ utils.setStatusError(node, "invalid message");
41
42
  if (done) done();
42
43
  return;
43
44
  }
44
45
 
45
46
  // Validate payload
46
47
  if (!msg.hasOwnProperty("payload")) {
47
- node.status({ fill: "red", shape: "ring", text: "missing payload" });
48
+ utils.setStatusError(node, "missing payload");
48
49
  if (done) done();
49
50
  return;
50
51
  }
@@ -72,7 +73,7 @@ module.exports = function(RED) {
72
73
  context.set("defaultValue", defaultValue);
73
74
  context.set("fallbackValue", fallbackValue);
74
75
  context.set("messages", messages);
75
- node.status({ fill: "green", shape: "dot", text: "all slots cleared" });
76
+ utils.setStatusOK(node, "all slots cleared");
76
77
  } else if (typeof clear === "string" && isValidSlot(clear)) {
77
78
  if (clear.startsWith("priority")) priorities[clear] = null;
78
79
  else if (clear === "default") defaultValue = null;
@@ -82,7 +83,7 @@ module.exports = function(RED) {
82
83
  context.set("defaultValue", defaultValue);
83
84
  context.set("fallbackValue", fallbackValue);
84
85
  context.set("messages", messages);
85
- node.status({ fill: "green", shape: "dot", text: `${clear} cleared` });
86
+ utils.setStatusOK(node, `${clear} cleared`);
86
87
  } else if (Array.isArray(clear) && clear.every(isValidSlot)) {
87
88
  clear.forEach(slot => {
88
89
  if (slot.startsWith("priority")) priorities[slot] = null;
@@ -94,16 +95,16 @@ module.exports = function(RED) {
94
95
  context.set("defaultValue", defaultValue);
95
96
  context.set("fallbackValue", fallbackValue);
96
97
  context.set("messages", messages);
97
- node.status({ fill: "green", shape: "dot", text: `${clear.join(", ")} cleared` });
98
+ utils.setStatusOK(node, `${clear.join(", ")} cleared`);
98
99
  } else {
99
- node.status({ fill: "red", shape: "ring", text: "invalid clear" });
100
+ utils.setStatusError(node, "invalid clear");
100
101
  if (done) done();
101
102
  return;
102
103
  }
103
104
  } else if (msg.payload === "clear") {
104
105
  // Handle string "clear" with msg.context
105
106
  if (!msg.hasOwnProperty("context") || typeof msg.context !== "string") {
106
- node.status({ fill: "red", shape: "ring", text: "missing or invalid context for clear" });
107
+ utils.setStatusError(node, "missing or invalid context for clear");
107
108
  if (done) done();
108
109
  return;
109
110
  }
@@ -117,16 +118,16 @@ module.exports = function(RED) {
117
118
  context.set("defaultValue", defaultValue);
118
119
  context.set("fallbackValue", fallbackValue);
119
120
  context.set("messages", messages);
120
- node.status({ fill: "green", shape: "dot", text: `${contextMsg} cleared` });
121
+ utils.setStatusOK(node, `${contextMsg} cleared`);
121
122
  } else {
122
- node.status({ fill: "red", shape: "ring", text: "invalid clear context" });
123
+ utils.setStatusError(node, "invalid clear context");
123
124
  if (done) done();
124
125
  return;
125
126
  }
126
127
  } else {
127
128
  // Handle non-object, non-"clear" payloads
128
129
  if (!msg.hasOwnProperty("context") || typeof msg.context !== "string") {
129
- node.status({ fill: "red", shape: "ring", text: "missing or invalid context" });
130
+ utils.setStatusError(node, "missing or invalid context");
130
131
  if (done) done();
131
132
  return;
132
133
  }
@@ -135,7 +136,7 @@ module.exports = function(RED) {
135
136
  const value = msg.payload === null ? null : typeof msg.payload === "number" ? parseFloat(msg.payload) : typeof msg.payload === "boolean" ? msg.payload : null;
136
137
 
137
138
  if (value === null && msg.payload !== null) {
138
- node.status({ fill: "red", shape: "ring", text: `invalid ${contextMsg}` });
139
+ utils.setStatusError(node, `invalid ${contextMsg}`);
139
140
  if (done) done();
140
141
  return;
141
142
  }
@@ -145,33 +146,24 @@ module.exports = function(RED) {
145
146
  messages[contextMsg] = RED.util.cloneMessage(msg);
146
147
  context.set("priorities", priorities);
147
148
  context.set("messages", messages);
148
- node.status({
149
- fill: "green",
150
- shape: "dot",
151
- text: value === null ? `${contextMsg} relinquished` : `${contextMsg}: ${typeof value === "number" ? value.toFixed(2) : value}`
152
- });
149
+ const priorityText = value === null ? `${contextMsg} relinquished` : `${contextMsg}: ${typeof value === "number" ? value.toFixed(2) : value}`;
150
+ utils.setStatusOK(node, priorityText);
153
151
  } else if (contextMsg === "default") {
154
152
  defaultValue = value;
155
153
  messages[contextMsg] = RED.util.cloneMessage(msg);
156
154
  context.set("defaultValue", defaultValue);
157
155
  context.set("messages", messages);
158
- node.status({
159
- fill: "green",
160
- shape: "dot",
161
- text: value === null ? "default relinquished" : `default: ${typeof value === "number" ? value.toFixed(2) : value}`
162
- });
156
+ const defaultText = value === null ? "default relinquished" : `default: ${typeof value === "number" ? value.toFixed(2) : value}`;
157
+ utils.setStatusOK(node, defaultText);
163
158
  } else if (contextMsg === "fallback") {
164
159
  fallbackValue = value;
165
160
  messages[contextMsg] = RED.util.cloneMessage(msg);
166
161
  context.set("fallbackValue", fallbackValue);
167
162
  context.set("messages", messages);
168
- node.status({
169
- fill: "green",
170
- shape: "dot",
171
- text: value === null ? "fallback relinquished" : `fallback: ${typeof value === "number" ? value.toFixed(2) : value}`
172
- });
163
+ const fallbackText = value === null ? "fallback relinquished" : `fallback: ${typeof value === "number" ? value.toFixed(2) : value}`;
164
+ utils.setStatusOK(node, fallbackText);
173
165
  } else {
174
- node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
166
+ utils.setStatusWarn(node, "unknown context");
175
167
  if (done) done("Unknown context");
176
168
  return;
177
169
  }
@@ -182,11 +174,8 @@ module.exports = function(RED) {
182
174
  send(currentOutput);
183
175
  const inDisplay = typeof msg.payload === "number" ? msg.payload.toFixed(2) : typeof msg.payload === "object" ? JSON.stringify(msg.payload).slice(0, 20) : msg.payload;
184
176
  const outDisplay = currentOutput.payload === null ? "null" : typeof currentOutput.payload === "number" ? currentOutput.payload.toFixed(2) : currentOutput.payload;
185
- node.status({
186
- fill: "blue",
187
- shape: "dot",
188
- text: `in: ${inDisplay}, out: ${outDisplay}, slot: ${currentOutput.diagnostics.activePriority || "none"}`
189
- });
177
+ const statusText = `in: ${inDisplay}, out: ${outDisplay}, slot: ${currentOutput.diagnostics.activePriority || "none"}`;
178
+ utils.setStatusChanged(node, statusText);
190
179
 
191
180
  if (done) done();
192
181
 
@@ -1,64 +1,64 @@
1
1
  module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
2
3
  function RateLimitBlockNode(config) {
3
4
  RED.nodes.createNode(this, config);
4
5
  const node = this;
5
6
 
6
7
  // Initialize runtime state
7
- node.runtime = {
8
- name: config.name,
9
- mode: config.mode,
10
- rate: parseFloat(config.rate),
11
- interval: parseInt(config.interval),
12
- threshold: parseFloat(config.threshold),
13
- currentValue: 0,
14
- targetValue: 0,
15
- lastUpdate: Date.now(),
16
- lastInputMsg: null
17
- };
8
+ // Initialize state
9
+ node.name = config.name;
10
+ node.mode = config.mode;
11
+ node.rate = parseFloat(config.rate);
12
+ node.interval = parseInt(config.interval);
13
+ node.threshold = parseFloat(config.threshold);
14
+ node.currentValue = 0;
15
+ node.targetValue = 0;
16
+ node.lastUpdate = Date.now();
17
+ node.lastInputMsg = null;
18
18
 
19
19
  // Validate initial config
20
- if (isNaN(node.runtime.rate) || node.runtime.rate <= 0 || !isFinite(node.runtime.rate)) {
21
- node.runtime.rate = 1.0;
22
- node.status({ fill: "red", shape: "ring", text: "invalid rate" });
20
+ if (isNaN(node.rate) || node.rate <= 0 || !isFinite(node.rate)) {
21
+ node.rate = 1.0;
22
+ utils.setStatusError(node, "invalid rate");
23
23
  }
24
- if (isNaN(node.runtime.interval) || node.runtime.interval < 10 || !Number.isInteger(node.runtime.interval)) {
25
- node.runtime.interval = 100;
26
- node.status({ fill: "red", shape: "ring", text: "invalid interval" });
24
+ if (isNaN(node.interval) || node.interval < 10 || !Number.isInteger(node.interval)) {
25
+ node.interval = 100;
26
+ utils.setStatusError(node, "invalid interval");
27
27
  }
28
- if (isNaN(node.runtime.threshold) || node.runtime.threshold < 0 || !isFinite(node.runtime.threshold)) {
29
- node.runtime.threshold = 5.0;
30
- node.status({ fill: "red", shape: "ring", text: "invalid threshold" });
28
+ if (isNaN(node.threshold) || node.threshold < 0 || !isFinite(node.threshold)) {
29
+ node.threshold = 5.0;
30
+ utils.setStatusError(node, "invalid threshold");
31
31
  }
32
- if (!["rate-limit", "threshold", "full-value"].includes(node.runtime.mode)) {
33
- node.runtime.mode = "rate-limit";
34
- node.status({ fill: "red", shape: "ring", text: "invalid mode" });
32
+ if (!["rate-limit", "threshold", "full-value"].includes(node.mode)) {
33
+ node.mode = "rate-limit";
34
+ utils.setStatusError(node, "invalid mode");
35
35
  }
36
36
 
37
37
  // Set initial status
38
- node.status({ fill: "blue", shape: "dot", text: `mode: ${node.runtime.mode}, out: ${node.runtime.currentValue.toFixed(2)}` });
38
+ utils.setStatusOK(node, `mode: ${node.mode}, out: ${node.currentValue.toFixed(2)}`);
39
39
 
40
40
  let updateTimer = null;
41
41
 
42
42
  // Function to update output for rate-limit mode
43
43
  function updateRateLimitOutput() {
44
- if (!node.runtime.lastInputMsg) return;
44
+ if (!node.lastInputMsg) return;
45
45
  const now = Date.now();
46
- const elapsed = (now - node.runtime.lastUpdate) / 1000; // Seconds
47
- const maxChange = node.runtime.rate * elapsed;
48
- let newValue = node.runtime.currentValue;
49
-
50
- if (node.runtime.currentValue < node.runtime.targetValue) {
51
- newValue = Math.min(node.runtime.currentValue + maxChange, node.runtime.targetValue);
52
- } else if (node.runtime.currentValue > node.runtime.targetValue) {
53
- newValue = Math.max(node.runtime.currentValue - maxChange, node.runtime.targetValue);
46
+ const elapsed = (now - node.lastUpdate) / 1000; // Seconds
47
+ const maxChange = node.rate * elapsed;
48
+ let newValue = node.currentValue;
49
+
50
+ if (node.currentValue < node.targetValue) {
51
+ newValue = Math.min(node.currentValue + maxChange, node.targetValue);
52
+ } else if (node.currentValue > node.targetValue) {
53
+ newValue = Math.max(node.currentValue - maxChange, node.targetValue);
54
54
  }
55
55
 
56
- if (newValue !== node.runtime.currentValue) {
57
- node.runtime.currentValue = newValue;
58
- node.runtime.lastUpdate = now;
59
- const msg = RED.util.cloneMessage(node.runtime.lastInputMsg);
60
- msg.payload = node.runtime.currentValue;
61
- node.status({ fill: "blue", shape: "dot", text: `mode: rate-limit, out: ${node.runtime.currentValue.toFixed(2)}` });
56
+ if (newValue !== node.currentValue) {
57
+ node.currentValue = newValue;
58
+ node.lastUpdate = now;
59
+ const msg = RED.util.cloneMessage(node.lastInputMsg);
60
+ msg.payload = node.currentValue;
61
+ utils.setStatusOK(node, `mode: rate-limit, out: ${node.currentValue.toFixed(2)}`);
62
62
  node.send(msg);
63
63
  }
64
64
  }
@@ -66,8 +66,8 @@ module.exports = function(RED) {
66
66
  // Start update timer for rate-limit mode
67
67
  function startTimer() {
68
68
  if (updateTimer) clearInterval(updateTimer);
69
- if (node.runtime.mode === "rate-limit") {
70
- updateTimer = setInterval(updateRateLimitOutput, node.runtime.interval);
69
+ if (node.mode === "rate-limit") {
70
+ updateTimer = setInterval(updateRateLimitOutput, node.interval);
71
71
  }
72
72
  }
73
73
 
@@ -76,7 +76,7 @@ module.exports = function(RED) {
76
76
 
77
77
  // Guard against invalid message
78
78
  if (!msg) {
79
- node.status({ fill: "red", shape: "ring", text: "invalid message" });
79
+ utils.setStatusError(node, "invalid message");
80
80
  if (done) done();
81
81
  return;
82
82
  }
@@ -84,54 +84,54 @@ module.exports = function(RED) {
84
84
  // Handle context updates
85
85
  if (msg.hasOwnProperty("context")) {
86
86
  if (!msg.hasOwnProperty("payload")) {
87
- node.status({ fill: "red", shape: "ring", text: `missing payload for ${msg.context}` });
87
+ utils.setStatusError(node, `missing payload for ${msg.context}`);
88
88
  if (done) done();
89
89
  return;
90
90
  }
91
91
  switch (msg.context) {
92
92
  case "mode":
93
93
  if (!["rate-limit", "threshold", "full-value"].includes(msg.payload)) {
94
- node.status({ fill: "red", shape: "ring", text: "invalid mode" });
94
+ utils.setStatusError(node, "invalid mode");
95
95
  if (done) done();
96
96
  return;
97
97
  }
98
- node.runtime.mode = msg.payload;
98
+ node.mode = msg.payload;
99
99
  startTimer();
100
- node.status({ fill: "green", shape: "dot", text: `mode: ${node.runtime.mode}` });
100
+ utils.setStatusOK(node, `mode: ${node.mode}`);
101
101
  break;
102
102
  case "rate":
103
103
  const rate = parseFloat(msg.payload);
104
104
  if (isNaN(rate) || rate <= 0 || !isFinite(rate)) {
105
- node.status({ fill: "red", shape: "ring", text: "invalid rate" });
105
+ utils.setStatusError(node, "invalid rate");
106
106
  if (done) done();
107
107
  return;
108
108
  }
109
- node.runtime.rate = rate;
110
- node.status({ fill: "green", shape: "dot", text: `rate: ${node.runtime.rate.toFixed(2)}` });
109
+ node.rate = rate;
110
+ utils.setStatusOK(node, `rate: ${node.rate.toFixed(2)}`);
111
111
  break;
112
112
  case "interval":
113
113
  const interval = parseInt(msg.payload);
114
114
  if (isNaN(interval) || interval < 10 || !Number.isInteger(interval)) {
115
- node.status({ fill: "red", shape: "ring", text: "invalid interval" });
115
+ utils.setStatusError(node, "invalid interval");
116
116
  if (done) done();
117
117
  return;
118
118
  }
119
- node.runtime.interval = interval;
119
+ node.interval = interval;
120
120
  startTimer();
121
- node.status({ fill: "green", shape: "dot", text: `interval: ${node.runtime.interval}` });
121
+ utils.setStatusOK(node, `interval: ${node.interval}`);
122
122
  break;
123
123
  case "threshold":
124
124
  const threshold = parseFloat(msg.payload);
125
125
  if (isNaN(threshold) || threshold < 0 || !isFinite(threshold)) {
126
- node.status({ fill: "red", shape: "ring", text: "invalid threshold" });
126
+ utils.setStatusError(node, "invalid threshold");
127
127
  if (done) done();
128
128
  return;
129
129
  }
130
- node.runtime.threshold = threshold;
131
- node.status({ fill: "green", shape: "dot", text: `threshold: ${node.runtime.threshold.toFixed(2)}` });
130
+ node.threshold = threshold;
131
+ utils.setStatusOK(node, `threshold: ${node.threshold.toFixed(2)}`);
132
132
  break;
133
133
  default:
134
- node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
134
+ utils.setStatusWarn(node, "unknown context");
135
135
  if (done) done("Unknown context");
136
136
  return;
137
137
  }
@@ -141,35 +141,33 @@ module.exports = function(RED) {
141
141
 
142
142
  // Validate input
143
143
  if (typeof msg.payload !== "number" || isNaN(msg.payload) || !isFinite(msg.payload)) {
144
- node.status({ fill: "red", shape: "ring", text: "invalid input" });
144
+ utils.setStatusError(node, "invalid input");
145
145
  if (done) done();
146
146
  return;
147
147
  }
148
148
 
149
149
  const inputValue = msg.payload;
150
- node.runtime.lastInputMsg = RED.util.cloneMessage(msg);
150
+ node.lastInputMsg = RED.util.cloneMessage(msg);
151
151
 
152
- if (node.runtime.mode === "rate-limit") {
153
- node.runtime.targetValue = inputValue;
154
- node.status({ fill: "green", shape: "dot", text: `mode: rate-limit, target: ${node.runtime.targetValue.toFixed(2)}` });
152
+ if (node.mode === "rate-limit") {
153
+ node.targetValue = inputValue;
154
+ utils.setStatusOK(node, `mode: rate-limit, target: ${node.targetValue.toFixed(2)}`);
155
155
  updateRateLimitOutput();
156
156
  startTimer();
157
- } else if (node.runtime.mode === "threshold") {
158
- const diff = Math.abs(inputValue - node.runtime.currentValue);
159
- if (diff > node.runtime.threshold) {
157
+ } else if (node.mode === "threshold") {
158
+ const diff = Math.abs(inputValue - node.currentValue);
159
+ if (diff > node.threshold) {
160
160
  msg.payload = inputValue;
161
- node.runtime.currentValue = inputValue;
162
- node.status({ fill: "blue", shape: "dot", text: `mode: threshold, out: ${node.runtime.currentValue.toFixed(2)}` });
161
+ node.currentValue = inputValue;
162
+ utils.setStatusChanged(node, `mode: threshold, out: ${node.currentValue.toFixed(2)}`);
163
163
  send(msg);
164
164
  } else {
165
- node.status({ fill: "blue", shape: "ring", text: `mode: threshold, out: ${node.runtime.currentValue.toFixed(2)}`
166
- });
165
+ utils.setStatusUnchanged(node, `mode: threshold, out: ${node.currentValue.toFixed(2)}`);
167
166
  }
168
- } else if (node.runtime.mode === "full-value") {
169
- node.runtime.currentValue = inputValue;
167
+ } else if (node.mode === "full-value") {
168
+ node.currentValue = inputValue;
170
169
  msg.payload = inputValue;
171
- node.status({ fill: "blue", shape: "dot", text: `mode: full-value, out: ${node.runtime.currentValue.toFixed(2)}`
172
- });
170
+ utils.setStatusChanged(node, `mode: full-value, out: ${node.currentValue.toFixed(2)}`);
173
171
  send(msg);
174
172
  }
175
173
 
@@ -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 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-sampleSize" title="Number of samples to track (minimum 2)"><i class="fa fa-list-ol"></i> Sample Size</label>
8
12
  <input type="number" id="node-input-sampleSize" placeholder="10" min="2" step="1">
@@ -33,6 +37,7 @@
33
37
  color: "#301934",
34
38
  defaults: {
35
39
  name: { value: "" },
40
+ inputProperty: { value: "payload" },
36
41
  sampleSize: {
37
42
  value: 10,
38
43
  required: true,
@@ -80,31 +85,45 @@
80
85
  </script>
81
86
 
82
87
  <script type="text/markdown" data-help-name="rate-of-change-block">
83
- Calculates the rate of temperature change over time for HVAC applications.
88
+ Calculates the rate of value change over time.
84
89
 
85
90
  ### Inputs
91
+ : input-property (number) : Numeric value to track for rate calculation, read from the configured Input Property.
86
92
  : context (string) : Configures reset (`"reset"`), sample size (`"sampleSize"`), or units (`"units"`).
87
- : payload (number) : Temperature value for rate calculation.
88
- : timestamp (optional) : Custom timestamp for the reading.
93
+ : payload (number | boolean | string) : Value for configuration (numeric for rate, boolean for reset, string for units).
94
+ : timestamp (optional) : Custom timestamp for the reading (ms since epoch).
89
95
 
90
96
  ### Outputs
91
- : payload (number | null) : Rate of change in temperature per time unit.
97
+ : payload (number | null) : Rate of change in value per time unit.
92
98
  : samples (number) : Current number of samples in buffer.
93
- : units (string) : Rate units ("°/s", "°/min", "°/hr").
94
- : currentValue (number) : Most recent temperature value.
99
+ : units (string) : Rate units ("per second", "per minute", "per hour").
100
+ : currentValue (number) : Most recent value.
95
101
  : timeSpan (number) : Time span of sample buffer in seconds.
96
102
 
103
+ ### Properties
104
+ : name (string) : Display name in editor.
105
+ : inputProperty (string) : Message property to read values from (default: `payload`). Supports nested properties (e.g., `data.value`).
106
+ : sampleSize (number) : Number of samples to maintain (≥ 2).
107
+ : rateUnits (string) : Units for rate output (`"seconds"`, `"minutes"`, `"hours"`).
108
+
97
109
  ### Details
98
- Tracks temperature changes over a rolling window of samples. Calculates rate as (last_value - first_value) / time_difference.
110
+ Tracks value changes over a rolling window of samples. Calculates rate as `(last_value - first_value) / time_difference`.
99
111
 
100
- Useful for detecting HVAC issues like temperature droop, defrost cycles, or rapid changes.
112
+ Useful for detecting system issues like rapid changes, droop, or anomalies.
101
113
 
102
- ### Configuration
103
- - Reset via `msg.context = "reset"` with `msg.payload = true`
104
- - Change sample size via `msg.context = "sampleSize"` with numeric payload
105
- - Change units via `msg.context = "units"` with "seconds", "minutes", or "hours"
114
+ Configuration via `msg.context`:
115
+ - `"reset"`: Clears sample buffer (no output).
116
+ - `"sampleSize"`: Changes number of samples with numeric `msg.payload`.
117
+ - `"units"`: Changes rate units with `msg.payload` as `"seconds"`, `"minutes"`, or `"hours"`.
106
118
 
107
119
  ### Status
108
- - Shows current rate with units
109
- - Color indicates state change
120
+ - Green (dot): Configuration update
121
+ - Blue (dot): State changed
122
+ - Blue (ring): State unchanged
123
+ - Red (ring): Error
124
+ - Yellow (ring): Warning
125
+
126
+ ### References
127
+ - [Node-RED Documentation](https://nodered.org/docs/)
128
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
110
129
  </script>