@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.
@@ -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: '#301934',
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(":").pop();
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
- : globalMetadata (object) : Metadata about the global variable (store, path).
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
@@ -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
- // CHECK: Is this our wrapper format?
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
- msg.payload = storedObject.value;
21
- msg.globalMetadata = storedObject.meta; // Expose the ID/Metadata here
23
+ val = storedObject.value;
24
+ units = storedObject.units;
25
+ meta = storedObject.meta;
22
26
  } else {
23
- // No: It's legacy/raw data, just pass it through
24
- msg.payload = storedObject;
27
+ // Legacy/Raw: Metadata is limited
28
+ meta = { path: setterNode.varName, legacy: true };
25
29
  }
26
30
 
27
- msg.topic = setterNode.varName;
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: ${msg.payload}` });
39
+ node.status({ fill: "blue", shape: "dot", text: `Get: ${val}` });
30
40
  node.send(msg);
31
41
  } else {
32
- // Variable exists in config but not in memory yet
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
+ }
@@ -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 (File, Memory, etc).
12
- When this node is redeployed or deleted, it will automatically remove (prune) the variable from that store.
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: '#301934',
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.
@@ -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
- 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()
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
- globalContext.set(node.varName, storedObject, node.storeName);
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 (node.varName) {
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
  }
@@ -41,7 +41,7 @@ module.exports = function(RED) {
41
41
  unit: config.unit
42
42
  };
43
43
 
44
- // Validate configuration (Req 8)
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
- send(outputMsg);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bldgblocks/node-red-contrib-control",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "Sedona-inspired control nodes for Node-RED",
5
5
  "keywords": [ "node-red", "sedona", "control", "hvac" ],
6
6
  "files": ["nodes/*.js", "nodes/*.html"],