@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,58 +1,50 @@
1
1
 
2
2
  module.exports = function(RED) {
3
+ const utils = require('./utils')(RED);
3
4
  function UnitsBlockNode(config) {
4
5
  RED.nodes.createNode(this, config);
5
6
  const node = this;
6
7
 
7
8
  // Initialize runtime state
8
- node.runtime = {
9
- name: config.name,
10
- unit: config.unit
11
- };
9
+ // Initialize state
10
+ node.name = config.name;
11
+ node.inputProperty = config.inputProperty || "payload";
12
+ node.unit = config.unit;
12
13
 
13
14
  node.on("input", function(msg, send, done) {
14
15
  send = send || function() { node.send.apply(node, arguments); };
15
16
 
16
17
  // Validate input
17
18
  if (!msg || typeof msg !== "object" || !msg.hasOwnProperty("payload")) {
18
- node.status({ fill: "red", shape: "ring", text: "invalid message" });
19
+ utils.setStatusError(node, "invalid message");
19
20
 
20
21
  if (done) done();
21
22
  return;
22
23
  }
23
24
 
24
25
  try {
25
- // Handle configuration messages
26
- if (msg.hasOwnProperty("context")) {
27
- // Configuration handling
28
- if (msg.context === "unit") {
29
- if (typeof msg.payload === "string") {
30
- node.runtime.unit = msg.payload;
31
- node.status({ fill: "green", shape: "dot", text: `unit: ${node.runtime.unit}` });
32
- } else {
33
- node.status({ fill: "red", shape: "ring", text: "invalid unit" });
34
- }
35
- if (done) done();
36
- return;
37
- }
38
-
39
- // Handle unknown context
40
- if (msg.context && msg.context !== "unit") {
41
- node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
42
- // Continue processing as passthrough
43
- }
44
- }
45
-
46
26
  // Process input
47
- const payloadPreview = msg.payload !== null ? (typeof msg.payload === "number" ? msg.payload.toFixed(2) : JSON.stringify(msg.payload).slice(0, 20)) : "none";
27
+ let input;
28
+ try {
29
+ input = RED.util.getMessageProperty(msg, node.inputProperty);
30
+ } catch (err) {
31
+ input = undefined;
32
+ }
33
+ if (input === undefined) {
34
+ utils.setStatusError(node, "missing or invalid input property");
35
+ if (done) done();
36
+ return;
37
+ }
38
+
39
+ const payloadPreview = input !== null ? (typeof input === "number" ? input.toFixed(2) : JSON.stringify(input).slice(0, 20)) : "none";
48
40
 
49
- node.status({ fill: "blue", shape: "dot", text: `in: ${payloadPreview} unit: ${node.runtime.unit !== "" ? node.runtime.unit : "none"}` });
41
+ utils.setStatusOK(node, `in: ${payloadPreview} unit: ${node.unit !== "" ? node.unit : "none"}`);
50
42
 
51
- msg.units = node.runtime.unit;
43
+ msg.units = node.unit;
52
44
  send(msg);
53
45
  if (done) done();
54
46
  } catch (error) {
55
- node.status({ fill: "red", shape: "ring", text: "processing error" });
47
+ utils.setStatusError(node, "processing error");
56
48
 
57
49
  if (done) done(error);
58
50
  return;
package/nodes/utils.js CHANGED
@@ -1,5 +1,30 @@
1
1
  module.exports = function(RED) {
2
- function requiresEvaluation(type) { return type === "flow" || type === "global" || type === "msg"; }
2
+ // Shared state across all nodes using this utils module
3
+ // Registries set attached to RED object to ensure a true singleton
4
+ // across multiple invocations of this module function.
5
+ if (!RED._bldgblocks_registries) {
6
+ RED._bldgblocks_registries = new Set();
7
+ }
8
+ const registries = RED._bldgblocks_registries;
9
+
10
+ function registerRegistryNode(node) {
11
+ registries.add(node);
12
+ node.on("close", () => registries.delete(node));
13
+ }
14
+
15
+ function lookupPointMetadata(pointId) {
16
+ const pid = parseInt(pointId);
17
+ if (isNaN(pid)) return null;
18
+
19
+ for (const reg of registries) {
20
+ if (reg.points && reg.points.has(pid)) {
21
+ return reg.points.get(pid);
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+
27
+ function requiresEvaluation(type) { return type === "flow" || type === "global" || type === "msg" || type === "jsonata"; }
3
28
 
4
29
  // Safe evaluation helper (promisified)
5
30
  function evaluateNodeProperty(value, type, node, msg) {
@@ -78,7 +103,172 @@ module.exports = function(RED) {
78
103
  }
79
104
  return { value, priority };
80
105
  }
81
-
106
+
107
+ // ============================================================================
108
+ // Status Helper Functions
109
+ // ============================================================================
110
+ // Simplified status reporting with consistent fill/shape/text protocol
111
+ // Usage: utils.setStatusOK(node, "sum: 42.5");
112
+
113
+ function setStatusOK(node, text) {
114
+ node.status({ fill: "green", shape: "dot", text });
115
+ }
116
+
117
+ function setStatusChanged(node, text) {
118
+ node.status({ fill: "blue", shape: "dot", text });
119
+ }
120
+
121
+ function setStatusUnchanged(node, text) {
122
+ node.status({ fill: "blue", shape: "ring", text });
123
+ }
124
+
125
+ function setStatusError(node, text) {
126
+ node.status({ fill: "red", shape: "ring", text });
127
+ }
128
+
129
+ function setStatusWarn(node, text) {
130
+ node.status({ fill: "yellow", shape: "ring", text });
131
+ }
132
+
133
+ function setStatusBusy(node, text = "busy - dropped msg") {
134
+ node.status({ fill: "yellow", shape: "ring", text });
135
+ }
136
+
137
+ // ============================================================================
138
+ // Validation Helper Functions
139
+ // ============================================================================
140
+ // Common validation patterns used across control blocks
141
+
142
+ /**
143
+ * Validate that msg exists and contains required properties
144
+ * @param {Object} msg - The message object to validate
145
+ * @param {string[]} requiredProps - Array of required property names (e.g., ["payload"])
146
+ * @returns {boolean} true if valid, false otherwise
147
+ */
148
+ function validateMessage(msg, requiredProps = []) {
149
+ if (!msg || typeof msg !== 'object') {
150
+ return false;
151
+ }
152
+ return requiredProps.every(prop => msg.hasOwnProperty(prop));
153
+ }
154
+
155
+ /**
156
+ * Validate and parse numeric payload
157
+ * @param {*} payload - The payload to validate
158
+ * @param {Object} options - Validation options {min, max, allowZero}
159
+ * @returns {Object} {valid: boolean, value: number|null, error: string|null}
160
+ */
161
+ function validateNumericPayload(payload, options = {}) {
162
+ const { min = -Infinity, max = Infinity, allowZero = true } = options;
163
+
164
+ if (payload === null || payload === undefined) {
165
+ return { valid: false, value: null, error: "missing payload" };
166
+ }
167
+
168
+ const value = parseFloat(payload);
169
+
170
+ if (isNaN(value)) {
171
+ return { valid: false, value: null, error: "invalid numeric payload" };
172
+ }
173
+
174
+ if (!allowZero && value === 0) {
175
+ return { valid: false, value: null, error: "payload cannot be zero" };
176
+ }
177
+
178
+ if (value < min || value > max) {
179
+ return { valid: false, value: null, error: `payload out of range [${min}, ${max}]` };
180
+ }
181
+
182
+ return { valid: true, value, error: null };
183
+ }
184
+
185
+ /**
186
+ * Validate slot index for multi-slot blocks
187
+ * @param {string|number} slotId - The slot identifier (e.g., "in1", "in2")
188
+ * @param {number} maxSlots - Maximum number of slots available
189
+ * @returns {Object} {valid: boolean, index: number|null, error: string|null}
190
+ */
191
+ function validateSlotIndex(slotId, maxSlots) {
192
+ if (!slotId) {
193
+ return { valid: false, index: null, error: "missing slot identifier" };
194
+ }
195
+
196
+ // Handle numeric string (e.g., "1") or prefixed (e.g., "in1")
197
+ let indexStr = slotId;
198
+ if (typeof slotId === 'string' && slotId.match(/^[a-z]+(\d+)$/i)) {
199
+ indexStr = slotId.match(/\d+/)[0];
200
+ }
201
+
202
+ const index = parseInt(indexStr, 10);
203
+
204
+ if (isNaN(index)) {
205
+ return { valid: false, index: null, error: `invalid slot index: ${slotId}` };
206
+ }
207
+
208
+ if (index < 1 || index > maxSlots) {
209
+ return { valid: false, index: null, error: `slot out of range [1, ${maxSlots}]` };
210
+ }
211
+
212
+ return { valid: true, index, error: null };
213
+ }
214
+
215
+ /**
216
+ * Validate boolean payload
217
+ * @param {*} payload - The payload to validate
218
+ * @returns {Object} {valid: boolean, value: boolean|null, error: string|null}
219
+ */
220
+ function validateBoolean(payload) {
221
+ if (payload === null || payload === undefined) {
222
+ return { valid: false, value: null, error: "missing boolean payload" };
223
+ }
224
+
225
+ if (typeof payload === 'boolean') {
226
+ return { valid: true, value: payload, error: null };
227
+ }
228
+
229
+ if (typeof payload === 'string') {
230
+ const lower = payload.toLowerCase();
231
+ if (lower === 'true' || lower === '1' || lower === 'on') {
232
+ return { valid: true, value: true, error: null };
233
+ }
234
+ if (lower === 'false' || lower === '0' || lower === 'off') {
235
+ return { valid: true, value: false, error: null };
236
+ }
237
+ }
238
+
239
+ if (typeof payload === 'number') {
240
+ return { valid: true, value: payload !== 0, error: null };
241
+ }
242
+
243
+ return { valid: false, value: null, error: "invalid boolean payload" };
244
+ }
245
+
246
+ /**
247
+ * Validate integer within range
248
+ * @param {*} payload - The payload to validate
249
+ * @param {Object} options - Validation options {min, max}
250
+ * @returns {Object} {valid: boolean, value: number|null, error: string|null}
251
+ */
252
+ function validateIntRange(payload, options = {}) {
253
+ const { min = -Infinity, max = Infinity } = options;
254
+
255
+ if (payload === null || payload === undefined) {
256
+ return { valid: false, value: null, error: "missing payload" };
257
+ }
258
+
259
+ const value = parseInt(payload, 10);
260
+
261
+ if (isNaN(value)) {
262
+ return { valid: false, value: null, error: "invalid integer payload" };
263
+ }
264
+
265
+ if (value < min || value > max) {
266
+ return { valid: false, value: null, error: `value out of range [${min}, ${max}]` };
267
+ }
268
+
269
+ return { valid: true, value, error: null };
270
+ }
271
+
82
272
  // Usage:
83
273
  // const utils = require('./utils')(RED);
84
274
 
@@ -89,6 +279,19 @@ module.exports = function(RED) {
89
279
  sendSuccess,
90
280
  getGlobalState,
91
281
  setGlobalState,
92
- getHighestPriority
282
+ getHighestPriority,
283
+ setStatusOK,
284
+ setStatusChanged,
285
+ setStatusUnchanged,
286
+ setStatusError,
287
+ setStatusWarn,
288
+ setStatusBusy,
289
+ validateMessage,
290
+ validateNumericPayload,
291
+ validateSlotIndex,
292
+ validateBoolean,
293
+ validateIntRange,
294
+ registerRegistryNode,
295
+ lookupPointMetadata
93
296
  };
94
297
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bldgblocks/node-red-contrib-control",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Sedona-inspired control nodes for Node-RED",
5
5
  "keywords": [ "node-red", "sedona", "control", "hvac" ],
6
6
  "files": ["nodes/*.js", "nodes/*.html"],
@@ -33,6 +33,7 @@
33
33
  "frequency-block": "nodes/frequency-block.js",
34
34
  "hysteresis-block": "nodes/hysteresis-block.js",
35
35
  "interpolate-block": "nodes/interpolate-block.js",
36
+ "join": "nodes/join.js",
36
37
  "latch-block": "nodes/latch-block.js",
37
38
  "load-sequence-block": "nodes/load-sequence-block.js",
38
39
  "max-block": "nodes/max-block.js",
@@ -66,11 +67,18 @@
66
67
  "global-setter": "nodes/global-setter.js",
67
68
  "history-collector": "nodes/history-collector.js",
68
69
  "history-config": "nodes/history-config.js",
69
- "network-read": "nodes/network-read.js",
70
- "network-register": "nodes/network-register.js",
71
- "network-write": "nodes/network-write.js",
72
- "network-point-registry": "nodes/network-point-registry.js"
73
-
70
+ "history-service": "nodes/history-service.js",
71
+ "history-buffer": "nodes/history-buffer.js",
72
+ "network-service-read": "nodes/network-service-read.js",
73
+ "network-service-write": "nodes/network-service-write.js",
74
+ "network-service-registry": "nodes/network-service-registry.js",
75
+ "network-service-bridge": "nodes/network-service-bridge.js",
76
+ "network-point-register": "nodes/network-point-register.js",
77
+ "network-point-read": "nodes/network-point-read.js",
78
+ "network-point-write": "nodes/network-point-write.js",
79
+ "alarm-collector": "nodes/alarm-collector.js",
80
+ "alarm-config": "nodes/alarm-config.js",
81
+ "alarm-service": "nodes/alarm-service.js"
74
82
  }
75
83
  },
76
84
  "author": "buildingblocks",
@@ -1,56 +0,0 @@
1
- <script type="text/html" data-template-name="network-read">
2
- <div class="form-row">
3
- <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
4
- <input type="text" id="node-input-name" placeholder="Name">
5
- </div>
6
-
7
- <div class="form-row">
8
- <label for="node-input-registry"><i class="fa fa-book"></i> Registry</label>
9
- <input type="text" id="node-input-registry">
10
- </div>
11
-
12
- <div class="form-tips">
13
- <b>Input Payload Format:</b><br>
14
- <pre>
15
- {
16
- "action": "read",
17
- "pointId": 101
18
- }
19
- </pre>
20
- </div>
21
- </script>
22
-
23
- <script type="text/javascript">
24
- RED.nodes.registerType('network-read', {
25
- category: 'bldgblocks network',
26
- color: '#3090C7',
27
- defaults: {
28
- name: { value: "" },
29
- registry: { value: "", type: "network-point-registry", required: true }
30
- },
31
- inputs: 1,
32
- outputs: 1,
33
- icon: "font-awesome/fa-database",
34
- label: function() {
35
- return "network read";
36
- },
37
- paletteLabel: "network read",
38
- oneditprepare: function() {
39
- const nodeId = this.id;
40
- }
41
- });
42
- </script>
43
-
44
- <script type="text/markdown" data-help-name="network-read">
45
- Reads a network point by pointId.
46
-
47
- ### Input
48
- : action (string) : Not used by the node, used for routing to the correct node over the network when received.
49
- : pointId (number) : The integer ID of the point.
50
-
51
- ### Output
52
- : payload (object) : Global data object
53
-
54
- ### Details
55
-
56
- </script>
@@ -1,65 +0,0 @@
1
- <script type="text/html" data-template-name="network-write">
2
- <div class="form-row">
3
- <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
4
- <input type="text" id="node-input-name" placeholder="Name">
5
- </div>
6
- <div class="form-row">
7
- <label for="node-input-registry"><i class="fa fa-book"></i> Registry</label>
8
- <input type="text" id="node-input-registry">
9
- </div>
10
-
11
- <div class="form-tips">
12
- <b>Input Payload Format:</b><br>
13
- <pre>
14
- {
15
- "action": "write",
16
- "pointId": 101,
17
- "priority": 8,
18
- "value": 75.5 // or null || "null" to release
19
- }
20
- </pre>
21
- </div>
22
- </script>
23
-
24
- <script type="text/javascript">
25
- RED.nodes.registerType('network-write', {
26
- category: 'bldgblocks network',
27
- color: '#3090C7',
28
- defaults: {
29
- name: { value: "" },
30
- registry: { value: "", type: "network-point-registry", required: true }
31
- },
32
- inputs: 1,
33
- outputs: 1,
34
- icon: "font-awesome/fa-list-ol",
35
- label: function() {
36
- return this.name || "network write";
37
- },
38
- paletteLabel: "network write",
39
- oneditprepare: function() {
40
-
41
- }
42
- });
43
- </script>
44
-
45
- <script type="text/markdown" data-help-name="network-write">
46
- Writes network commands to Global Variables using the Priority Array logic.
47
-
48
- ### Input
49
- : payload (object) : A command object containing
50
- * `action` (string): Not used by the node, used for routing to the correct node over the network when received.
51
- * `pointId` (number): The integer ID of the point.
52
- * `priority` (number): The priority level (1-16) to write to.
53
- * `value` (any): The value to set. Send `null` to relinquish (clear) this priority level.
54
-
55
- ### Output
56
- : payload (object) : Confirmation object containing status, pointId, and the new calculated "Winner" value.
57
-
58
- ### Details
59
- This node acts as the inbound gateway.
60
- 1. It looks up the `pointId` in the selected **Registry** to find the corresponding Global Variable path.
61
- 2. It fetches the current State Object.
62
- 3. It updates the specific slot in the `priority` array based on the command.
63
- 4. It recalculates the "Present Value" (highest priority active).
64
- 5. It saves the Global Variable and emits an update event, triggering any reactive Getters immediately.
65
- </script>