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

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.
@@ -5,10 +5,8 @@
5
5
  </div>
6
6
  <div class="form-row">
7
7
  <label for="node-input-algorithm" title="Control algorithm type"><i class="fa fa-cog"></i> Algorithm</label>
8
- <select id="node-input-algorithm">
9
- <option value="single">Single Setpoint</option>
10
- <option value="split">Split Setpoints</option>
11
- </select>
8
+ <input type="text" id="node-input-algorithm" class="node-input-typed" placeholder="single">
9
+ <input type="hidden" id="node-input-algorithmType">
12
10
  </div>
13
11
  <div class="form-row single-only">
14
12
  <label for="node-input-setpoint" title="Target temperature setpoint (number from num, msg, flow, or global)"><i class="fa fa-thermometer-half"></i> Setpoint</label>
@@ -50,6 +48,18 @@
50
48
  Invalid
51
49
  </div>
52
50
  </div>
51
+
52
+ <div class="form-row specified-only" style="display: none;">
53
+ <label for="node-input-coolingOn" title="Temperature to turn cooling on (number from num, msg, flow, or global)"><i class="fa fa-snowflake-o"></i> Cooling On</label>
54
+ <input type="text" id="node-input-coolingOn" placeholder="74">
55
+ <input type="hidden" id="node-input-coolingOnType">
56
+ </div>
57
+ <div class="form-row specified-only" style="display: none;">
58
+ <label for="node-input-heatingOn" title="Temperature to turn heating on (number from num, msg, flow, or global)"><i class="fa fa-fire"></i> Heating On</label>
59
+ <input type="text" id="node-input-heatingOn" placeholder="66">
60
+ <input type="hidden" id="node-input-heatingOnType">
61
+ </div>
62
+
53
63
  <div class="form-row">
54
64
  <label for="node-input-swapTime" title="Minimum time before mode change (seconds, minimum 60, from num, msg, flow, or global)"><i class="fa fa-clock-o"></i> Swap Time</label>
55
65
  <input type="text" id="node-input-swapTime" class="node-input-typed" placeholder="300">
@@ -76,11 +86,8 @@
76
86
  </div>
77
87
  <div class="form-row">
78
88
  <label for="node-input-operationMode" title="Operation mode"><i class="fa fa-cogs"></i> Operation Mode</label>
79
- <select id="node-input-operationMode">
80
- <option value="auto">Auto</option>
81
- <option value="heat">Heat</option>
82
- <option value="cool">Cool</option>
83
- </select>
89
+ <input type="text" id="node-input-operationMode" placeholder="auto">
90
+ <input type="hidden" id="node-input-operationModeType">
84
91
  </div>
85
92
  </script>
86
93
 
@@ -91,6 +98,7 @@
91
98
  defaults: {
92
99
  name: { value: "" },
93
100
  algorithm: { value: "single" },
101
+ algorithmType: { value: "dropdown" },
94
102
  setpoint: { value: "70" },
95
103
  setpointType: { value: "num" },
96
104
  deadband: { value: "2" },
@@ -98,6 +106,10 @@
98
106
  heatingSetpointType: { value: "num" },
99
107
  coolingSetpoint: { value: "74" },
100
108
  coolingSetpointType: { value: "num" },
109
+ coolingOn: { value: "74" },
110
+ coolingOnType: { value: "num" },
111
+ heatingOn: { value: "66" },
112
+ heatingOnType: { value: "num" },
101
113
  extent: { value: "1" },
102
114
  swapTime: { value: "300" },
103
115
  swapTimeType: { value: "num" },
@@ -106,7 +118,8 @@
106
118
  maxTempSetpoint: { value: "90" },
107
119
  maxTempSetpointType: { value: "num"},
108
120
  initWindow: { value: "10" },
109
- operationMode: { value: "auto" }
121
+ operationMode: { value: "auto" },
122
+ operationModeType: { value: "dropdown" },
110
123
  },
111
124
  inputs: 1,
112
125
  outputs: 1,
@@ -122,9 +135,37 @@
122
135
  const $algorithm = $("#node-input-algorithm");
123
136
  const $singleFields = $(".single-only");
124
137
  const $splitFields = $(".split-only");
138
+ const $specifiedFields = $(".specified-only");
125
139
 
