@bldgblocks/node-red-contrib-control 0.1.29 → 0.1.30
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/nodes/global-getter.html +39 -7
- package/nodes/global-getter.js +21 -12
- package/nodes/global-setter.html +18 -7
- package/nodes/global-setter.js +33 -15
- package/nodes/units-block.js +4 -3
- package/package.json +1 -1
package/nodes/global-getter.html
CHANGED
|
@@ -3,22 +3,33 @@
|
|
|
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
|
+
<div class="form-row">
|
|
16
|
+
<label for="node-input-outputProperty"><i class="fa fa-arrow-right"></i> Output</label>
|
|
17
|
+
<input type="text" id="node-input-outputProperty" placeholder="payload" style="width:70%;">
|
|
18
|
+
</div>
|
|
19
|
+
|
|
10
20
|
<div class="form-tips">
|
|
11
|
-
Targeting by Node ID allows the source path to change without breaking this link.
|
|
21
|
+
<b>Note:</b> Targeting by Node ID allows the source path to change without breaking this link.
|
|
12
22
|
</div>
|
|
13
23
|
</script>
|
|
14
24
|
|
|
15
25
|
<script type="text/javascript">
|
|
16
26
|
RED.nodes.registerType('global-getter', {
|
|
17
27
|
category: 'control',
|
|
18
|
-
color: '#
|
|
28
|
+
color: '#3FADB5',
|
|
19
29
|
defaults: {
|
|
20
30
|
name: { value: "" },
|
|
21
|
-
targetNode: { value: "", required: true }
|
|
31
|
+
targetNode: { value: "", required: true },
|
|
32
|
+
outputProperty: { value: "payload", required: true }
|
|
22
33
|
},
|
|
23
34
|
inputs: 1,
|
|
24
35
|
outputs: 1,
|
|
@@ -27,10 +38,9 @@
|
|
|
27
38
|
if (this.targetNode) {
|
|
28
39
|
const target = RED.nodes.node(this.targetNode);
|
|
29
40
|
if (target) {
|
|
30
|
-
// Display the path, removing the #store: prefix for readability in the label if present
|
|
31
41
|
let lbl = target.path || target.name;
|
|
32
42
|
if(lbl && lbl.includes(":")) {
|
|
33
|
-
lbl = lbl.split(":")
|
|
43
|
+
lbl = lbl.split(":")[3];
|
|
34
44
|
}
|
|
35
45
|
return "Get: " + lbl;
|
|
36
46
|
}
|
|
@@ -41,6 +51,7 @@
|
|
|
41
51
|
oneditprepare: function() {
|
|
42
52
|
const node = this;
|
|
43
53
|
|
|
54
|
+
// 1. Populate Dropdown
|
|
44
55
|
let candidateNodes = [];
|
|
45
56
|
RED.nodes.eachNode(function(n) {
|
|
46
57
|
if (n.type === 'global-setter') {
|
|
@@ -61,6 +72,22 @@
|
|
|
61
72
|
options: candidateNodes
|
|
62
73
|
}]
|
|
63
74
|
});
|
|
75
|
+
|
|
76
|
+
// 2. Output Property Selector
|
|
77
|
+
$("#node-input-outputProperty").typedInput({
|
|
78
|
+
types: ['msg']
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// 3. Find Button Logic
|
|
82
|
+
$("#node-config-find-source").on("click", function() {
|
|
83
|
+
const selectedId = $("#node-input-targetNode").val();
|
|
84
|
+
if (selectedId) {
|
|
85
|
+
// Node-RED API to jump to a node
|
|
86
|
+
RED.view.reveal(selectedId);
|
|
87
|
+
} else {
|
|
88
|
+
RED.notify("Please select a source node first.", "warning");
|
|
89
|
+
}
|
|
90
|
+
});
|
|
64
91
|
}
|
|
65
92
|
});
|
|
66
93
|
</script>
|
|
@@ -70,11 +97,14 @@
|
|
|
70
97
|
Manage a global variable in a repeatable way.
|
|
71
98
|
|
|
72
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.
|
|
73
102
|
|
|
74
103
|
### Outputs
|
|
75
104
|
: payload (any) : The global variable value
|
|
76
105
|
: topic (string) : The variable name/path used to store the value.
|
|
77
|
-
:
|
|
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).
|
|
78
108
|
|
|
79
109
|
### Details
|
|
80
110
|
Global variables are meant to be retrieved in other places, this necessarily means managing the same string in multiple places.
|
|
@@ -83,6 +113,8 @@ This node allows you to get a global variable while supporting rename and deleti
|
|
|
83
113
|
|
|
84
114
|
It links to a `global-setter` node by Node ID, so if that node's path is changed, this node will still work.
|
|
85
115
|
|
|
116
|
+
Configurable output property allows chaining multiple getters in series if only interested in the value.
|
|
117
|
+
|
|
86
118
|
### Status
|
|
87
119
|
- Green (dot): Configuration update
|
|
88
120
|
- Blue (dot): State changed
|
package/nodes/global-getter.js
CHANGED
|
@@ -3,34 +3,43 @@ 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";
|
|
6
7
|
|
|
7
8
|
node.on('input', function(msg) {
|
|
8
9
|
const setterNode = RED.nodes.getNode(node.targetNodeId);
|
|
9
10
|
|
|
10
11
|
if (setterNode && setterNode.varName) {
|
|
11
12
|
const globalContext = node.context().global;
|
|
12
|
-
|
|
13
|
-
// Retrieve the wrapper object
|
|
14
13
|
const storedObject = globalContext.get(setterNode.varName, setterNode.storeName);
|
|
15
14
|
|
|
16
15
|
if (storedObject !== undefined) {
|
|
17
|
-
|
|
16
|
+
let val = storedObject;
|
|
17
|
+
let units = null;
|
|
18
|
+
let meta = {};
|
|
19
|
+
|
|
20
|
+
// CHECK: Is this wrapper format?
|
|
18
21
|
if (storedObject && typeof storedObject === 'object' && storedObject.hasOwnProperty('value') && storedObject.hasOwnProperty('meta')) {
|
|
19
22
|
// Yes: Unwrap it
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
val = storedObject.value;
|
|
24
|
+
units = storedObject.units;
|
|
25
|
+
meta = storedObject.meta;
|
|
22
26
|
} else {
|
|
23
|
-
//
|
|
24
|
-
|
|
27
|
+
// Legacy/Raw: Metadata is limited
|
|
28
|
+
meta = { path: setterNode.varName, legacy: true };
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
msg.
|
|
31
|
+
RED.util.setMessageProperty(msg, node.outputProperty, val);
|
|
32
|
+
|
|
33
|
+
msg.topic = setterNode.varName;
|
|
34
|
+
|
|
35
|
+
msg.units = units;
|
|
36
|
+
|
|
37
|
+
msg.metadata = meta;
|
|
28
38
|
|
|
29
|
-
node.status({ fill: "blue", shape: "dot", text: `Get: ${
|
|
39
|
+
node.status({ fill: "blue", shape: "dot", text: `Get: ${val}` });
|
|
30
40
|
node.send(msg);
|
|
31
41
|
} else {
|
|
32
|
-
|
|
33
|
-
// Optional: warn or just do nothing
|
|
42
|
+
node.status({ fill: "red", shape: "ring", text: "Global variable undefined" });
|
|
34
43
|
}
|
|
35
44
|
} else {
|
|
36
45
|
node.warn("Source node not found or not configured.");
|
|
@@ -39,4 +48,4 @@ module.exports = function(RED) {
|
|
|
39
48
|
});
|
|
40
49
|
}
|
|
41
50
|
RED.nodes.registerType("global-getter", GlobalGetterNode);
|
|
42
|
-
}
|
|
51
|
+
}
|
package/nodes/global-setter.html
CHANGED
|
@@ -5,32 +5,36 @@
|
|
|
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
|
+
</div>
|
|
10
|
+
<div class="form-row">
|
|
11
|
+
<label for="node-input-property"><i class="fa fa-ellipsis-h"></i> Input Property</label>
|
|
12
|
+
<input type="text" id="node-input-property" style="width:70%;">
|
|
9
13
|
</div>
|
|
10
14
|
<div class="form-tips">
|
|
11
|
-
<b>Note:</b> Use the dropdown inside the Path input to select a specific Context Store
|
|
12
|
-
When this node is redeployed or deleted, it will automatically remove
|
|
15
|
+
<b>Note:</b> Use the dropdown inside the Path input to select a specific Context Store.
|
|
16
|
+
When this node is redeployed or deleted, it will automatically remove the variable from memory.
|
|
13
17
|
</div>
|
|
14
18
|
</script>
|
|
15
19
|
|
|
16
20
|
<script type="text/javascript">
|
|
17
21
|
RED.nodes.registerType('global-setter', {
|
|
18
22
|
category: 'control',
|
|
19
|
-
color: '#
|
|
23
|
+
color: '#E2D96E',
|
|
20
24
|
defaults: {
|
|
21
25
|
name: { value: "" },
|
|
22
|
-
path: { value: "", required: true }
|
|
26
|
+
path: { value: "", required: true },
|
|
27
|
+
property: { value: "payload", required: true }
|
|
23
28
|
},
|
|
24
29
|
inputs: 1,
|
|
25
30
|
outputs: 1,
|
|
26
31
|
icon: "font-awesome/fa-align-right",
|
|
27
32
|
label: function() {
|
|
28
|
-
// Remove #store: prefix for cleaner label
|
|
29
33
|
let lbl = this.path;
|
|
30
34
|
if (lbl && lbl.startsWith("#") && lbl.includes(":")) {
|
|
31
35
|
lbl = lbl.split(":")[3];
|
|
32
36
|
}
|
|
33
|
-
return this.name || lbl || "global set";
|
|
37
|
+
return this.name || "Set: " + lbl || "global set";
|
|
34
38
|
},
|
|
35
39
|
paletteLabel: "global set",
|
|
36
40
|
oneditprepare: function() {
|
|
@@ -38,6 +42,11 @@
|
|
|
38
42
|
$("#node-input-path").typedInput({
|
|
39
43
|
types: ['global']
|
|
40
44
|
});
|
|
45
|
+
|
|
46
|
+
// Input Property Selector
|
|
47
|
+
$("#node-input-property").typedInput({
|
|
48
|
+
types: ['msg']
|
|
49
|
+
});
|
|
41
50
|
}
|
|
42
51
|
});
|
|
43
52
|
</script>
|
|
@@ -48,6 +57,8 @@ Manage a global variable in a repeatable way.
|
|
|
48
57
|
|
|
49
58
|
### Inputs
|
|
50
59
|
: payload (any) : Input payload is passed through unchanged.
|
|
60
|
+
: property (string) : The input property where the value is taken from.
|
|
61
|
+
: units (string) : The units associated with the value, if any. Also supports nested units at `msg.<inputProperty>.units`.
|
|
51
62
|
|
|
52
63
|
### Outputs
|
|
53
64
|
: payload (any) : Original payload.
|
package/nodes/global-setter.js
CHANGED
|
@@ -7,32 +7,50 @@ module.exports = function(RED) {
|
|
|
7
7
|
|
|
8
8
|
node.varName = parsed.key;
|
|
9
9
|
node.storeName = parsed.store;
|
|
10
|
+
node.inputProperty = config.property;
|
|
10
11
|
|
|
11
12
|
node.on('input', function(msg) {
|
|
12
13
|
if (node.varName) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
// READ from the configured property
|
|
15
|
+
const valueToStore = RED.util.getMessageProperty(msg, node.inputProperty);
|
|
16
|
+
|
|
17
|
+
if (valueToStore !== undefined) {
|
|
18
|
+
const globalContext = node.context().global;
|
|
19
|
+
|
|
20
|
+
// 1. Try to find units in the standard location (msg.units)
|
|
21
|
+
// 2. If not found, check if the input property itself is an object containing .units
|
|
22
|
+
let capturedUnits = msg.units;
|
|
23
|
+
|
|
24
|
+
// Optional: Deep check if msg.payload was an object that contained units
|
|
25
|
+
if (!capturedUnits && typeof valueToStore === 'object' && valueToStore !== null && valueToStore.units) {
|
|
26
|
+
capturedUnits = valueToStore.units;
|
|
23
27
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
// Create wrapper with simplified metadata
|
|
30
|
+
const storedObject = {
|
|
31
|
+
value: valueToStore,
|
|
32
|
+
topic: node.varName,
|
|
33
|
+
units: capturedUnits,
|
|
34
|
+
meta: {
|
|
35
|
+
sourceId: node.id,
|
|
36
|
+
sourceName: node.name || config.path,
|
|
37
|
+
sourcePath: node.varName,
|
|
38
|
+
lastSet: new Date().toISOString()
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
node.status({ fill: "blue", shape: "dot", text: `Set: ${storedObject.value}` });
|
|
43
|
+
globalContext.set(node.varName, storedObject, node.storeName);
|
|
44
|
+
}
|
|
27
45
|
}
|
|
28
46
|
|
|
29
|
-
node.status({ fill: "blue", shape: "dot", text: `Set: ${msg.payload}` });
|
|
30
47
|
node.send(msg);
|
|
31
48
|
});
|
|
32
49
|
|
|
33
50
|
// CLEANUP
|
|
34
51
|
node.on('close', function(removed, done) {
|
|
35
|
-
if
|
|
52
|
+
// Do NOT prune if Node-RED is simply restarting or deploying.
|
|
53
|
+
if (removed && node.varName) {
|
|
36
54
|
const globalContext = node.context().global;
|
|
37
55
|
globalContext.set(node.varName, undefined, node.storeName);
|
|
38
56
|
}
|
package/nodes/units-block.js
CHANGED
|
@@ -41,7 +41,7 @@ module.exports = function(RED) {
|
|
|
41
41
|
unit: config.unit
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
// Validate configuration
|
|
44
|
+
// Validate configuration
|
|
45
45
|
if (!validUnits.includes(node.runtime.unit)) {
|
|
46
46
|
node.runtime.unit = "°F";
|
|
47
47
|
node.status({ fill: "red", shape: "ring", text: "invalid unit, using °F" });
|
|
@@ -83,11 +83,12 @@ module.exports = function(RED) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// Process input
|
|
86
|
-
const outputMsg = RED.util.cloneMessage(msg);
|
|
87
86
|
const payloadPreview = msg.payload !== null ? (typeof msg.payload === "number" ? msg.payload.toFixed(2) : JSON.stringify(msg.payload).slice(0, 20)) : "none";
|
|
88
87
|
|
|
89
88
|
node.status({ fill: "blue", shape: "dot", text: `in: ${payloadPreview} unit: ${node.runtime.unit}` });
|
|
90
|
-
|
|
89
|
+
|
|
90
|
+
msg.units = node.runtime.unit;
|
|
91
|
+
send(msg);
|
|
91
92
|
if (done) done();
|
|
92
93
|
} catch (error) {
|
|
93
94
|
node.status({ fill: "red", shape: "ring", text: "processing error" });
|
package/package.json
CHANGED