@bldgblocks/node-red-contrib-control 0.1.30 → 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.
- package/LICENSE.md +202 -0
- package/nodes/average-block.js +43 -22
- package/nodes/changeover-block.js +119 -55
- package/nodes/delay-block.html +2 -2
- package/nodes/delay-block.js +47 -18
- package/nodes/enum-switch-block.js +36 -7
- package/nodes/global-getter.html +21 -56
- package/nodes/global-getter.js +64 -30
- package/nodes/global-setter.html +53 -14
- package/nodes/global-setter.js +150 -35
- package/nodes/hysteresis-block.js +67 -11
- package/nodes/max-block.js +37 -16
- package/nodes/min-block.js +36 -19
- package/nodes/minmax-block.js +43 -17
- package/nodes/on-change-block.js +34 -13
- package/nodes/pid-block.js +108 -44
- package/nodes/rate-of-change-block.js +43 -17
- package/nodes/string-builder-block.js +58 -25
- package/nodes/tstat-block.js +134 -60
- package/nodes/utils.js +17 -2
- package/package.json +2 -2
package/nodes/global-getter.html
CHANGED
|
@@ -11,6 +11,15 @@
|
|
|
11
11
|
<i class="fa fa-search"></i>
|
|
12
12
|
</button>
|
|
13
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>
|
|
14
23
|
|
|
15
24
|
<div class="form-row">
|
|
16
25
|
<label for="node-input-outputProperty"><i class="fa fa-arrow-right"></i> Output</label>
|
|
@@ -29,7 +38,8 @@
|
|
|
29
38
|
defaults: {
|
|
30
39
|
name: { value: "" },
|
|
31
40
|
targetNode: { value: "", required: true },
|
|
32
|
-
outputProperty: { value: "payload", required: true }
|
|
41
|
+
outputProperty: { value: "payload", required: true },
|
|
42
|
+
updates: { value: "always", required: true }
|
|
33
43
|
},
|
|
34
44
|
inputs: 1,
|
|
35
45
|
outputs: 1,
|
|
@@ -39,10 +49,10 @@
|
|
|
39
49
|
const target = RED.nodes.node(this.targetNode);
|
|
40
50
|
if (target) {
|
|
41
51
|
let lbl = target.path || target.name;
|
|
42
|
-
if(lbl && lbl.includes(":")) {
|
|
43
|
-
lbl = lbl.
|
|
52
|
+
if(lbl && lbl.startsWith("#") && lbl.includes(":")) {
|
|
53
|
+
lbl = lbl.substring(lbl.indexOf(":") + 1);
|
|
44
54
|
}
|
|
45
|
-
return "
|
|
55
|
+
return "get: " + lbl;
|
|
46
56
|
}
|
|
47
57
|
}
|
|
48
58
|
return this.name || "global get";
|
|
@@ -51,12 +61,13 @@
|
|
|
51
61
|
oneditprepare: function() {
|
|
52
62
|
const node = this;
|
|
53
63
|
|
|
54
|
-
// 1. Populate Dropdown
|
|
55
64
|
let candidateNodes = [];
|
|
56
65
|
RED.nodes.eachNode(function(n) {
|
|
57
66
|
if (n.type === 'global-setter') {
|
|
58
67
|
let displayPath = n.path || "No Path";
|
|
59
|
-
|
|
68
|
+
if (displayPath.startsWith("#") && displayPath.includes(":")) {
|
|
69
|
+
displayPath = displayPath.substring(displayPath.indexOf(":") + 1);
|
|
70
|
+
}
|
|
60
71
|
candidateNodes.push({
|
|
61
72
|
value: n.id,
|
|
62
73
|
label: displayPath + (n.name ? ` (${n.name})` : "")
|
|
@@ -67,62 +78,16 @@
|
|
|
67
78
|
candidateNodes.sort((a, b) => a.label.localeCompare(b.label));
|
|
68
79
|
|
|
69
80
|
$("#node-input-targetNode").typedInput({
|
|
70
|
-
types: [{
|
|
71
|
-
value: "target",
|
|
72
|
-
options: candidateNodes
|
|
73
|
-
}]
|
|
81
|
+
types: [{ value: "target", options: candidateNodes }]
|
|
74
82
|
});
|
|
75
83
|
|
|
76
|
-
|
|
77
|
-
$("#node-input-outputProperty").typedInput({
|
|
78
|
-
types: ['msg']
|
|
79
|
-
});
|
|
84
|
+
$("#node-input-outputProperty").typedInput({ types: ['msg'] });
|
|
80
85
|
|
|
81
|
-
// 3. Find Button Logic
|
|
82
86
|
$("#node-config-find-source").on("click", function() {
|
|
83
87
|
const selectedId = $("#node-input-targetNode").val();
|
|
84
|
-
if (selectedId) {
|
|
85
|
-
|
|
86
|
-
RED.view.reveal(selectedId);
|
|
87
|
-
} else {
|
|
88
|
-
RED.notify("Please select a source node first.", "warning");
|
|
89
|
-
}
|
|
88
|
+
if (selectedId) { RED.view.reveal(selectedId); }
|
|
89
|
+
else { RED.notify("Please select a source node first.", "warning"); }
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
});
|
|
93
93
|
</script>
|
|
94
|
-
|
|
95
|
-
<!-- Help Section -->
|
|
96
|
-
<script type="text/markdown" data-help-name="global-getter">
|
|
97
|
-
Manage a global variable in a repeatable way.
|
|
98
|
-
|
|
99
|
-
### Inputs
|
|
100
|
-
: source (any) : Select the source 'global-setter' node to get the variable from.
|
|
101
|
-
: output (string) : The output property to store the variable value in.
|
|
102
|
-
|
|
103
|
-
### Outputs
|
|
104
|
-
: payload (any) : The global variable value
|
|
105
|
-
: topic (string) : The variable name/path used to store the value.
|
|
106
|
-
: units (string) : The units associated with the value, if any. Also supports nested units at `msg.<inputProperty>.units`.
|
|
107
|
-
: metadata (object) : Metadata about the global variable (store, path).
|
|
108
|
-
|
|
109
|
-
### Details
|
|
110
|
-
Global variables are meant to be retrieved in other places, this necessarily means managing the same string in multiple places.
|
|
111
|
-
|
|
112
|
-
This node allows you to get a global variable while supporting rename and deletion.
|
|
113
|
-
|
|
114
|
-
It links to a `global-setter` node by Node ID, so if that node's path is changed, this node will still work.
|
|
115
|
-
|
|
116
|
-
Configurable output property allows chaining multiple getters in series if only interested in the value.
|
|
117
|
-
|
|
118
|
-
### Status
|
|
119
|
-
- Green (dot): Configuration update
|
|
120
|
-
- Blue (dot): State changed
|
|
121
|
-
- Blue (ring): State unchanged
|
|
122
|
-
- Red (ring): Error
|
|
123
|
-
- Yellow (ring): Warning
|
|
124
|
-
|
|
125
|
-
### References
|
|
126
|
-
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
127
|
-
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
128
|
-
</script>
|
package/nodes/global-getter.js
CHANGED
|
@@ -4,47 +4,81 @@ module.exports = function(RED) {
|
|
|
4
4
|
const node = this;
|
|
5
5
|
node.targetNodeId = config.targetNode;
|
|
6
6
|
node.outputProperty = config.outputProperty || "payload";
|
|
7
|
+
node.updates = config.updates;
|
|
7
8
|
|
|
8
|
-
node.
|
|
9
|
-
|
|
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 || {};
|
|
10
14
|
|
|
11
|
-
if (
|
|
12
|
-
const globalContext = node.context().global;
|
|
13
|
-
const storedObject = globalContext.get(setterNode.varName, setterNode.storeName);
|
|
15
|
+
if (storedObject !== undefined) {
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
let units = null;
|
|
18
|
-
let meta = {};
|
|
19
|
-
|
|
20
|
-
// CHECK: Is this wrapper format?
|
|
21
|
-
if (storedObject && typeof storedObject === 'object' && storedObject.hasOwnProperty('value') && storedObject.hasOwnProperty('meta')) {
|
|
22
|
-
// Yes: Unwrap it
|
|
23
|
-
val = storedObject.value;
|
|
24
|
-
units = storedObject.units;
|
|
25
|
-
meta = storedObject.meta;
|
|
26
|
-
} else {
|
|
27
|
-
// Legacy/Raw: Metadata is limited
|
|
28
|
-
meta = { path: setterNode.varName, legacy: true };
|
|
29
|
-
}
|
|
17
|
+
// CHECK: Is this our Wrapper Format? (Created by Global Setter)
|
|
18
|
+
if (storedObject && typeof storedObject === 'object' && storedObject.hasOwnProperty('value')) {
|
|
30
19
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
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);
|
|
36
30
|
|
|
37
|
-
msg.metadata = meta;
|
|
38
|
-
|
|
39
|
-
node.status({ fill: "blue", shape: "dot", text: `Get: ${val}` });
|
|
40
|
-
node.send(msg);
|
|
41
31
|
} else {
|
|
42
|
-
|
|
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 };
|
|
43
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);
|
|
44
56
|
} else {
|
|
45
57
|
node.warn("Source node not found or not configured.");
|
|
46
58
|
node.status({ fill: "red", shape: "ring", text: "Source node not found" });
|
|
47
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
|
+
}
|
|
48
82
|
});
|
|
49
83
|
}
|
|
50
84
|
RED.nodes.registerType("global-getter", GlobalGetterNode);
|
package/nodes/global-setter.html
CHANGED
|
@@ -8,12 +8,24 @@
|
|
|
8
8
|
<input type="text" id="node-input-path" style="width: 70%;" placeholder="furnace/outputs/heat">
|
|
9
9
|
</div>
|
|
10
10
|
<div class="form-row">
|
|
11
|
-
<label for="node-input-property"><i class="fa fa-ellipsis-h"></i> Input
|
|
11
|
+
<label for="node-input-property"><i class="fa fa-ellipsis-h"></i> Input</label>
|
|
12
12
|
<input type="text" id="node-input-property" style="width:70%;">
|
|
13
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
|
+
|
|
14
27
|
<div class="form-tips">
|
|
15
|
-
<b>Note:</b>
|
|
16
|
-
When this node is redeployed or deleted, it will automatically remove the variable from memory.
|
|
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.
|
|
17
29
|
</div>
|
|
18
30
|
</script>
|
|
19
31
|
|
|
@@ -24,7 +36,10 @@
|
|
|
24
36
|
defaults: {
|
|
25
37
|
name: { value: "" },
|
|
26
38
|
path: { value: "", required: true },
|
|
27
|
-
property: { value: "payload", required: true }
|
|
39
|
+
property: { value: "payload", required: true },
|
|
40
|
+
defaultValue: { value: "", required: true },
|
|
41
|
+
writePriority: { value: "16", required: true },
|
|
42
|
+
writePriorityType: { value: "dropdown" }
|
|
28
43
|
},
|
|
29
44
|
inputs: 1,
|
|
30
45
|
outputs: 1,
|
|
@@ -32,21 +47,45 @@
|
|
|
32
47
|
label: function() {
|
|
33
48
|
let lbl = this.path;
|
|
34
49
|
if (lbl && lbl.startsWith("#") && lbl.includes(":")) {
|
|
35
|
-
lbl = lbl.
|
|
50
|
+
lbl = lbl.substring(lbl.indexOf(":") + 1);
|
|
36
51
|
}
|
|
37
|
-
return this.name || "
|
|
52
|
+
return this.name || "set: " + lbl || "global set";
|
|
38
53
|
},
|
|
39
54
|
paletteLabel: "global set",
|
|
40
55
|
oneditprepare: function() {
|
|
41
|
-
//
|
|
42
|
-
$("#node-input-path").typedInput({
|
|
43
|
-
|
|
44
|
-
});
|
|
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'] });
|
|
45
62
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
types: [
|
|
49
|
-
|
|
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);
|
|
50
89
|
}
|
|
51
90
|
});
|
|
52
91
|
</script>
|
package/nodes/global-setter.js
CHANGED
|
@@ -1,55 +1,170 @@
|
|
|
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;
|
|
10
|
-
node.inputProperty = config.property;
|
|
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
|
+
}
|
|
11
78
|
|
|
12
|
-
node.on('input', function(msg) {
|
|
13
79
|
if (node.varName) {
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
80
|
+
const inputValue = RED.util.getMessageProperty(msg, node.inputProperty);
|
|
81
|
+
const globalContext = node.context().global;
|
|
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: {}
|
|
40
108
|
};
|
|
41
|
-
|
|
42
|
-
node.status({ fill: "blue", shape: "dot", text: `Set: ${storedObject.value}` });
|
|
43
|
-
globalContext.set(node.varName, storedObject, node.storeName);
|
|
44
109
|
}
|
|
45
|
-
}
|
|
46
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;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
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
|
+
}
|
|
132
|
+
|
|
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
|
+
|
|
47
163
|
node.send(msg);
|
|
164
|
+
if (done) done();
|
|
48
165
|
});
|
|
49
166
|
|
|
50
|
-
// CLEANUP
|
|
51
167
|
node.on('close', function(removed, done) {
|
|
52
|
-
// Do NOT prune if Node-RED is simply restarting or deploying.
|
|
53
168
|
if (removed && node.varName) {
|
|
54
169
|
const globalContext = node.context().global;
|
|
55
170
|
globalContext.set(node.varName, undefined, node.storeName);
|
|
@@ -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
|
-
|
|
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 {
|