126
140
  try {
127
141
  // Initialize typed inputs
142
+
143
+ $("#node-input-operationMode").typedInput({
144
+ default: "dropdown",
145
+ types: [{
146
+ value: "dropdown",
147
+ options: [
148
+ { value: "auto", label: "Auto"},
149
+ { value: "heat", label: "Heat"},
150
+ { value: "cool", label: "Cool"},
151
+ ]
152
+ }, "msg", "flow", "global"],
153
+ typeField: "#node-input-operationModeType"
154
+ }).typedInput("type", node.operationModeType).typedInput("value", node.operationMode);
155
+
156
+ $("#node-input-algorithm").typedInput({
157
+ default: "dropdown",
158
+ types: [{
159
+ value: "dropdown",
160
+ options: [
161
+ { value: "single", label: "Single"},
162
+ { value: "split", label: "Split"},
163
+ { value: "specified", label: "Specified"},
164
+ ]
165
+ }, "msg", "flow", "global"],
166
+ typeField: "#node-input-algorithmType"
167
+ }).typedInput("type", node.algorithmType).typedInput("value", node.algorithm);
168
+
128
169
  $("#node-input-setpoint").typedInput({
129
170
  default: "num",
130
171
  types: ["num", "msg", "flow", "global"],
@@ -143,6 +184,18 @@
143
184
  typeField: "#node-input-coolingSetpointType"
144
185
  }).typedInput("type", node.coolingSetpointType || "num").typedInput("value", node.coolingSetpoint);
145
186
 
187
+ $("#node-input-heatingOn").typedInput({
188
+ default: "num",
189
+ types: ["num", "msg", "flow", "global"],
190
+ typeField: "#node-input-heatingOnType"
191
+ }).typedInput("type", node.heatingOnType || "num").typedInput("value", node.heatingOn);
192
+
193
+ $("#node-input-coolingOn").typedInput({
194
+ default: "num",
195
+ types: ["num", "msg", "flow", "global"],
196
+ typeField: "#node-input-coolingOnType"
197
+ }).typedInput("type", node.coolingOnType || "num").typedInput("value", node.coolingOn);
198
+
146
199
  $("#node-input-swapTime").typedInput({
147
200
  default: "num",
148
201
  types: ["num", "msg", "flow", "global"],
@@ -178,9 +231,20 @@
178
231
  if ($algorithm.val() === "single") {
179
232
  $singleFields.show();
180
233
  $splitFields.hide();
181
- } else {
234
+ $specifiedFields.hide();
235
+ } else if ($algorithm.val() === "split") {
182
236
  $singleFields.hide();
183
237
  $splitFields.show();
238
+ $specifiedFields.hide();
239
+ } else if ($algorithm.val() === "specified") {
240
+ $singleFields.hide();
241
+ $splitFields.hide();
242
+ $specifiedFields.show();
243
+ } else {
244
+ $algorithm.val("single");
245
+ $singleFields.show();
246
+ $splitFields.hide();
247
+ $specifiedFields.hide();
184
248
  }
185
249
  }
186
250
 
@@ -188,7 +252,7 @@
188
252
  toggleFields();
189
253
 
190
254
  } catch (err) {
191
- console.error("Error in changeover-block oneditprepare:", err);
255
+ console.error("Error in oneditprepare:", err);
192
256
  }
193
257
  }
194
258
  });
@@ -10,10 +10,7 @@ module.exports = function(RED) {
10
10
  // Initialize runtime state
11
11
  node.runtime = {
12
12
  name: config.name,
13
- algorithm: config.algorithm,
14
- operationMode: config.operationMode,
15
13
  initWindow: parseFloat(config.initWindow),
16
- currentMode: (config.operationMode === "cool" ? "cooling" : "heating"),
17
14
  lastTemperature: null,
18
15
  lastModeChange: 0
19
16
  };
@@ -27,7 +24,10 @@ module.exports = function(RED) {
27
24
  node.runtime.deadband = parseFloat(RED.util.evaluateNodeProperty( config.deadband, config.deadbandType, node ));
28
25
  node.runtime.extent = parseFloat(RED.util.evaluateNodeProperty( config.extent, config.extentType, node ));
29
26
  node.runtime.minTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.minTempSetpoint, config.minTempSetpointType, node ));
30
- node.runtime.maxTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.maxTempSetpoint, config.maxTempSetpointType, node ));
27
+ node.runtime.maxTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.maxTempSetpoint, config.maxTempSetpointType, node ));
28
+ node.runtime.algorithm = RED.util.evaluateNodeProperty( config.algorithm, config.algorithmType, node );
29
+ node.runtime.operationMode = RED.util.evaluateNodeProperty( config.operationMode, config.operationModeType, node );
30
+ node.runtime.currentMode = node.runtime.operationMode === "cool" ? "cooling" : "heating";
31
31
  } catch (err) {
32
32
  node.error(`Error evaluating properties: ${err.message}`);
33
33
  if (done) done();
@@ -74,7 +74,14 @@ module.exports = function(RED) {
74
74
  }
75
75
  if (utils.requiresEvaluation(config.maxTempSetpointType)) {
76
76
  node.runtime.maxTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.maxTempSetpoint, config.maxTempSetpointType, node, msg ));
