@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
@@ -11,6 +11,11 @@
11
11
  <label for="node-input-seriesName" title="Specify the measurement series name"><i class="fa fa-list"></i> Series</label>
12
12
  <input type="text" id="node-input-seriesName">
13
13
  </div>
14
+ <div class="form-row">
15
+ <label for="node-input-inputProperty" title="Source for value extraction (msg property or jsonata expression)"><i class="fa fa-folder-open"></i> Input Value</label>
16
+ <input type="text" id="node-input-inputProperty" placeholder="payload">
17
+ <input type="hidden" id="node-input-inputPropertyType">
18
+ </div>
14
19
  <div class="form-row">
15
20
  <label for="node-input-tags"><i class="fa fa-tags"></i> Tags</label>
16
21
  <input type="hidden" id="node-input-tags">
@@ -49,12 +54,14 @@
49
54
  defaults: {
50
55
  historyConfig: { value: "", type: "history-config", required: true },
51
56
  seriesName: { value: "", required: true },
57
+ inputProperty: { value: "payload" },
58
+ inputPropertyType: { value: "msg" },
52
59
  tags: { value: "" },
53
60
  storageType: { value: "batchObject" },
54
61
  name: { value: "" }
55
62
  },
56
63
  inputs: 1,
57
- outputs: 1,
64
+ outputs: 0,
58
65
  icon: "file.png",
59
66
  paletteLabel: "history collector",
