@bldgblocks/node-red-contrib-control 0.1.29 → 0.1.31

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.
@@ -3,22 +3,43 @@
3
3
  <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
4
4
  <input type="text" id="node-input-name" placeholder="Name">
5
5
  </div>
6
+
6
7
  <div class="form-row">
7
8
  <label for="node-input-targetNode"><i class="fa fa-crosshairs"></i> Source</label>
8
- <input type="text" id="node-input-targetNode" style="width:70%;">
9
+ <input type="text" id="node-input-targetNode" style="width: calc(70% - 45px);">
10
+ <button id="node-config-find-source" class="editor-button" style="margin-left: 5px; width: 40px;" title="Find Source Node">
11
+ <i class="fa fa-search"></i>
12
+ </button>
9
13
  </div>
14
+
15
+ <!-- NEW: Trigger Mode Selection -->
16
+ <div class="form-row">
17
+ <label for="node-input-updates"><i class="fa fa-bolt"></i> Trigger</label>
18
+ <select id="node-input-updates" style="width: 70%;">
19
+ <option value="input">Manual (On Input Only)</option>
20
+ <option value="always">Reactive (On Input & Update)</option>
21
+ </select>
22
+ </div>
23
+
24
+ <div class="form-row">
25
+ <label for="node-input-outputProperty"><i class="fa fa-arrow-right"></i> Output</label>
26
+ <input type="text" id="node-input-outputProperty" placeholder="payload" style="width:70%;">
27
+ </div>
28
+
10
29
  <div class="form-tips">
11
- Targeting by Node ID allows the source path to change without breaking this link.
30
+ <b>Note:</b> Targeting by Node ID allows the source path to change without breaking this link.
12
31
  </div>
13
32
  </script>
14
33
 
15
34
  <script type="text/javascript">
