@bldgblocks/node-red-contrib-control 0.1.33 → 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 (113) 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 +74 -67
  41. package/nodes/global-setter.html +1 -1
  42. package/nodes/global-setter.js +168 -188
  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-point-register.js +126 -0
  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-service-read.js +58 -0
  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-service-write.js +83 -0
  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 +275 -3
  108. package/package.json +14 -6
  109. package/nodes/network-read.html +0 -56
  110. package/nodes/network-read.js +0 -59
  111. package/nodes/network-register.js +0 -161
  112. package/nodes/network-write.html +0 -64
  113. package/nodes/network-write.js +0 -126
@@ -230,7 +230,19 @@
230
230
  </script>
231
231
 
232
232
  <script type="text/markdown" data-help-name="history-config">
233
- Store configuration for history series selections.
233
+ Configuration node for the History Collector. Defines storage behavior and data retention policies.
234
234
 
235
+ ### Details
235
236
 
237
+ This node does not process messages directly. It serves as a configuration reference for one or more History Collector nodes. The properties you set here determine how data is collected, stored, and aged out of the history buffer.
238
+
239
+ Configure the node in the editor panel on the right.
240
+
241
+ ### Status
242
+ - Green (dot): Configuration valid
243
+ - Red (ring): Error in configuration
244
+
245
+ ### References
246
+ - [Node-RED Documentation](https://nodered.org/docs/)
247
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
236
248
  </script>
@@ -0,0 +1,84 @@
1
+ <script type="text/html" data-template-name="history-service">
2
+ <div class="form-row">
3
+ <label for="node-input-name" title="Display name shown on the canvas"><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-historyConfig" title="History configuration to relay events from"><i class="fa fa-database"></i> History Config</label>
8
+ <input type="text" id="node-input-historyConfig">
9
+ </div>
10
+ </script>
11
+
12
+ <script type="text/javascript">
13
+ RED.nodes.registerType("history-service", {
14
+ category: "bldgblocks history",
15
+ color: "#b9f2ff",
16
+ defaults: {
17
+ name: { value: "" },
18
+ historyConfig: {
19
+ value: "",
20
+ required: true,
21
+ type: "history-config"
22
+ }
23
+ },
24
+ inputs: 0,
25
+ outputs: 1,
26
+ outputLabels: ["records"],
27
+ icon: "font-awesome/fa-database",
28
+ paletteLabel: "history-service",
29
+ label: function() {
30
+ const historyNode = RED.nodes.node(this.historyConfig);
31
+ const configName = historyNode ? historyNode.name : "unknown";
32
+ return this.name ? `${this.name} (${configName})` : `history-service (${configName})`;
33
+ }
34
+ });
35
+ </script>
36
+
37
+ <script type="text/markdown" data-help-name="history-service">
38
+ Event relay for history collectors.
39
+
40
+ ### Purpose
41
+ Receives events emitted by **history-collector** nodes and outputs them as individual records. Acts as an event-to-message bridge, allowing collectors to emit data without needing output wires.
42
+
43
+ ### Configuration
44
+ - **History Config**: The history-config node to relay events from
45
+
46
+ ### Outputs
47
+ : payload (object) : History record object containing:
48
+ - `measurement` (string): Measurement name
49
+ - `timestamp` (number): Nanosecond Unix timestamp
50
+ - `fields` (object): Values (typically `{value: number}`)
51
+ - `tags` (object): Metadata tags including `historyGroup`
52
+ - `lineProtocol` (string): Pre-formatted InfluxDB line protocol
53
+ - `seriesName` (string): Original series name from collector
54
+ - `historyConfigId` (string): Config ID
55
+ - `historyConfigName` (string): Config name
56
+
57
+ ### Details
58
+ This node listens for events emitted by all **history-collector** nodes configured with the same history-config. Each incoming record is output immediately.
59
+
60
+ **Typical wiring**:
61
+ ```
62
+ [history-collector] ──(event)──> [history-service] ──> [join] ──> [influxdb batch]
63
+ (batches)
64
+ ```
65
+
66
+ Use the **join** node to batch records if needed:
67
+ - Set join mode: "Custom"
68
+ - Build: "Array"
69
+ - Count: number of records to batch (e.g., 5000)
70
+ - Or timeout: seconds to wait before sending partial batch
71
+
72
+ This design decouples collection from storage and lets you choose batching strategy.
73
+
74
+ ### Status
75
+ - Green (dot): Ready and listening
76
+ - Blue (dot): Just relayed a record
77
+ - Red (ring): Configuration error
78
+
79
+ ### References
80
+ - [history-collector](history-collector.html) - Emits events to this service
81
+ - [history-config](history-config.html) - Configuration node
82
+ - [Node-RED join node](https://nodered.org/docs/nodes/core/flow/join) - For batching
83
+ - [Node-RED Documentation](https://nodered.org/docs/)
84
+ </script>
@@ -0,0 +1,52 @@
1
+ module.exports = function(RED) {
2
+ const utils = require("./utils")(RED);
3
+
4
+ function HistoryServiceNode(config) {
5
+ RED.nodes.createNode(this, config);
6
+ this.historyConfig = RED.nodes.getNode(config.historyConfig);
7
+ const node = this;
8
+
9
+ // Validate configuration
10
+ if (!node.historyConfig) {
11
+ utils.setStatusError(node, "missing history config");
12
+ return;
13
+ }
14
+
15
+ // Generate matching event name based on history-config ID
16
+ const eventName = `bldgblocks:history:${node.historyConfig.id}`;
17
+
18
+ // Listen for events from history-collector nodes with this config
19
+ const eventListener = (eventData) => {
20
+ // Guard against invalid event data
21
+ if (!eventData || typeof eventData !== 'object') {
22
+ utils.setStatusError(node, "invalid event data");
23
+ node.warn("Invalid event data received");
24
+ return;
25
+ }
26
+
27
+ // Create output message with the event data as payload
28
+ // Preserve topic if it exists in the event data
29
+ const msg = {
30
+ payload: eventData,
31
+ topic: eventData.topic
32
+ };
33
+
34
+ node.send(msg);
35
+
36
+ // Update status
37
+ utils.setStatusChanged(node, `relayed: ${eventData.seriesName || 'data'}`);
38
+ };
39
+
40
+ // Subscribe to events
41
+ RED.events.on(eventName, eventListener);
42
+ utils.setStatusOK(node, `listening on ${node.historyConfig.name}`);
43
+
44
+ node.on("close", function(done) {
45
+ // Unsubscribe from events on close
46
+ RED.events.off(eventName, eventListener);
47
+ done();
48
+ });
49
+ }
50
+
51
+ RED.nodes.registerType("history-service", HistoryServiceNode);
52
+ };
@@ -4,6 +4,10 @@
4
4
  <label for="node-input-name"><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-upperLimit"><i class="fa fa-arrow-up"></i> Upper Limit (turn on)</label>
9
13
  <input type="text" id="node-input-upperLimit" placeholder="50">
@@ -34,6 +38,7 @@
34
38
  color: "#301934",
35
39
  defaults: {
36
40
  name: { value: "" },
41
+ inputProperty: { value: "payload" },
37
42
  upperLimit: { value: 50, required: true },
38
43
  upperLimitType: { value: "num" },
39
44
  lowerLimit: { value: 30, required: true },
@@ -5,6 +5,7 @@ module.exports = function(RED) {
5
5
  RED.nodes.createNode(this, config);
6
6
  const node = this;
7
7
  node.name = config.name;
8
+ node.inputProperty = config.inputProperty || "payload";
8
9
  node.state = "within";
9
10
  node.isBusy = false;
10
11
  node.upperLimit = parseFloat(config.upperLimit);
@@ -16,7 +17,7 @@ module.exports = function(RED) {
16
17
  send = send || function() { node.send.apply(node, arguments); };
17
18
 
18
19
  if (!msg) {
19
- node.status({ fill: "red", shape: "ring", text: "invalid message" });
20
+ utils.setStatusError(node, "invalid message");
20
21
  if (done) done();
21
22
  return;
22
23
  }
@@ -27,7 +28,7 @@ module.exports = function(RED) {
27
28
  // Check busy lock
28
29
  if (node.isBusy) {
29
30
  // Update status to let user know they are pushing too fast
30
- node.status({ fill: "yellow", shape: "ring", text: "busy - dropped msg" });
31
+ utils.setStatusBusy(node, "busy - dropped msg");
31
32
  if (done) done();
32
33
  return;
33
34
  }
@@ -107,27 +108,27 @@ module.exports = function(RED) {
107
108
  const value = parseFloat(msg.payload);
108
109
  if (!isNaN(value) && value >= 0) {
109
110
  node.upperLimitThreshold = value;
110
- node.status({ fill: "green", shape: "dot", text: `upperLimitThreshold: ${value}` });
111
+ utils.setStatusOK(node, `upperLimitThreshold: ${value}`);
111
112
  }
112
113
  } else if (msg.context === "lowerLimitThreshold") {
113
114
  const value = parseFloat(msg.payload);
114
115
  if (!isNaN(value) && value >= 0) {
115
116
  node.lowerLimitThreshold = value;
116
- node.status({ fill: "green", shape: "dot", text: `lowerLimitThreshold: ${value}` });
117
+ utils.setStatusOK(node, `lowerLimitThreshold: ${value}`);
117
118
  }
118
119
  }
119
120
  if (done) done();
120
121
  return;
121
122
  }
122
123
 
123
- if (!msg.hasOwnProperty("payload")) {
124
- node.status({ fill: "red", shape: "ring", text: "missing payload" });
125
- if (done) done();
126
- return;
124
+ let inputValue;
125
+ try {
126
+ inputValue = parseFloat(RED.util.getMessageProperty(msg, node.inputProperty));
127
+ } catch (err) {
128
+ inputValue = NaN;
127
129
  }
128
- const inputValue = parseFloat(msg.payload);
129
130
  if (isNaN(inputValue)) {
130
- node.status({ fill: "red", shape: "ring", text: "invalid payload" });
131
+ utils.setStatusError(node, "invalid input");
131
132
  if (done) done();
132
133
  return;
133
134
  }
@@ -140,7 +141,7 @@ module.exports = function(RED) {
140
141
 
141
142
  // Add validation to ensure numbers
142
143
  if (isNaN(upperTurnOn) || isNaN(upperTurnOff) || isNaN(lowerTurnOn) || isNaN(lowerTurnOff)) {
143
- node.status({ fill: "red", shape: "ring", text: "invalid limits calculation" });
144
+ utils.setStatusError(node, "invalid limits calculation");
144
145
  if (done) done();
145
146
  return;
146
147
  }
@@ -179,11 +180,7 @@ module.exports = function(RED) {
179
180
  { payload: newState === "below" }
180
181
  ];
181
182
 
182
- node.status({
183
- fill: "blue",
184
- shape: "dot",
185
- text: `in: ${inputValue.toFixed(2)}, state: ${newState}`
186
- });
183
+ utils.setStatusChanged(node, `in: ${inputValue.toFixed(2)}, state: ${newState}`);
187
184
 
188
185
  node.state = newState;
189
186
  send(output);
@@ -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-points" title="Default points table for interpolation (array of {x, y} objects, ≥2 points)"><i class="fa fa-table"></i> Points</label>
8
12
  <textarea id="node-input-points" placeholder='[{"x": 0, "y": 0}, {"x": 100, "y": 100}]' style="height: 100px;"></textarea>
@@ -15,6 +19,7 @@
15
19
  color: "#301934",
16
20
  defaults: {
17
21
  name: { value: "" },
22
+ inputProperty: { value: "payload" },
18
23
  points: {
19
24
  value: JSON.stringify([{ x: 0, y: 0 }, { x: 100, y: 100 }], null, 2),
20
25
  required: true,
@@ -43,11 +48,24 @@
43
48
  </script>
44
49
 
45
50
  <script type="text/markdown" data-help-name="interpolate-block">
46
- Linearly interpolates a numeric input using a configurable points table.
51
+ Linearly interpolates numeric input from a configured property using a points table.
47
52
 
48
53
  ### Inputs
54
+ : input-property (number) : Numeric value to interpolate, read from the configured Input Property.
49
55
  : context (string) : Configures points table (`"points"`).
50
- : payload (number | array) : Number for interpolation, or array of `{x, y}` objects for points configuration.
56
+ : payload (array, for context) : Array of `{x, y}` objects for points configuration.
57
+
58
+ ### Outputs
59
+ : payload (number) : Interpolated output value.
60
+
61
+ ### Properties
62
+ : name (string) : Display name in editor.
63
+ : inputProperty (string) : Message property to read input from (default: `payload`). Supports nested properties (e.g., `data.value`).
64
+ : points (array) : Array of `{x, y}` objects defining the interpolation curve (minimum 2 points).
65
+
66
+ ### Details
67
+ Linearly interpolates numeric input (read from the configured **Input Property**, default: `msg.payload`) using a configurable points table.
68
+ Points can be updated dynamically via `msg.context = "points"` with an array of `{x, y}` objects in `msg.payload`.
51
69
 
52
70
  ### Outputs
53
71
  : payload (number) : Interpolated output value.
@@ -1,34 +1,31 @@
1
1
  module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
2
3
  function InterpolateBlockNode(config) {
3
4
  RED.nodes.createNode(this, config);
4
5
 
5
6
  const node = this;
6
7
 
7
8
  // Initialize runtime state
8
- node.runtime = {
9
- name: config.name,
10
- points: null,
11
- lastOutput: null
12
- };
9
+ // Initialize state
10
+ node.name = config.name;
11
+ node.inputProperty = config.inputProperty || "payload";
12
+ node.points = null;
13
+ node.lastOutput = null;
13
14
 
14
15
  // Initialize points
15
16
  try {
16
- node.runtime.points = config.points ? JSON.parse(config.points) : [{ x: 0, y: 0 }, { x: 100, y: 100 }];
17
- if (!Array.isArray(node.runtime.points) || node.runtime.points.length < 2 ||
18
- !node.runtime.points.every(p => typeof p.x === "number" && !isNaN(p.x) &&
17
+ node.points = config.points ? JSON.parse(config.points) : [{ x: 0, y: 0 }, { x: 100, y: 100 }];
18
+ if (!Array.isArray(node.points) || node.points.length < 2 ||
19
+ !node.points.every(p => typeof p.x === "number" && !isNaN(p.x) &&
19
20
  typeof p.y === "number" && !isNaN(p.y))) {
20
- node.runtime.points = [{ x: 0, y: 0 }, { x: 100, y: 100 }];
21
- node.status({ fill: "red", shape: "ring", text: "invalid points, using default" });
21
+ node.points = [{ x: 0, y: 0 }, { x: 100, y: 100 }];
22
+ utils.setStatusError(node, "invalid points, using default");
22
23
  } else {
23
- node.status({
24
- fill: "green",
25
- shape: "dot",
26
- text: `name: ${node.runtime.name}, points: ${node.runtime.points.length}`
27
- });
24
+ utils.setStatusOK(node, `name: ${node.name}, points: ${node.points.length}`);
28
25
  }
29
26
  } catch (e) {
30
- node.runtime.points = [{ x: 0, y: 0 }, { x: 100, y: 100 }];
31
- node.status({ fill: "red", shape: "ring", text: "invalid points, using default" });
27
+ node.points = [{ x: 0, y: 0 }, { x: 100, y: 100 }];
28
+ utils.setStatusError(node, "invalid points, using default");
32
29
  }
33
30
 
34
31
  node.on("input", function(msg, send, done) {
@@ -36,7 +33,7 @@ module.exports = function(RED) {
36
33
 
37
34
  // Guard against invalid msg
38
35
  if (!msg) {
39
- node.status({ fill: "red", shape: "ring", text: "invalid message" });
36
+ utils.setStatusError(node, "invalid message");
40
37
  if (done) done();
41
38
  return;
42
39
  }
@@ -44,12 +41,12 @@ module.exports = function(RED) {
44
41
  // Handle configuration messages
45
42
  if (msg.context) {
46
43
  if (typeof msg.context !== "string" || !msg.context.trim()) {
47
- node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
44
+ utils.setStatusWarn(node, "unknown context");
48
45
  if (done) done();
49
46
  return;
50
47
  }
51
48
  if (!msg.hasOwnProperty("payload")) {
52
- node.status({ fill: "red", shape: "ring", text: "missing payload" });
49
+ utils.setStatusError(node, "missing payload");
53
50
  if (done) done();
54
51
  return;
55
52
  }
@@ -59,49 +56,43 @@ module.exports = function(RED) {
59
56
  if (Array.isArray(newPoints) && newPoints.length >= 2 &&
60
57
  newPoints.every(p => typeof p.x === "number" && !isNaN(p.x) &&
61
58
  typeof p.y === "number" && !isNaN(p.y))) {
62
- node.runtime.points = newPoints;
63
- node.status({
64
- fill: "green",
65
- shape: "dot",
66
- text: `points: ${newPoints.length}`
67
- });
59
+ node.points = newPoints;
60
+ utils.setStatusOK(node, `points: ${newPoints.length}`);
68
61
  } else {
69
- node.status({ fill: "red", shape: "ring", text: "invalid points" });
62
+ utils.setStatusError(node, "invalid points");
70
63
  }
71
64
  } catch (e) {
72
- node.status({ fill: "red", shape: "ring", text: "invalid points" });
65
+ utils.setStatusError(node, "invalid points");
73
66
  }
74
67
  if (done) done();
75
68
  return;
76
69
  } else {
77
- node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
70
+ utils.setStatusWarn(node, "unknown context");
78
71
  if (done) done();
79
72
  return;
80
73
  }
81
74
  }
82
75
 
83
- // Check for missing payload
84
- if (!msg.hasOwnProperty("payload")) {
85
- node.status({ fill: "red", shape: "ring", text: "missing payload" });
86
- if (done) done();
87
- return;
76
+ // Check for missing input property
77
+ let inputValue;
78
+ try {
79
+ inputValue = parseFloat(RED.util.getMessageProperty(msg, node.inputProperty));
80
+ } catch (err) {
81
+ inputValue = NaN;
88
82
  }
89
-
90
- // Process input
91
- const inputValue = parseFloat(msg.payload);
92
83
  if (isNaN(inputValue)) {
93
- node.status({ fill: "red", shape: "ring", text: "invalid input" });
84
+ utils.setStatusError(node, "missing or invalid input property");
94
85
  if (done) done();
95
86
  return;
96
87
  }
97
88
 
98
89
  // Linear interpolation
99
90
  let outputValue = NaN;
100
- const isPositiveSlope = node.runtime.points.length >= 2 && node.runtime.points[1].x > node.runtime.points[0].x;
91
+ const isPositiveSlope = node.points.length >= 2 && node.points[1].x > node.points[0].x;
101
92
 
102
- for (let i = 0; i < node.runtime.points.length - 1; i++) {
103
- let x1 = node.runtime.points[i].x, y1 = node.runtime.points[i].y;
104
- let x2 = node.runtime.points[i + 1].x, y2 = node.runtime.points[i + 1].y;
93
+ for (let i = 0; i < node.points.length - 1; i++) {
94
+ let x1 = node.points[i].x, y1 = node.points[i].y;
95
+ let x2 = node.points[i + 1].x, y2 = node.points[i + 1].y;
105
96
  if (isPositiveSlope ? (inputValue >= x1 && inputValue <= x2) : (inputValue <= x1 && inputValue >= x2)) {
106
97
  let m = (y2 - y1) / (x2 - x1);
107
98
  let b = y1 - (m * x1);
@@ -111,21 +102,19 @@ module.exports = function(RED) {
111
102
  }
112
103
 
113
104
  if (isNaN(outputValue)) {
114
- node.status({ fill: "red", shape: "ring", text: "input out of range" });
105
+ utils.setStatusError(node, "input out of range");
115
106
  if (done) done();
116
107
  return;
117
108
  }
118
109
 
119
110
  // Check if output value has changed
120
- const isUnchanged = outputValue === node.runtime.lastOutput;
121
- node.status({
122
- fill: "blue",
123
- shape: isUnchanged ? "ring" : "dot",
124
- text: `in: ${inputValue.toFixed(2)}, out: ${outputValue.toFixed(2)}`
125
- });
111
+ const isUnchanged = outputValue === node.lastOutput;
112
+ const statusShape = isUnchanged ? "ring" : "dot";
113
+ utils.setStatusOK(node, `in: ${inputValue.toFixed(2)}, out: ${outputValue.toFixed(2)}`);
114
+ if (statusShape === "ring") utils.setStatusUnchanged(node, `in: ${inputValue.toFixed(2)}, out: ${outputValue.toFixed(2)}`);
126
115
 
127
116
  if (!isUnchanged) {
128
- node.runtime.lastOutput = outputValue;
117
+ node.lastOutput = outputValue;
129
118
  send({ payload: outputValue });
130
119
  }
131
120
 
@@ -0,0 +1,78 @@
1
+ <!-- Data Template -->
2
+ <script type="text/html" data-template-name="bldgblocks-join">
3
+ <div class="form-row">
4
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
5
+ <input type="text" id="node-input-name" placeholder="Name">
6
+ </div>
7
+
8
+ <div class="form-row">
9
+ <label for="node-input-count"><i class="fa fa-sort-numeric-asc"></i> Key Count</label>
10
+ <input type="number" id="node-input-count" placeholder="4">
11
+ </div>
12
+
13
+ <div class="form-row">
14
+ <label for="node-input-excludedKeys"><i class="fa fa-ban"></i> Exclude</label>
15
+ <input type="text" id="node-input-excludedKeys" placeholder="status, req, res">
16
+ <div style="font-size: 0.8em; color: #888; margin-left: 105px;">Comma separated list of properties to ignore</div>
17
+ </div>
18
+
19
+ <div class="form-tips">
20
+ <b>Note:</b> Works with root properties only and must all be unique. Nested properties are not supported.
21
+ Instead of incoming properties like 'outdoor.temp' (a nested object), consider using outdoor/temp or outdoorTemp as property names.
22
+ </div>
23
+ </script>
24
+
25
+ <!-- Registration & logic -->
26
+ <script type="text/javascript">
27
+ RED.nodes.registerType('bldgblocks-join', {
28
+ category: "bldgblocks control",
29
+ color: '#301934',
30
+ defaults: {
31
+ name: { value: "" },
32
+ count: { value: 4, required: true, validate: RED.validators.number() },
33
+ excludedKeys: { value: "status" } // Default to 'status' to maintain previous behavior
34
+ },
35
+ inputs: 1,
36
+ outputs: 1,
37
+ icon: "font-awesome/fa-compress",
38
+ label: function() {
39
+ return this.name || "join (" + this.count + ")";
40
+ },
41
+ paletteLabel: "join",
42
+ oneditprepare: function() {
43
+ // Optional: visual tweaks when opening the edit dialog
44
+ }
45
+ });
46
+ </script>
47
+
48
+ <!-- Help Section -->
49
+ <script type="text/markdown" data-help-name="bldgblocks-join">
50
+ Joins multiple messages into a single message by accumulating unique properties.
51
+ This differs from the standard Node-RED Join node by focusing on unique keys rather than message counts or parts
52
+ and combines to root level using property names.
53
+
54
+ ### Inputs
55
+ : all (any) : All unique properties from incoming messages.
56
+ : Key Count (number) : The number of unique keys required before the node emits the accumulated message.
57
+ : Exclude (string) : A comma-separated list of properties to ignore (e.g., `status, topic`).
58
+
59
+ ### Outputs
60
+ : msg (any) : All unique properties combined at root level into a single message.
61
+
62
+ ### Details
63
+ 1. The node stores a running list of properties from every `msg` it receives.
64
+ 2. It ignores properties starting with `_` (like `_msgid`).
65
+ 3. It ignores any properties listed in the `Exclude` configuration.
66
+ 4. When the number of stored unique keys equals or exceeds the `Key Count`, it emits the combined object as a new message.
67
+
68
+ ### Status
69
+ - Green (dot): Configuration update
70
+ - Blue (dot): State changed
71
+ - Blue (ring): State unchanged
72
+ - Red (ring): Error
73
+ - Yellow (ring): Warning
74
+
75
+ ### References
76
+ - [Node-RED Documentation](https://nodered.org/docs/)
77
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
78
+ </script>
package/nodes/join.js ADDED
@@ -0,0 +1,78 @@
1
+ module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
3
+ function BldgBlocksJoinNode(config) {
4
+ RED.nodes.createNode(this, config);
5
+ const node = this;
6
+
7
+ // Get configuration from the UI
8
+ node.targetCount = parseInt(config.count) || 4;
9
+
10
+ // Parse excluded keys string into a Set for fast lookup
11
+ // Split by comma, trim whitespace, and remove empty entries
12
+ const exclusionString = config.excludedKeys || "";
13
+ const excludedSet = new Set(
14
+ exclusionString.split(',').map(s => s.trim()).filter(s => s.length > 0)
15
+ );
16
+
17
+ // --- INPUT HANDLER ---
18
+ node.on('input', function(msg, send, done) {
19
+ send = send || function() { node.send.apply(node, arguments); };
20
+
21
+ // Get current state from context
22
+ let valueMap = node.context().get("valueMap") || {};
23
+
24
+ // Add properties from incoming message to the state
25
+ Object.keys(msg).forEach(key => {
26
+ // Logic:
27
+ // 1. Value must exist (not undefined/null)
28
+ // 2. Key must NOT start with '_' (internal Node-RED props)
29
+ // 3. Key must NOT be in the user-defined excluded list
30
+ if (
31
+ msg[key] !== undefined &&
32
+ !key.startsWith('_') &&
33
+ !excludedSet.has(key)
34
+ ) {
35
+ valueMap[key] = msg[key];
36
+ }
37
+ });
38
+
39
+ // Calculate current unique key count
40
+ const currentCount = Object.keys(valueMap).length;
41
+
42
+ // Update status
43
+ if (currentCount >= node.targetCount) {
44
+ utils.setStatusOK(node, `${currentCount}/${node.targetCount} keys`);
45
+ } else {
46
+ utils.setStatusChanged(node, `${currentCount}/${node.targetCount} keys`);
47
+ }
48
+
49
+ // Save state back to context
50
+ node.context().set("valueMap", valueMap);
51
+
52
+ // Check if we hit the target
53
+ if (currentCount >= node.targetCount) {
54
+ // Clone the map to create the output message
55
+ const outputMsg = RED.util.cloneMessage(valueMap);
56
+
57
+ // Ensure we have a msgid
58
+ if (!outputMsg._msgid) {
59
+ outputMsg._msgid = RED.util.generateId();
60
+ }
61
+
62
+ send(outputMsg);
63
+ }
64
+
65
+ if (done) {
66
+ done();
67
+ }
68
+ });
69
+
70
+ node.on('close', function(removed, done) {
71
+ if (removed) {
72
+ node.context().set("valueMap", undefined);
73
+ }
74
+ done();
75
+ });
76
+ }
77
+ RED.nodes.registerType("bldgblocks-join", BldgBlocksJoinNode);
78
+ }