77
- }
77
+ }
78
+ if (utils.requiresEvaluation(config.algorithmType)) {
79
+ node.runtime.algorithm = RED.util.evaluateNodeProperty( config.algorithm, config.algorithmType, node, msg );
80
+ }
81
+ if (utils.requiresEvaluation(config.operationModeType)) {
82
+ node.runtime.operationMode = RED.util.evaluateNodeProperty( config.operationMode, config.operationModeType, node, msg );
83
+ node.runtime.currentMode = node.runtime.operationMode === "cool" ? "cooling" : "heating";
84
+ }
78
85
  } catch (err) {
79
86
  node.error(`Error evaluating properties: ${err.message}`);
80
87
  if (done) done();
@@ -277,9 +284,12 @@ module.exports = function(RED) {
277
284
  if (node.runtime.algorithm === "single") {
278
285
  heatingThreshold = node.runtime.setpoint - node.runtime.deadband / 2;
279
286
  coolingThreshold = node.runtime.setpoint + node.runtime.deadband / 2;
280
- } else {
287
+ } else if (node.runtime.algorithm === "split") {
281
288
  heatingThreshold = node.runtime.heatingSetpoint - node.runtime.extent;
282
289
  coolingThreshold = node.runtime.coolingSetpoint + node.runtime.extent;
290
+ } else if (node.runtime.algorithm === "specified") {
291
+ heatingThreshold = node.runtime.heatingOn - node.runtime.extent;
292
+ coolingThreshold = node.runtime.coolingOn + node.runtime.extent;
283
293
  }
284
294
 
285
295
  if (temp < heatingThreshold) {
@@ -311,9 +321,12 @@ module.exports = function(RED) {
311
321
  if (node.runtime.algorithm === "single") {
312
322
  heatingThreshold = node.runtime.setpoint - node.runtime.deadband / 2;
313
323
  coolingThreshold = node.runtime.setpoint + node.runtime.deadband / 2;
314
- } else {
324
+ } else if (node.runtime.algorithm === "split") {
315
325
  heatingThreshold = node.runtime.heatingSetpoint - node.runtime.extent;
316
326
  coolingThreshold = node.runtime.coolingSetpoint + node.runtime.extent;
327
+ } else if (node.runtime.algorithm === "specified") {
328
+ heatingThreshold = node.runtime.heatingOn - node.runtime.extent;
329
+ coolingThreshold = node.runtime.coolingOn + node.runtime.extent;
317
330
  }
318
331
 
319
332
  let desiredMode = node.runtime.currentMode;
@@ -352,9 +365,12 @@ module.exports = function(RED) {
352
365
  if (node.runtime.algorithm === "single") {
353
366
  effectiveHeatingSetpoint = node.runtime.setpoint - node.runtime.deadband / 2;
354
367
  effectiveCoolingSetpoint = node.runtime.setpoint + node.runtime.deadband / 2;
355
- } else {
368
+ } else if (node.runtime.algorithm === "split") {
356
369
  effectiveHeatingSetpoint = node.runtime.heatingSetpoint;
357
370
  effectiveCoolingSetpoint = node.runtime.coolingSetpoint;
371
+ } else if (node.runtime.algorithm === "specified") {
372
+ effectiveHeatingSetpoint = node.runtime.heatingOn;
373
+ effectiveCoolingSetpoint = node.runtime.coolingOn;
358
374
  }
359
375
 
360
376
  return [
@@ -70,7 +70,7 @@
70
70
  }).typedInput("type", node.delayOffType || "num").typedInput("value", node.delayOff);
71
71
 
72
72
  } catch (err) {
73
- console.error("Error in hysteresis-block oneditprepare:", err);
73
+ console.error("Error in oneditprepare:", err);
74
74
  }
75
75
  }
76
76
  });
@@ -39,9 +39,11 @@ Measures pulse frequency from boolean rising edges.
39
39
 
40
40
  ### Details
41
41
  Measures pulse frequency from rising edges in `msg.payload` (boolean, `true` for pulse), outputting a message with