16
35
  RED.nodes.registerType('global-getter', {
17
36
  category: 'control',
18
- color: '#301934',
37
+ color: '#3FADB5',
19
38
  defaults: {
20
39
  name: { value: "" },
21
- targetNode: { value: "", required: true }
40
+ targetNode: { value: "", required: true },
41
+ outputProperty: { value: "payload", required: true },
42
+ updates: { value: "always", required: true }
22
43
  },
23
44
  inputs: 1,
24
45
  outputs: 1,
@@ -27,12 +48,11 @@
27
48
  if (this.targetNode) {
28
49
  const target = RED.nodes.node(this.targetNode);
29
50
  if (target) {
30
- // Display the path, removing the #store: prefix for readability in the label if present
31
51
  let lbl = target.path || target.name;
32
- if(lbl && lbl.includes(":")) {
33
- lbl = lbl.split(":").pop();
52
+ if(lbl && lbl.startsWith("#") && lbl.includes(":")) {
53
+ lbl = lbl.substring(lbl.indexOf(":") + 1);
34
54
  }
35
- return "Get: " + lbl;
55
+ return "get: " + lbl;
36
56
  }
37
57
  }
38
58
  return this.name || "global get";
@@ -45,7 +65,9 @@
45
65
  RED.nodes.eachNode(function(n) {
46
66
  if (n.type === 'global-setter') {
47
67
  let displayPath = n.path || "No Path";
48
-
68
+ if (displayPath.startsWith("#") && displayPath.includes(":")) {
69
+ displayPath = displayPath.substring(displayPath.indexOf(":") + 1);
70
+ }
49
71
  candidateNodes.push({
50
72
  value: n.id,
51
73
  label: displayPath + (n.name ? ` (${n.name})` : "")
@@ -56,41 +78,16 @@
56
78
  candidateNodes.sort((a, b) => a.label.localeCompare(b.label));
57
79
 
58
80
  $("#node-input-targetNode").typedInput({
59
- types: [{
60
- value: "target",
61
- options: candidateNodes
62
- }]
81
+ types: [{ value: "target", options: candidateNodes }]
82
+ });
83
+
84
+ $("#node-input-outputProperty").typedInput({ types: ['msg'] });
85
+
86
+ $("#node-config-find-source").on("click", function() {
87
+ const selectedId = $("#node-input-targetNode").val();
88
+ if (selectedId) { RED.view.reveal(selectedId); }
89
+ else { RED.notify("Please select a source node first.", "warning"); }
63
90
  });
64
91
  }
65
92
  });
66
93
  </script>
67
-
68
- <!-- Help Section -->
69
- <script type="text/markdown" data-help-name="global-getter">
70
- Manage a global variable in a repeatable way.
71
-
72
- ### Inputs
73
-
74
- ### Outputs
75
- : payload (any) : The global variable value
76
- : topic (string) : The variable name/path used to store the value.
77
- : globalMetadata (object) : Metadata about the global variable (store, path).
78
-
79
- ### Details
80
- Global variables are meant to be retrieved in other places, this necessarily means managing the same string in multiple places.
81
-
82
- This node allows you to get a global variable while supporting rename and deletion.
83
-
84
- It links to a `global-setter` node by Node ID, so if that node's path is changed, this node will still work.
85
-
86
- ### Status
87
- - Green (dot): Configuration update
88
- - Blue (dot): State changed
89
- - Blue (ring): State unchanged
90
- - Red (ring): Error
91
- - Yellow (ring): Warning
92
-
93
- ### References
94
- - [Node-RED Documentation](https://nodered.org/docs/)
95
- - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
96
- </script>
@@ -3,40 +3,83 @@ module.exports = function(RED) {
3
3
  RED.nodes.createNode(this, config);
4
4
  const node = this;
5
5
  node.targetNodeId = config.targetNode;
6
+ node.outputProperty = config.outputProperty || "payload";
7
+ node.updates = config.updates;
6
8
 
7
- node.on('input', function(msg) {
8
- const setterNode = RED.nodes.getNode(node.targetNodeId);
9
+ const setterNode = RED.nodes.getNode(node.targetNodeId);
10
+
11
+ // --- HELPER: Process Wrapper and Send Msg ---
12
+ function sendValue(storedObject, msgToReuse) {
13
+ const msg = msgToReuse || {};
9
14
 
10
- if (setterNode && setterNode.varName) {
11
- const globalContext = node.context().global;
12
-
13
- // Retrieve the wrapper object
14
- const storedObject = globalContext.get(setterNode.varName, setterNode.storeName);
15
+ if (storedObject !== undefined) {
15
16
 
16
- if (storedObject !== undefined) {
17
- // CHECK: Is this our wrapper format?
18
- if (storedObject && typeof storedObject === 'object' && storedObject.hasOwnProperty('value') && storedObject.hasOwnProperty('meta')) {
19
- // Yes: Unwrap it
20
- msg.payload = storedObject.value;
21
- msg.globalMetadata = storedObject.meta; // Expose the ID/Metadata here
22
- } else {
23
- // No: It's legacy/raw data, just pass it through
24
- msg.payload = storedObject;
25
- }
26
-
27
- msg.topic = setterNode.varName;
17
+ // CHECK: Is this our Wrapper Format? (Created by Global Setter)
18
+ if (storedObject && typeof storedObject === 'object' && storedObject.hasOwnProperty('value')) {
28
19
 
29
- node.status({ fill: "blue", shape: "dot", text: `Get: ${msg.payload}` });
30
- node.send(msg);
20
+ // 1. Separate the Value from everything else (Rest operator)
21
+ // 'attributes' will contain: priority, units, metadata, topic, etc.
22
+ const { value, ...attributes } = storedObject;
23
+
24
+ // 2. Set the Main Output (e.g. msg.payload = 75)
25
+ RED.util.setMessageProperty(msg, node.outputProperty, value);
26
+
27
+ // 3. Merge all attributes onto the msg root
28
+ // This automatically handles priority, units, metadata, and any future fields
29
+ Object.assign(msg, attributes);
30
+
31
31
  } else {
32
- // Variable exists in config but not in memory yet
33
- // Optional: warn or just do nothing
32
+ // Handle Legacy/Raw values (not created by your Setter)
33
+ RED.util.setMessageProperty(msg, node.outputProperty, storedObject);
34
+ msg.metadata = { path: setterNode ? setterNode.varName : "unknown", legacy: true };
34
35
  }
36
+
37
+ // Visual Status
38
+ const valDisplay = RED.util.getMessageProperty(msg, node.outputProperty);
39
+ node.status({ fill: "blue", shape: "dot", text: `Get: ${valDisplay}` });
40
+
41
+ node.send(msg);
42
+
43
+ } else {
44
+ node.status({ fill: "red", shape: "ring", text: "global variable undefined" });
45
+ }
46
+ }
47
+
48
+ // --- 1. HANDLE MANUAL INPUT ---
49
+ node.on('input', function(msg, send, done) {
50
+ send = send || function() { node.send.apply(node, arguments); };
51
+
52
+ if (setterNode && setterNode.varName) {
53
+ const globalContext = node.context().global;
54
+ const storedObject = globalContext.get(setterNode.varName, setterNode.storeName);
55
+ sendValue(storedObject, msg);
35
56
  } else {
36
57
  node.warn("Source node not found or not configured.");
37
58
  node.status({ fill: "red", shape: "ring", text: "Source node not found" });
38
59
  }
60
+
61
+ if (done) done();
62
+ });
63
+
64
+ // --- 2. HANDLE REACTIVE UPDATES ---
65
+ let updateListener = null;
66
+
67
+ if (node.updates === 'always' && setterNode && setterNode.varName) {
68
+ updateListener = function(evt) {
69
+ if (evt.key === setterNode.varName && evt.store === setterNode.storeName) {
70
+ // Pass data directly from event
71
+ sendValue(evt.data, {});
72
+ }
73
+ };
74
+ RED.events.on("bldgblocks-global-update", updateListener);
75
+ }
76
+
77
+ // --- CLEANUP ---
78
+ node.on('close', function() {
79
+ if (updateListener) {
80
+ RED.events.removeListener("bldgblocks-global-update", updateListener);
81
+ }
39
82
  });
40
83
  }
41
84
  RED.nodes.registerType("global-getter", GlobalGetterNode);
42
- }
85
+ }
@@ -5,39 +5,87 @@
5
5
  </div>
6
6
  <div class="form-row">
7
7
  <label for="node-input-path"><i class="fa fa-sitemap"></i> Global Path</label>
8
- <input type="text" id="node-input-path" placeholder="furnace/outputs/heat">
8
+ <input type="text" id="node-input-path" style="width: 70%;" placeholder="furnace/outputs/heat">
9
9
  </div>
10
+ <div class="form-row">
11
+ <label for="node-input-property"><i class="fa fa-ellipsis-h"></i> Input</label>
12
+ <input type="text" id="node-input-property" style="width:70%;">
13
+ </div>
14
+ <div class="form-row">
15
+ <label for="node-input-writePriority" title="Priority is evaluated for final value when using global-setter/getter nodes."><i class="fa fa-sort-numeric-asc"></i> Priority</label>
16
+ <input type="text" id="node-input-writePriority" placeholder="single">
17
+ <input type="hidden" id="node-input-writePriorityType">
18
+ </div>
19
+
20
+ <hr>
21
+
22
+ <div class="form-row">
23
+ <label for="node-input-defaultValue" title="Static default value to ensure one exists. You may write to default through message configuration."><i class="fa fa-undo"></i> Default</label>
24
+ <input type="text" id="node-input-defaultValue" placeholder="e.g. 0 or false">
25
+ </div>
26
+
10
27
  <div class="form-tips">
11
- <b>Note:</b> Use the dropdown inside the Path input to select a specific Context Store (File, Memory, etc).
12
- When this node is redeployed or deleted, it will automatically remove (prune) the variable from that store.
28
+ <b>Note:</b> This node writes to the selected <b>Priority Level</b>. The actual Global Variable value will be the highest active priority.
13
29
  </div>
14
30
  </script>
15
31
 
16
32
  <script type="text/javascript">
17
33
  RED.nodes.registerType('global-setter', {
18
34
  category: 'control',
19
- color: '#301934',
35
+ color: '#E2D96E',
20
36
  defaults: {
21
37
  name: { value: "" },
22
- path: { value: "", required: true }
38
+ path: { value: "", required: true },
39
+ property: { value: "payload", required: true },
40
+ defaultValue: { value: "", required: true },
41
+ writePriority: { value: "16", required: true },
42
+ writePriorityType: { value: "dropdown" }
23
43
  },
24
44
  inputs: 1,
25
45
  outputs: 1,
26
46
  icon: "font-awesome/fa-align-right",
27
47
  label: function() {
28
- // Remove #store: prefix for cleaner label
29
48
  let lbl = this.path;
30
49
  if (lbl && lbl.startsWith("#") && lbl.includes(":")) {
31
- lbl = lbl.split(":")[3];
50
+ lbl = lbl.substring(lbl.indexOf(":") + 1);
32
51
  }
33
- return this.name || lbl || "global set";
52
+ return this.name || "set: " + lbl || "global set";
34
53
  },
35
54
  paletteLabel: "global set",
36
55
  oneditprepare: function() {
37
- // RESTRICT TO GLOBAL ONLY
38
- $("#node-input-path").typedInput({
39
- types: ['global']
40
- });
56
+ // These are read as strings not evaluated typed-inputs.
57
+ $("#node-input-path").typedInput({ types: ['global'] });
58
+ // getMessageProperty handles msg access on the incoming message at the specified path.
59
+ $("#node-input-property").typedInput({ types: ['msg'] });
60
+ // Default for the default value, meaning it should only be manually set. You can still pass default values through messages.
61
+ $("#node-input-defaultValue").typedInput({ types: ['str','num','bool'] });
62
+
63
+ $("#node-input-writePriority").typedInput({
64
+ default: "dropdown",
65
+ types: [{
66
+ value: "dropdown",
67
+ options: [
68
+ { value: "1", label: "1 (Life Safety)"},
69
+ { value: "2", label: "2"},
70
+ { value: "3", label: "3"},
71
+ { value: "4", label: "4"},
72
+ { value: "5", label: "5"},
73
+ { value: "6", label: "6"},
74
+ { value: "7", label: "7"},
75
+ { value: "8", label: "8 (Manual Operator)"},
76
+ { value: "9", label: "9"},
77
+ { value: "10", label: "10"},
78
+ { value: "11", label: "11"},
79
+ { value: "12", label: "12"},
80
+ { value: "13", label: "13"},
81
+ { value: "14", label: "14"},
82
+ { value: "15", label: "15"},
83
+ { value: "16", label: "16 (Schedule/Logic)"},
84
+ { value: "default", label: "default (fallback)"},
85
+ ]
86
+ }, "msg", "flow"],
87
+ typeField: "#node-input-writePriorityType"
88
+ }).typedInput("type", node.writePriorityType).typedInput("value", node.writePriority);
41
89
  }
42
90
  });
43
91
  </script>
@@ -48,6 +96,8 @@ Manage a global variable in a repeatable way.
48
96
 
49
97
  ### Inputs
50
98
  : payload (any) : Input payload is passed through unchanged.
99
+ : property (string) : The input property where the value is taken from.
100
+ : units (string) : The units associated with the value, if any. Also supports nested units at `msg.<inputProperty>.units`.
51
101
 
52
102
  ### Outputs
53
103
  : payload (any) : Original payload.
@@ -1,38 +1,171 @@
1
+
1
2
  module.exports = function(RED) {
3
+ const utils = require('./utils')(RED);
4
+
2
5
  function GlobalSetterNode(config) {
3
6
  RED.nodes.createNode(this, config);
4
7
  const node = this;
5
8
 
6
9
  const parsed = RED.util.parseContextStore(config.path);
7
-
8
10
  node.varName = parsed.key;
9
11
  node.storeName = parsed.store;
12
+ node.inputProperty = config.property;
13
+ node.defaultValue = config.defaultValue;
14
+ node.writePriority = config.writePriority;
15
+
16
+ // Cast default value logic
17
+ if(!isNaN(node.defaultValue) && node.defaultValue !== "") node.defaultValue = Number(node.defaultValue);
18
+ if(node.defaultValue === "true") node.defaultValue = true;
19
+ if(node.defaultValue === "false") node.defaultValue = false;
20
+
21
+ // --- HELPER: Calculate Winner ---
22
+ function calculateWinner(state) {
23
+ for (let i = 1; i <= 16; i++) {
24
+ if (state.priority[i] !== undefined && state.priority[i] !== null) {
25
+ return state.priority[i];
26
+ }
27
+ }
28
+ return state.defaultValue;
29
+ }
30
+
31
+ node.isBusy = false;
32
+
33
+ node.on('input', async function(msg, send, done) {
34
+ send = send || function() { node.send.apply(node, arguments); };
35
+
36
+ // Guard against invalid msg
37
+ if (!msg) {
38
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
39
+ if (done) done();
40
+ return;
41
+ }
42
+
43
+ // Evaluate dynamic properties
44
+ try {
45
+
46
+ // Check busy lock
47
+ if (node.isBusy) {
48
+ // Update status to let user know they are pushing too fast
49
+ node.status({ fill: "yellow", shape: "ring", text: "busy - dropped msg" });
50
+ if (done) done();
51
+ return;
52
+ }
53
+
54
+ // Lock node during evaluation
55
+ node.isBusy = true;
56
+
57
+ // Begin evaluations
58
+ const evaluations = [];
59
+
60
+ evaluations.push(
61
+ utils.requiresEvaluation(config.writePriorityType)
62
+ ? utils.evaluateNodeProperty( config.writePriority, config.writePriorityType, node, msg )
63
+ : Promise.resolve(config.writePriority),
64
+ );
65
+
66
+ const results = await Promise.all(evaluations);
67
+
68
+ // Update runtime with evaluated values
69
+ node.writePriority = results[0];
70
+ } catch (err) {
71
+ node.error(`Error evaluating properties: ${err.message}`);
72
+ if (done) done();
73
+ return;
74
+ } finally {
75
+ // Release, all synchronous from here on
76
+ node.isBusy = false;
77
+ }
10
78
 
11
- node.on('input', function(msg) {
12
79
  if (node.varName) {
80
+ const inputValue = RED.util.getMessageProperty(msg, node.inputProperty);
13
81
  const globalContext = node.context().global;
14
-
15
- // Create a clean wrapper object to store in global context
16
- const storedObject = {
17
- value: msg.payload,
18
- meta: {
19
- sourceId: node.id,
20
- sourceName: node.name || config.path,
21
- topic: msg.topic,
22
- ts: Date.now()
82
+
83
+ // Get existing state or initialize new
84
+ let state = globalContext.get(node.varName, node.storeName);
85
+ if (!state || typeof state !== 'object' || !state.priority) {
86
+ state = {
87
+ value: null,
88
+ defaultValue: node.defaultValue,
89
+ priority: {
90
+ 1: null,
91
+ 2: null,
92
+ 3: null,
93
+ 4: null,
94
+ 5: null,
95
+ 6: null,
96
+ 7: null,
97
+ 8: null,
98
+ 9: null,
99
+ 10: null,
100
+ 11: null,
101
+ 12: null,
102
+ 13: null,
103
+ 14: null,
104
+ 15: null,
105
+ 16: null,
106
+ },
107
+ metadata: {}
108
+ };
109
+ }
110
+
111
+ // Update Default, can not be set null
112
+ if (node.writePriority === 'default') {
113
+ state.defaultValue = inputValue !== null ? inputValue : node.defaultValue;
114
+ } else {
115
+ const priority = parseInt(node.writePriority, 10);
116
+ if (isNaN(priority) || priority < 1 || priority > 16) {
117
+ node.status({ fill: "red", shape: "ring", text: `Invalid priority: ${node.writePriority}` });
118
+ if (done) done();
119
+ return;
23
120
  }
24
- };
121
+ }
25
122
 
26
- globalContext.set(node.varName, storedObject, node.storeName);
27
- }
123
+ // Ensure defaultValue always has a value
124
+ if (state.defaultValue === null || state.defaultValue === undefined) {
125
+ state.defaultValue = node.defaultValue;
126
+ }
127
+
128
+ // Update Specific Priority Slot
129
+ if (inputValue !== undefined) {
130
+ state.priority[node.writePriority] = inputValue;
131
+ }
28
132
 
29
- node.status({ fill: "blue", shape: "dot", text: `Set: ${msg.payload}` });
133
+ // Calculate Winner
134
+ state.value = calculateWinner(state);
135
+
136
+ // Update Metadata
137
+ state.metadata.sourceId = node.id;
138
+ state.metadata.lastSet = new Date().toISOString();
139
+ state.metadata.sourceName = node.name || config.path;
140
+ state.metadata.sourcePath = node.varName;
141
+ state.metadata.store = node.storeName;
142
+
143
+ // Units logic
144
+ let capturedUnits = msg.units;
145
+ if (!capturedUnits && typeof inputValue === 'object' && inputValue !== null && inputValue.units) {
146
+ capturedUnits = inputValue.units;
147
+ }
148
+ if(capturedUnits) state.units = capturedUnits;
149
+
150
+ // Save & Emit
151
+ globalContext.set(node.varName, state, node.storeName);
152
+
153
+ node.status({ fill: "blue", shape: "dot", text: `P${node.writePriority}:${inputValue} > Val:${state.value}` });
154
+
155
+ // Fire Event
156
+ RED.events.emit("bldgblocks-global-update", {
157
+ key: node.varName,
158
+ store: node.storeName,
159
+ data: state
160
+ });
161
+ }
162
+
30
163
  node.send(msg);
164
+ if (done) done();
31
165
  });
32
166
 
33
- // CLEANUP
34
167
  node.on('close', function(removed, done) {
35
- if (node.varName) {
168
+ if (removed && node.varName) {
36
169
  const globalContext = node.context().global;
37
170
  globalContext.set(node.varName, undefined, node.storeName);
38
171
  }
@@ -6,18 +6,13 @@ module.exports = function(RED) {
6
6
  const node = this;
7
7
  node.name = config.name;
8
8
  node.state = "within";
9
+ node.isBusy = false;
10
+ node.upperLimit = parseFloat(config.upperLimit);
11
+ node.lowerLimit = parseFloat(config.lowerLimit);
12
+ node.upperLimitThreshold = parseFloat(config.upperLimitThreshold);
13
+ node.lowerLimitThreshold = parseFloat(config.lowerLimitThreshold);
9
14
 
10
- // Evaluate typed-input properties
11
- try {
12
- node.upperLimit = parseFloat(RED.util.evaluateNodeProperty( config.upperLimit, config.upperLimitType, node ));
13
- node.lowerLimit = parseFloat(RED.util.evaluateNodeProperty( config.lowerLimit, config.lowerLimitType, node ));
14
- node.upperLimitThreshold = parseFloat(RED.util.evaluateNodeProperty( config.upperLimitThreshold, config.upperLimitThresholdType, node ));
15
- node.lowerLimitThreshold = parseFloat(RED.util.evaluateNodeProperty( config.lowerLimitThreshold, config.lowerLimitThresholdType, node ));
16
- } catch (err) {
17
- node.error(`Error evaluating properties: ${err.message}`);
18
- }
19
-
20
- node.on("input", function(msg, send, done) {
15
+ node.on("input", async function(msg, send, done) {
21
16
  send = send || function() { node.send.apply(node, arguments); };
22
17
 
23
18
  if (!msg) {
@@ -25,6 +20,67 @@ module.exports = function(RED) {
25
20
  if (done) done();
26
21
  return;
27
22
  }
23
+
24
+ // Evaluate dynamic properties
25
+ try {
26
+
27
+ // Check busy lock
28
+ if (node.isBusy) {
29
+ // Update status to let user know they are pushing too fast
30
+ node.status({ fill: "yellow", shape: "ring", text: "busy - dropped msg" });
31
+ if (done) done();
32
+ return;
33
+ }
34
+
35
+ // Lock node during evaluation
36
+ node.isBusy = true;
37
+
38
+ // Begin evaluations
39
+ const evaluations = [];
40
+
41
+ evaluations.push(
42
+ utils.requiresEvaluation(config.upperLimitType)
43
+ ? utils.evaluateNodeProperty(config.upperLimit, config.upperLimitType, node, msg)
44
+ .then(val => parseFloat(val))
45
+ : Promise.resolve(node.upperLimit),
46
+ );
47
+
48
+ evaluations.push(
49
+ utils.requiresEvaluation(config.lowerLimitType)
50
+ ? utils.evaluateNodeProperty(config.lowerLimit, config.lowerLimitType, node, msg)
51
+ .then(val => parseFloat(val))
52
+ : Promise.resolve(node.lowerLimit),
53
+ );
54
+
55
+ evaluations.push(
56
+ utils.requiresEvaluation(config.upperLimitThresholdType)
57
+ ? utils.evaluateNodeProperty(config.upperLimitThreshold, config.upperLimitThresholdType, node, msg)
58
+ .then(val => parseFloat(val))
59
+ : Promise.resolve(node.upperLimitThreshold),
60
+ );
61
+
62
+ evaluations.push(
63
+ utils.requiresEvaluation(config.lowerLimitThresholdType)
64
+ ? utils.evaluateNodeProperty(config.lowerLimitThreshold, config.lowerLimitThresholdType, node, msg)
65
+ .then(val => parseFloat(val))
66
+ : Promise.resolve(node.lowerLimitThreshold),
67
+ );
68
+
69
+ const results = await Promise.all(evaluations);
70
+
71
+ // Update runtime with evaluated values
72
+ if (!isNaN(results[0])) node.upperLimit = results[0];
73
+ if (!isNaN(results[1])) node.lowerLimit = results[1];
74
+ if (!isNaN(results[2])) node.upperLimitThreshold = results[2];
75
+ if (!isNaN(results[3])) node.lowerLimitThreshold = results[3];
76
+ } catch (err) {
77
+ node.error(`Error evaluating properties: ${err.message}`);
78
+ if (done) done();
79
+ return;
80
+ } finally {
81
+ // Release, all synchronous from here on
82
+ node.isBusy = false;
83
+ }
28
84
 
29
85
  // Update typed-input properties if needed
30
86
  try {