@bldgblocks/node-red-contrib-control 0.1.28 → 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/README.md +1 -1
- package/nodes/frequency-block.html +3 -1
- package/nodes/frequency-block.js +64 -7
- package/nodes/global-getter.html +128 -0
- package/nodes/global-getter.js +51 -0
- package/nodes/global-setter.html +83 -0
- package/nodes/global-setter.js +61 -0
- package/nodes/units-block.js +4 -3
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ There are TONS of fantastic node libraries out there but usually focusing on one
|
|
|
24
24
|
- Node commands, such as 'reset' or 'mode' or changing setpoints via messages.
|
|
25
25
|
|
|
26
26
|
## How To Use
|
|
27
|
-
|
|
27
|
+
<img width="1049" height="131" alt="Screenshot_20251204_194633" src="https://github.com/user-attachments/assets/d29fb46d-c23d-4185-a9e7-f01ff1efdb81" />
|
|
28
28
|
|
|
29
29
|
The help section of every node describes the expected msg.context (data tag) for the intended msg.payload incoming. You can of course do this as you process data through a 'change' block, or use the provided 'contextual label' block which makes it easier to add and remove tags, more compact (especially if label hidden), and more transparent of the data flowing (ALL nodes contain complete status usage). Most nodes use a simple in1, in2, and so on.
|
|
30
30
|
|
|
@@ -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
|
package/nodes/frequency-block.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
|
89
|
-
|
|
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,128 @@
|
|
|
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
|
+
|
|
7
|
+
<div class="form-row">
|
|
8
|
+
<label for="node-input-targetNode"><i class="fa fa-crosshairs"></i> Source</label>
|
|
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>
|
|
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
|
+
|
|
20
|
+
<div class="form-tips">
|
|
21
|
+
<b>Note:</b> Targeting by Node ID allows the source path to change without breaking this link.
|
|
22
|
+
</div>
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<script type="text/javascript">
|
|
26
|
+
RED.nodes.registerType('global-getter', {
|
|
27
|
+
category: 'control',
|
|
28
|
+
color: '#3FADB5',
|
|
29
|
+
defaults: {
|
|
30
|
+
name: { value: "" },
|
|
31
|
+
targetNode: { value: "", required: true },
|
|
32
|
+
outputProperty: { value: "payload", required: true }
|
|
33
|
+
},
|
|
34
|
+
inputs: 1,
|
|
35
|
+
outputs: 1,
|
|
36
|
+
icon: "font-awesome/fa-align-left",
|
|
37
|
+
label: function() {
|
|
38
|
+
if (this.targetNode) {
|
|
39
|
+
const target = RED.nodes.node(this.targetNode);
|
|
40
|
+
if (target) {
|
|
41
|
+
let lbl = target.path || target.name;
|
|
42
|
+
if(lbl && lbl.includes(":")) {
|
|
43
|
+
lbl = lbl.split(":")[3];
|
|
44
|
+
}
|
|
45
|
+
return "Get: " + lbl;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return this.name || "global get";
|
|
49
|
+
},
|
|
50
|
+
paletteLabel: "global get",
|
|
51
|
+
oneditprepare: function() {
|
|
52
|
+
const node = this;
|
|
53
|
+
|
|
54
|
+
// 1. Populate Dropdown
|
|
55
|
+
let candidateNodes = [];
|
|
56
|
+
RED.nodes.eachNode(function(n) {
|
|
57
|
+
if (n.type === 'global-setter') {
|
|
58
|
+
let displayPath = n.path || "No Path";
|
|
59
|
+
|
|
60
|
+
candidateNodes.push({
|
|
61
|
+
value: n.id,
|
|
62
|
+
label: displayPath + (n.name ? ` (${n.name})` : "")
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
candidateNodes.sort((a, b) => a.label.localeCompare(b.label));
|
|
68
|
+
|
|
69
|
+
$("#node-input-targetNode").typedInput({
|
|
70
|
+
types: [{
|
|
71
|
+
value: "target",
|
|
72
|
+
options: candidateNodes
|
|
73
|
+
}]
|
|
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
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
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>
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
node.outputProperty = config.outputProperty || "payload";
|
|
7
|
+
|
|
8
|
+
node.on('input', function(msg) {
|
|
9
|
+
const setterNode = RED.nodes.getNode(node.targetNodeId);
|
|
10
|
+
|
|
11
|
+
if (setterNode && setterNode.varName) {
|
|
12
|
+
const globalContext = node.context().global;
|
|
13
|
+
const storedObject = globalContext.get(setterNode.varName, setterNode.storeName);
|
|
14
|
+
|
|
15
|
+
if (storedObject !== undefined) {
|
|
16
|
+
let val = storedObject;
|
|
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
|
+
}
|
|
30
|
+
|
|
31
|
+
RED.util.setMessageProperty(msg, node.outputProperty, val);
|
|
32
|
+
|
|
33
|
+
msg.topic = setterNode.varName;
|
|
34
|
+
|
|
35
|
+
msg.units = units;
|
|
36
|
+
|
|
37
|
+
msg.metadata = meta;
|
|
38
|
+
|
|
39
|
+
node.status({ fill: "blue", shape: "dot", text: `Get: ${val}` });
|
|
40
|
+
node.send(msg);
|
|
41
|
+
} else {
|
|
42
|
+
node.status({ fill: "red", shape: "ring", text: "Global variable undefined" });
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
node.warn("Source node not found or not configured.");
|
|
46
|
+
node.status({ fill: "red", shape: "ring", text: "Source node not found" });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
RED.nodes.registerType("global-getter", GlobalGetterNode);
|
|
51
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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" 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%;">
|
|
13
|
+
</div>
|
|
14
|
+
<div class="form-tips">
|
|
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.
|
|
17
|
+
</div>
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<script type="text/javascript">
|
|
21
|
+
RED.nodes.registerType('global-setter', {
|
|
22
|
+
category: 'control',
|
|
23
|
+
color: '#E2D96E',
|
|
24
|
+
defaults: {
|
|
25
|
+
name: { value: "" },
|
|
26
|
+
path: { value: "", required: true },
|
|
27
|
+
property: { value: "payload", required: true }
|
|
28
|
+
},
|
|
29
|
+
inputs: 1,
|
|
30
|
+
outputs: 1,
|
|
31
|
+
icon: "font-awesome/fa-align-right",
|
|
32
|
+
label: function() {
|
|
33
|
+
let lbl = this.path;
|
|
34
|
+
if (lbl && lbl.startsWith("#") && lbl.includes(":")) {
|
|
35
|
+
lbl = lbl.split(":")[3];
|
|
36
|
+
}
|
|
37
|
+
return this.name || "Set: " + lbl || "global set";
|
|
38
|
+
},
|
|
39
|
+
paletteLabel: "global set",
|
|
40
|
+
oneditprepare: function() {
|
|
41
|
+
// RESTRICT TO GLOBAL ONLY
|
|
42
|
+
$("#node-input-path").typedInput({
|
|
43
|
+
types: ['global']
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Input Property Selector
|
|
47
|
+
$("#node-input-property").typedInput({
|
|
48
|
+
types: ['msg']
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<!-- Help Section -->
|
|
55
|
+
<script type="text/markdown" data-help-name="global-setter">
|
|
56
|
+
Manage a global variable in a repeatable way.
|
|
57
|
+
|
|
58
|
+
### Inputs
|
|
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`.
|
|
62
|
+
|
|
63
|
+
### Outputs
|
|
64
|
+
: payload (any) : Original payload.
|
|
65
|
+
|
|
66
|
+
### Details
|
|
67
|
+
Global variables are meant to be retrieved in other places, this necessarily means managing the same string in multiple places.
|
|
68
|
+
|
|
69
|
+
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.
|
|
70
|
+
|
|
71
|
+
When this node is deleted or the flow is redeployed, it will automatically remove (prune) the variable from the selected Context Store.
|
|
72
|
+
|
|
73
|
+
### Status
|
|
74
|
+
- Green (dot): Configuration update
|
|
75
|
+
- Blue (dot): State changed
|
|
76
|
+
- Blue (ring): State unchanged
|
|
77
|
+
- Red (ring): Error
|
|
78
|
+
- Yellow (ring): Warning
|
|
79
|
+
|
|
80
|
+
### References
|
|
81
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
82
|
+
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
83
|
+
</script>
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
node.inputProperty = config.property;
|
|
11
|
+
|
|
12
|
+
node.on('input', function(msg) {
|
|
13
|
+
if (node.varName) {
|
|
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;
|
|
27
|
+
}
|
|
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
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
node.send(msg);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// CLEANUP
|
|
51
|
+
node.on('close', function(removed, done) {
|
|
52
|
+
// Do NOT prune if Node-RED is simply restarting or deploying.
|
|
53
|
+
if (removed && node.varName) {
|
|
54
|
+
const globalContext = node.context().global;
|
|
55
|
+
globalContext.set(node.varName, undefined, node.storeName);
|
|
56
|
+
}
|
|
57
|
+
done();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
RED.nodes.registerType("global-setter", GlobalSetterNode);
|
|
61
|
+
}
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bldgblocks/node-red-contrib-control",
|
|
3
|
-
"version": "0.1.
|
|
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"],
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
"edge-block": "nodes/edge-block.js",
|
|
32
32
|
"enum-switch-block": "nodes/enum-switch-block.js",
|
|
33
33
|
"frequency-block": "nodes/frequency-block.js",
|
|
34
|
+
"global-getter": "nodes/global-getter.js",
|
|
35
|
+
"global-setter": "nodes/global-setter.js",
|
|
34
36
|
"hysteresis-block": "nodes/hysteresis-block.js",
|
|
35
37
|
"interpolate-block": "nodes/interpolate-block.js",
|
|
36
38
|
"latch-block": "nodes/latch-block.js",
|