42
- `msg.payload = { ppm, pph, ppd }` (pulses per minute, hour, day) on the second and subsequent rising edges
42
+ `msg.payload = { ppm, pph, ppd, duty }` (pulses per minute, hour, day, duty/hr) on the second and subsequent rising edges
43
43
  (first edge sets baseline). Resets state via `msg.context = "reset"` with `msg.payload = true`.
44
44
 
45
+ Outputs a duty cycle in percentage of the last 60 minutes that the signal has been true.
46
+
45
47
  ### Status
46
48
  - Green (dot): Configuration
47
49
  - Blue (dot): Output, no alarm
@@ -11,7 +11,9 @@ module.exports = function(RED) {
11
11
  completeCycle: false,
12
12
  ppm: 0,
13
13
  pph: 0,
14
- ppd: 0
14
+ ppd: 0,
15
+ pulseHistory: [], // Array to store {start: timestamp, duration: ms}
16
+ currentPulseStart: 0
15
17
  };
16
18
 
17
19
  node.status({
@@ -20,7 +22,37 @@ module.exports = function(RED) {
20
22
  text: "awaiting first pulse"
21
23
  });
22
24
 
23
- // FEATURE: I want a runtime percentage per hour duty cycle
25
+ function calculateDutyCycle(now, currentInputValue) {
26
+ const oneHourAgo = now - 3600000;
27
+
28
+ // Clean up pulses older than 1 hour
29
+ node.runtime.pulseHistory = node.runtime.pulseHistory.filter(pulse => {
30
+ return (pulse.start + pulse.duration) > oneHourAgo;
31
+ });
32
+
33
+ let totalOnTime = 0;
34
+
35
+ // Sum all pulse durations within the last hour
36
+ node.runtime.pulseHistory.forEach(pulse => {
37
+ const pulseEnd = pulse.start + pulse.duration;
38
+ const effectiveStart = Math.max(pulse.start, oneHourAgo);
39
+ const effectiveEnd = Math.min(pulseEnd, now);
40
+ if (effectiveEnd > effectiveStart) {
41
+ totalOnTime += (effectiveEnd - effectiveStart);
42
+ }
43
+ });
44
+
45
+ // Add current ongoing pulse if active
46
+ if (currentInputValue && node.runtime.currentPulseStart > 0) {
47
+ const currentPulseTime = Math.max(node.runtime.currentPulseStart, oneHourAgo);
48
+ totalOnTime += (now - currentPulseTime);
49
+ }
50
+
51
+ return {
52
+ dutyCycle: (totalOnTime / 3600000) * 100,
53
+ onTime: totalOnTime
54
+ };
55
+ }
24
56
 
25
57
  node.on("input", function(msg, send, done) {
26
58
  send = send || function() { node.send.apply(node, arguments); };
@@ -52,6 +84,8 @@ module.exports = function(RED) {
52
84
  node.runtime.ppm = 0;
53
85
  node.runtime.pph = 0;
54
86
  node.runtime.ppd = 0;
87
+ node.runtime.pulseHistory = [];
88
+ node.runtime.currentPulseStart = 0;
55
89
  node.status({ fill: "green", shape: "dot", text: "reset" });
56
90
  }
57
91
  if (done) done();
@@ -77,16 +111,39 @@ module.exports = function(RED) {
77
111
  return;
78
112
  }
79
113
 
114
+ const now = Date.now();
115
+
116
+ // Track pulse edges for duty cycle
117
+ if (inputValue && !node.runtime.lastIn) {
118
+ // Rising edge - start new pulse
119
+ node.runtime.currentPulseStart = now;
120
+ } else if (!inputValue && node.runtime.lastIn) {
121
+ // Falling edge - record completed pulse
122
+ if (node.runtime.currentPulseStart > 0) {
123
+ const duration = now - node.runtime.currentPulseStart;
124
+ node.runtime.pulseHistory.push({
125
+ start: node.runtime.currentPulseStart,
126
+ duration: duration
127
+ });
128
+ node.runtime.currentPulseStart = 0;
129
+ }
130
+ }
131
+
132
+ // Calculate duty cycle for the rolling hour
133
+ const dutyData = calculateDutyCycle(now, inputValue);
134
+
80
135
  // Initialize output
81
136
  let output = {
82
137
  ppm: node.runtime.ppm,
83
138
  pph: node.runtime.pph,
84
- ppd: node.runtime.ppd
139
+ ppd: node.runtime.ppd,
140
+ dutyCycle: dutyData.dutyCycle.toFixed(2),
141
+ onTime: dutyData.onTime
85
142
  };
86
143
 
87
144
  // Detect rising edge
88
- if (inputValue && !node.runtime.lastIn) { // Rising edge: true and lastIn was false
89
- let now = Date.now();
145
+ if (inputValue && !node.runtime.lastIn) {
146
+ // Rising edge: true and lastIn was false
90
147
  if (!node.runtime.completeCycle) {
91
148
  node.runtime.completeCycle = true;
92
149
  } else {
@@ -114,14 +171,14 @@ module.exports = function(RED) {
114
171
  node.status({
115
172
  fill: "blue",
116
173
  shape: "dot",
117
- text: `input: ${inputValue}, ppm: ${output.ppm.toFixed(2)}, pph: ${output.pph.toFixed(2)}, ppd: ${output.ppd.toFixed(2)}`
174
+ text: `input: ${inputValue}, ppm: ${output.ppm.toFixed(2)}, pph: ${output.pph.toFixed(2)}, ppd: ${output.ppd.toFixed(2)}, duty: ${output.dutyCycle}%`
118
175
  });
119
176
  send({ payload: output });
120
177
  } else {
121
178
  node.status({
122
179
  fill: "blue",
123
180
  shape: "ring",
124
- text: `input: ${inputValue}, ppm: ${node.runtime.ppm.toFixed(2)}, pph: ${node.runtime.pph.toFixed(2)}, ppd: ${node.runtime.ppd.toFixed(2)}`
181
+ text: `input: ${inputValue}, ppm: ${node.runtime.ppm.toFixed(2)}, pph: ${node.runtime.pph.toFixed(2)}, ppd: ${node.runtime.ppd.toFixed(2)}, duty: ${output.dutyCycle}%`
125
182
  });
126
183
  }
127
184
 
@@ -0,0 +1,96 @@
1
+ <script type="text/html" data-template-name="global-getter">
2
+ <div class="form-row">
3
+ <label for="node-input-name"><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-targetNode"><i class="fa fa-crosshairs"></i> Source</label>
8
+ <input type="text" id="node-input-targetNode" style="width:70%;">
9
+ </div>
10
+ <div class="form-tips">
11
+ Targeting by Node ID allows the source path to change without breaking this link.
12
+ </div>
13
+ </script>
14
+
15
+ <script type="text/javascript">
16
+ RED.nodes.registerType('global-getter', {
17
+ category: 'control',
18
+ color: '#301934',
19
+ defaults: {
20
+ name: { value: "" },
21
+ targetNode: { value: "", required: true }
22
+ },
23
+ inputs: 1,
24
+ outputs: 1,
25
+ icon: "font-awesome/fa-align-left",
26
+ label: function() {
27
+ if (this.targetNode) {
28
+ const target = RED.nodes.node(this.targetNode);
29
+ if (target) {
30
+ // Display the path, removing the #store: prefix for readability in the label if present
31
+ let lbl = target.path || target.name;
32
+ if(lbl && lbl.includes(":")) {
33
+ lbl = lbl.split(":").pop();
34
+ }
35
+ return "Get: " + lbl;
36
+ }
37
+ }
38
+ return this.name || "global get";
39
+ },
40
+ paletteLabel: "global get",
41
+ oneditprepare: function() {
42
+ const node = this;
43
+
44
+ let candidateNodes = [];
45
+ RED.nodes.eachNode(function(n) {
46
+ if (n.type === 'global-setter') {
47
+ let displayPath = n.path || "No Path";
48
+
49
+ candidateNodes.push({
50
+ value: n.id,
51
+ label: displayPath + (n.name ? ` (${n.name})` : "")
52
+ });
53
+ }
54
+ });
55
+
56
+ candidateNodes.sort((a, b) => a.label.localeCompare(b.label));
57
+
58
+ $("#node-input-targetNode").typedInput({
59
+ types: [{
60
+ value: "target",
61
+ options: candidateNodes
62
+ }]
63
+ });
64
+ }
65
+ });
66
+ </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>
@@ -0,0 +1,42 @@
1
+ module.exports = function(RED) {
2
+ function GlobalGetterNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+ node.targetNodeId = config.targetNode;
6
+
7
+ node.on('input', function(msg) {
8
+ const setterNode = RED.nodes.getNode(node.targetNodeId);
9
+
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
+
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;
28
+
29
+ node.status({ fill: "blue", shape: "dot", text: `Get: ${msg.payload}` });
30
+ node.send(msg);
31
+ } else {
32
+ // Variable exists in config but not in memory yet
33
+ // Optional: warn or just do nothing
34
+ }
35
+ } else {
36
+ node.warn("Source node not found or not configured.");
37
+ node.status({ fill: "red", shape: "ring", text: "Source node not found" });
38
+ }
39
+ });
40
+ }
41
+ RED.nodes.registerType("global-getter", GlobalGetterNode);
42
+ }
@@ -0,0 +1,72 @@
1
+ <script type="text/html" data-template-name="global-setter">
2
+ <div class="form-row">
3
+ <label for="node-input-name"><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-path"><i class="fa fa-sitemap"></i> Global Path</label>
8
+ <input type="text" id="node-input-path" placeholder="furnace/outputs/heat">
9
+ </div>
10
+ <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.
13
+ </div>
14
+ </script>
15
+
16
+ <script type="text/javascript">
17
+ RED.nodes.registerType('global-setter', {
18
+ category: 'control',
19
+ color: '#301934',
20
+ defaults: {
21
+ name: { value: "" },
22
+ path: { value: "", required: true }
23
+ },
24
+ inputs: 1,
25
+ outputs: 1,
26
+ icon: "font-awesome/fa-align-right",
27
+ label: function() {
28
+ // Remove #store: prefix for cleaner label
29
+ let lbl = this.path;
30
+ if (lbl && lbl.startsWith("#") && lbl.includes(":")) {
31
+ lbl = lbl.split(":")[3];
32
+ }
33
+ return this.name || lbl || "global set";
34
+ },
35
+ paletteLabel: "global set",
36
+ oneditprepare: function() {
37
+ // RESTRICT TO GLOBAL ONLY
38
+ $("#node-input-path").typedInput({
39
+ types: ['global']
40
+ });
41
+ }
42
+ });
43
+ </script>
44
+
45
+ <!-- Help Section -->
46
+ <script type="text/markdown" data-help-name="global-setter">
47
+ Manage a global variable in a repeatable way.
48
+
49
+ ### Inputs
50
+ : payload (any) : Input payload is passed through unchanged.
51
+
52
+ ### Outputs
53
+ : payload (any) : Original payload.
54
+
55
+ ### Details
56
+ Global variables are meant to be retrieved in other places, this necessarily means managing the same string in multiple places.
57
+
58
+ This node allows you to set a global variable in one place, and retrieve it elsewhere using the `global-getter` node while supporting rename and deletion.
59
+
60
+ When this node is deleted or the flow is redeployed, it will automatically remove (prune) the variable from the selected Context Store.
61
+
62
+ ### Status
63
+ - Green (dot): Configuration update
64
+ - Blue (dot): State changed
65
+ - Blue (ring): State unchanged
66
+ - Red (ring): Error
67
+ - Yellow (ring): Warning
68
+
69
+ ### References
70
+ - [Node-RED Documentation](https://nodered.org/docs/)
71
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
72
+ </script>
@@ -0,0 +1,43 @@
1
+ module.exports = function(RED) {
2
+ function GlobalSetterNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ const node = this;
5
+
6
+ const parsed = RED.util.parseContextStore(config.path);
7
+
8
+ node.varName = parsed.key;
9
+ node.storeName = parsed.store;
10
+
11
+ node.on('input', function(msg) {
12
+ 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()
23
+ }
24
+ };
25
+
26
+ globalContext.set(node.varName, storedObject, node.storeName);
27
+ }
28
+
29
+ node.status({ fill: "blue", shape: "dot", text: `Set: ${msg.payload}` });
30
+ node.send(msg);
31
+ });
32
+
33
+ // CLEANUP
34
+ node.on('close', function(removed, done) {
35
+ if (node.varName) {
36
+ const globalContext = node.context().global;
37
+ globalContext.set(node.varName, undefined, node.storeName);
38
+ }
39
+ done();
40
+ });
41
+ }
42
+ RED.nodes.registerType("global-setter", GlobalSetterNode);
43
+ }
@@ -84,7 +84,7 @@ module.exports = function(RED) {
84
84
 
85
85
  // Add validation to ensure numbers
86
86
  if (isNaN(upperTurnOn) || isNaN(upperTurnOff) || isNaN(lowerTurnOn) || isNaN(lowerTurnOff)) {
87
- node.status({ fill: "red", shape: "ring", text: "invalid boundary calculation" });
87
+ node.status({ fill: "red", shape: "ring", text: "invalid limits calculation" });
88
88
  if (done) done();
89
89
  return;
90
90
  }