60
67
  label: function() {
@@ -67,6 +74,17 @@
67
74
  const tagContainer = $("#node-input-tag-container");
68
75
  const hiddenTagInput = $("#node-input-tags");
69
76
 
77
+ // Initialize typed input for input property (msg or jsonata)
78
+ try {
79
+ $("#node-input-inputProperty").typedInput({
80
+ default: "msg",
81
+ types: ["msg", "jsonata"],
82
+ typeField: "#node-input-inputPropertyType"
83
+ }).typedInput("type", node.inputPropertyType || "msg").typedInput("value", node.inputProperty);
84
+ } catch (err) {
85
+ console.error("Error initializing inputProperty typedInput:", err);
86
+ }
87
+
70
88
  // Initialize empty typedInputs first
71
89
  seriesInput.typedInput({
72
90
  types: [{ value: "series", options: [] }]
@@ -147,7 +165,10 @@
147
165
  },
148
166
  oneditsave: function() {
149
167
  const seriesInput = $("#node-input-seriesName");
168
+ const inputProperty = $("#node-input-inputProperty");
150
169
  this.seriesName = seriesInput.typedInput('value');
170
+ this.inputProperty = inputProperty.typedInput('value');
171
+ this.inputPropertyType = inputProperty.typedInput('type');
151
172
  this.storageType = $("#node-input-storageType").val();
152
173
  // tags are already in the hidden input thanks to the checkbox change handler
153
174
  }
@@ -275,6 +296,13 @@ The node supports five `storageType` options, each producing a unique output for
275
296
  - Timestamp: Generated as nanoseconds (ms * 1e6) for InfluxDB v2 precision.
276
297
  - Error Handling: Invalid configurations or payloads trigger warnings and status updates (red ring).
277
298
 
299
+ ### Status
300
+ - Green (dot): Configuration update
301
+ - Blue (dot): Data collected successfully
302
+ - Blue (ring): No state change
303
+ - Red (ring): Configuration error or invalid payload
304
+ - Yellow (ring): Warning (validation issue)
305
+
278
306
  ### References
279
307
  - [Node-RED Documentation](https://nodered.org/docs/)
280
308
  - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control)
@@ -1,10 +1,14 @@
1
1
  module.exports = function(RED) {
2
+ const utils = require("./utils")(RED);
3
+
2
4
  function HistoryCollectorNode(config) {
3
5
  RED.nodes.createNode(this, config);
4
6
  this.historyConfig = RED.nodes.getNode(config.historyConfig);
5
7
  this.seriesName = config.seriesName;
6
8
  this.storageType = config.storageType || 'memory';
7
9
  this.tags = config.tags || '';
10
+ this.inputProperty = config.inputProperty || "payload";
11
+ this.inputPropertyType = config.inputPropertyType || "msg";
8
12
  const node = this;
9
13
 
10
14
  // Parse tags into key-value object
@@ -24,33 +28,48 @@ module.exports = function(RED) {
24
28
  return tags;
25
29
  }
26
30
 
27
- node.on('input', function(msg) {
31
+ node.on('input', async function(msg, send, done) {
32
+ send = send || function() { node.send.apply(node, arguments); };
33
+
28
34
  // Guard against invalid message
29
35
  if (!msg) {
30
- node.status({ fill: "red", shape: "ring", text: "invalid message" });
36
+ utils.setStatusError(node, "invalid message");
31
37
  node.error('Invalid message received');
38
+ if (done) done();
32
39
  return;
33
40
  }
34
41
 
35
42
  // Validate configuration
36
43
  if (!node.historyConfig) {
37
- node.status({ fill: "red", shape: "ring", text: "missing history config" });
44
+ utils.setStatusError(node, "missing history config");
38
45
  node.error('Missing history configuration', msg);
46
+ if (done) done();
39
47
  return;
40
48
  }
41
49
  if (!node.seriesName) {
42
- node.status({ fill: "red", shape: "ring", text: "missing series name" });
50
+ utils.setStatusError(node, "missing series name");
43
51
  node.error('Missing series name', msg);
52
+ if (done) done();
44
53
  return;
45
54
  }
46
55
  if (!node.historyConfig.name) {
47
- node.status({ fill: "red", shape: "ring", text: "missing bucket name" });
56
+ utils.setStatusError(node, "missing bucket name");
48
57
  node.error('Missing bucket name in history configuration', msg);
58
+ if (done) done();
59
+ return;
60
+ }
61
+
62
+ // Evaluate input property (msg or jsonata)
63
+ let payloadValue;
64
+ try {
65
+ payloadValue = await utils.evaluateNodeProperty(node.inputProperty, node.inputPropertyType, node, msg);
66
+ } catch (err) {
67
+ utils.setStatusError(node, "input evaluation error");
68
+ if (done) done();
49
69
  return;
50
70
  }
51
71
 
52
72
  // Validate payload
53
- let payloadValue = msg.payload;
54
73
  let formattedValue;
55
74
  if (typeof payloadValue === 'number') {
56
75
  formattedValue = isNaN(payloadValue) ? null : payloadValue;
@@ -62,14 +81,14 @@ module.exports = function(RED) {
62
81
  formattedValue = parseInt(payloadValue); // Handle InfluxDB integer format
63
82
  }
64
83
  } else {
65
- node.status({ fill: "red", shape: "ring", text: "invalid payload" });
66
- node.warn(`Invalid payload type: ${typeof payloadValue}`);
84
+ utils.setStatusError(node, "invalid payload");
85
+ if (done) done();
67
86
  return;
68
87
  }
69
88
 
70
89
  if (formattedValue === null) {
71
- node.status({ fill: "red", shape: "ring", text: "invalid payload" });
72
- node.warn(`Invalid payload value: ${msg.payload}`);
90
+ utils.setStatusError(node, "invalid payload");
91
+ if (done) done();
73
92
  return;
74
93
  }
75
94
 
@@ -85,7 +104,7 @@ module.exports = function(RED) {
85
104
  const line = `${escapedMeasurementName}${tagsString ? ',' + tagsString : ''} value=${valueString} ${timestamp}`;
86
105
 
87
106
  // Set initial status
88
- node.status({ fill: "green", shape: "dot", text: "configuration received" });
107
+ utils.setStatusOK(node, "configuration received");
89
108
 
90
109
  // Handle storage type
91
110
  if (node.storageType === 'memory') {
@@ -101,12 +120,12 @@ module.exports = function(RED) {
101
120
  }
102
121
 
103
122
  node.context().global.set(contextKey, bucketData);
104
- node.status({ fill: "blue", shape: "dot", text: `stored: ${valueString}` });
123
+ utils.setStatusChanged(node, `stored: ${valueString}`);
105
124
  } else if (node.storageType === 'lineProtocol') {
106
125
  msg.measurement = escapedMeasurementName;
107
126
  msg.payload = line;
108
127
  node.send(msg);
109
- node.status({ fill: "blue", shape: "dot", text: `sent: ${valueString}` });
128
+ utils.setStatusChanged(node, `sent: ${valueString}`);
110
129
  } else if (node.storageType === 'object') {
111
130
  msg.measurement = escapedMeasurementName;
112
131
  msg.payload = {
@@ -116,7 +135,7 @@ module.exports = function(RED) {
116
135
  timestamp: timestamp
117
136
  };
118
137
  node.send(msg);
119
- node.status({ fill: "blue", shape: "dot", text: `sent: ${valueString}` });
138
+ utils.setStatusChanged(node, `sent: ${valueString}`);
120
139
  } else if (node.storageType === 'objectArray') {
121
140
  msg.measurement = escapedMeasurementName;
122
141
  msg.timestamp = timestamp;
@@ -127,7 +146,7 @@ module.exports = function(RED) {
127
146
  tagsObj
128
147
  ]
129
148
  node.send(msg);
130
- node.status({ fill: "blue", shape: "dot", text: `sent: ${valueString}` });
149
+ utils.setStatusChanged(node, `sent: ${valueString}`);
131
150
  } else if (node.storageType === 'batchObject') {
132
151
  msg.payload = {
133
152
  measurement: escapedMeasurementName,
@@ -138,8 +157,10 @@ module.exports = function(RED) {
138
157
  tags: tagsObj
139
158
  }
140
159
  node.send(msg);
141
- node.status({ fill: "blue", shape: "dot", text: `sent: ${valueString}` });
160
+ utils.setStatusChanged(node, `sent: ${valueString}`);
142
161
  }
162
+
163
+ if (done) done();
143
164
  });
144
165
 
145
166
  node.on("close", function(done) {
@@ -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.