@bldgblocks/node-red-contrib-control 0.1.34 → 0.1.37
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/accumulate-block.html +18 -8
- package/nodes/accumulate-block.js +39 -44
- package/nodes/add-block.html +1 -1
- package/nodes/add-block.js +18 -11
- package/nodes/alarm-collector.html +260 -0
- package/nodes/alarm-collector.js +292 -0
- package/nodes/alarm-config.html +129 -0
- package/nodes/alarm-config.js +126 -0
- package/nodes/alarm-service.html +96 -0
- package/nodes/alarm-service.js +142 -0
- package/nodes/analog-switch-block.js +25 -36
- package/nodes/and-block.js +44 -15
- package/nodes/average-block.js +46 -41
- package/nodes/boolean-switch-block.js +10 -28
- package/nodes/boolean-to-number-block.html +18 -5
- package/nodes/boolean-to-number-block.js +24 -16
- package/nodes/cache-block.js +24 -37
- package/nodes/call-status-block.html +91 -32
- package/nodes/call-status-block.js +398 -115
- package/nodes/changeover-block.html +5 -0
- package/nodes/changeover-block.js +167 -162
- package/nodes/comment-block.html +1 -1
- package/nodes/comment-block.js +14 -9
- package/nodes/compare-block.html +14 -4
- package/nodes/compare-block.js +23 -18
- package/nodes/contextual-label-block.html +5 -0
- package/nodes/contextual-label-block.js +6 -16
- package/nodes/convert-block.html +25 -39
- package/nodes/convert-block.js +31 -16
- package/nodes/count-block.html +11 -5
- package/nodes/count-block.js +34 -32
- package/nodes/delay-block.js +58 -53
- package/nodes/divide-block.js +43 -45
- package/nodes/edge-block.html +17 -10
- package/nodes/edge-block.js +43 -41
- package/nodes/enum-switch-block.js +6 -6
- package/nodes/frequency-block.html +6 -1
- package/nodes/frequency-block.js +64 -74
- package/nodes/global-getter.html +51 -15
- package/nodes/global-getter.js +43 -13
- package/nodes/global-setter.html +1 -1
- package/nodes/global-setter.js +40 -12
- package/nodes/history-buffer.html +96 -0
- package/nodes/history-buffer.js +464 -0
- package/nodes/history-collector.html +29 -1
- package/nodes/history-collector.js +46 -16
- package/nodes/history-config.html +13 -1
- package/nodes/history-service.html +84 -0
- package/nodes/history-service.js +66 -0
- package/nodes/hysteresis-block.html +5 -0
- package/nodes/hysteresis-block.js +13 -16
- package/nodes/interpolate-block.html +20 -2
- package/nodes/interpolate-block.js +39 -50
- package/nodes/join.html +78 -0
- package/nodes/join.js +78 -0
- package/nodes/latch-block.js +12 -14
- package/nodes/load-sequence-block.js +102 -110
- package/nodes/max-block.js +26 -26
- package/nodes/memory-block.js +57 -58
- package/nodes/min-block.js +26 -25
- package/nodes/minmax-block.js +35 -34
- package/nodes/modulo-block.js +45 -43
- package/nodes/multiply-block.js +43 -41
- package/nodes/negate-block.html +17 -7
- package/nodes/negate-block.js +25 -19
- package/nodes/network-point-read.html +128 -0
- package/nodes/network-point-read.js +230 -0
- package/nodes/{network-register.html → network-point-register.html} +94 -7
- package/nodes/{network-register.js → network-point-register.js} +18 -4
- package/nodes/network-point-write.html +149 -0
- package/nodes/network-point-write.js +222 -0
- package/nodes/network-service-bridge.html +131 -0
- package/nodes/network-service-bridge.js +376 -0
- package/nodes/network-service-read.html +81 -0
- package/nodes/{network-read.js → network-service-read.js} +4 -3
- package/nodes/{network-point-registry.html → network-service-registry.html} +19 -4
- package/nodes/{network-point-registry.js → network-service-registry.js} +7 -2
- package/nodes/network-service-write.html +89 -0
- package/nodes/{network-write.js → network-service-write.js} +3 -3
- package/nodes/nullify-block.js +13 -15
- package/nodes/on-change-block.html +17 -9
- package/nodes/on-change-block.js +49 -46
- package/nodes/oneshot-block.html +13 -10
- package/nodes/oneshot-block.js +57 -75
- package/nodes/or-block.js +44 -15
- package/nodes/pid-block.html +54 -4
- package/nodes/pid-block.js +459 -248
- package/nodes/priority-block.js +24 -35
- package/nodes/rate-limit-block.js +70 -72
- package/nodes/rate-of-change-block.html +33 -14
- package/nodes/rate-of-change-block.js +74 -62
- package/nodes/round-block.html +14 -9
- package/nodes/round-block.js +32 -25
- package/nodes/saw-tooth-wave-block.js +49 -76
- package/nodes/scale-range-block.html +12 -6
- package/nodes/scale-range-block.js +46 -39
- package/nodes/sine-wave-block.js +49 -57
- package/nodes/string-builder-block.js +6 -6
- package/nodes/subtract-block.js +38 -34
- package/nodes/thermistor-block.js +44 -44
- package/nodes/tick-tock-block.js +32 -32
- package/nodes/time-sequence-block.js +30 -42
- package/nodes/triangle-wave-block.js +49 -69
- package/nodes/tstat-block.js +34 -44
- package/nodes/units-block.html +90 -69
- package/nodes/units-block.js +22 -30
- package/nodes/utils.js +206 -3
- package/package.json +14 -6
- package/nodes/network-read.html +0 -56
- package/nodes/network-write.html +0 -65
|
@@ -21,7 +21,7 @@ module.exports = function(RED) {
|
|
|
21
21
|
|
|
22
22
|
// Guard against invalid msg
|
|
23
23
|
if (!msg) {
|
|
24
|
-
|
|
24
|
+
utils.setStatusError(node, "invalid message");
|
|
25
25
|
if (done) done();
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
@@ -34,7 +34,7 @@ module.exports = function(RED) {
|
|
|
34
34
|
// Check busy lock
|
|
35
35
|
if (node.isBusy) {
|
|
36
36
|
// Update status to let user know they are pushing too fast
|
|
37
|
-
|
|
37
|
+
utils.setStatusBusy(node, "busy - dropped msg");
|
|
38
38
|
if (done) done();
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
@@ -57,7 +57,7 @@ module.exports = function(RED) {
|
|
|
57
57
|
matchAgainst = results[0];
|
|
58
58
|
|
|
59
59
|
if (matchAgainst === undefined) {
|
|
60
|
-
|
|
60
|
+
utils.setStatusError(node, "property evaluation failed");
|
|
61
61
|
if (done) done();
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
@@ -97,7 +97,7 @@ module.exports = function(RED) {
|
|
|
97
97
|
|
|
98
98
|
if (match) {
|
|
99
99
|
matched = true;
|
|
100
|
-
|
|
100
|
+
utils.setStatusChanged(node, `Matched: ${rule.value}`);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -113,9 +113,9 @@ module.exports = function(RED) {
|
|
|
113
113
|
send(messages);
|
|
114
114
|
|
|
115
115
|
if (!matched && rules.length > 0) {
|
|
116
|
-
|
|
116
|
+
utils.setStatusUnchanged(node, "No match");
|
|
117
117
|
} else if (rules.length === 0) {
|
|
118
|
-
|
|
118
|
+
utils.setStatusWarn(node, "No rules configured");
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
if (done) done();
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
<label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
|
|
5
5
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
6
6
|
</div>
|
|
7
|
+
<div class="form-row">
|
|
8
|
+
<label for="node-input-inputProperty" title="Message property to read input from"><i class="fa fa-folder-open"></i> Input Property</label>
|
|
9
|
+
<input type="text" id="node-input-inputProperty" placeholder="payload">
|
|
10
|
+
</div>
|
|
7
11
|
</script>
|
|
8
12
|
|
|
9
13
|
<!-- JavaScript Section: Registers the node and handles editor logic -->
|
|
@@ -12,7 +16,8 @@
|
|
|
12
16
|
category: "bldgblocks control",
|
|
13
17
|
color: "#301934",
|
|
14
18
|
defaults: {
|
|
15
|
-
name: { value: "" }
|
|
19
|
+
name: { value: "" },
|
|
20
|
+
inputProperty: { value: "payload" }
|
|
16
21
|
},
|
|
17
22
|
inputs: 1,
|
|
18
23
|
outputs: 1,
|
package/nodes/frequency-block.js
CHANGED
|
@@ -1,39 +1,36 @@
|
|
|
1
1
|
module.exports = function(RED) {
|
|
2
|
+
const utils = require('./utils')(RED);
|
|
3
|
+
|
|
2
4
|
function FrequencyBlockNode(config) {
|
|
3
5
|
RED.nodes.createNode(this, config);
|
|
4
6
|
const node = this;
|
|
5
7
|
|
|
6
|
-
// Initialize
|
|
7
|
-
node.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
node.status({
|
|
20
|
-
fill: "green",
|
|
21
|
-
shape: "dot",
|
|
22
|
-
text: "awaiting first pulse"
|
|
23
|
-
});
|
|
8
|
+
// Initialize state
|
|
9
|
+
node.name = config.name || "";
|
|
10
|
+
node.inputProperty = config.inputProperty || "payload";
|
|
11
|
+
node.lastIn = false;
|
|
12
|
+
node.lastEdge = 0;
|
|
13
|
+
node.completeCycle = false;
|
|
14
|
+
node.ppm = 0;
|
|
15
|
+
node.pph = 0;
|
|
16
|
+
node.ppd = 0;
|
|
17
|
+
node.pulseHistory = []; // Array to store {start: timestamp, duration: ms}
|
|
18
|
+
node.currentPulseStart = 0;
|
|
19
|
+
|
|
20
|
+
utils.setStatusOK(node, "awaiting first pulse");
|
|
24
21
|
|
|
25
22
|
function calculateDutyCycle(now, currentInputValue) {
|
|
26
23
|
const oneHourAgo = now - 3600000;
|
|
27
24
|
|
|
28
25
|
// Clean up pulses older than 1 hour
|
|
29
|
-
node.
|
|
26
|
+
node.pulseHistory = node.pulseHistory.filter(pulse => {
|
|
30
27
|
return (pulse.start + pulse.duration) > oneHourAgo;
|
|
31
28
|
});
|
|
32
29
|
|
|
33
30
|
let totalOnTime = 0;
|
|
34
31
|
|
|
35
32
|
// Sum all pulse durations within the last hour
|
|
36
|
-
node.
|
|
33
|
+
node.pulseHistory.forEach(pulse => {
|
|
37
34
|
const pulseEnd = pulse.start + pulse.duration;
|
|
38
35
|
const effectiveStart = Math.max(pulse.start, oneHourAgo);
|
|
39
36
|
const effectiveEnd = Math.min(pulseEnd, now);
|
|
@@ -43,8 +40,8 @@ module.exports = function(RED) {
|
|
|
43
40
|
});
|
|
44
41
|
|
|
45
42
|
// Add current ongoing pulse if active
|
|
46
|
-
if (currentInputValue && node.
|
|
47
|
-
const currentPulseTime = Math.max(node.
|
|
43
|
+
if (currentInputValue && node.currentPulseStart > 0) {
|
|
44
|
+
const currentPulseTime = Math.max(node.currentPulseStart, oneHourAgo);
|
|
48
45
|
totalOnTime += (now - currentPulseTime);
|
|
49
46
|
}
|
|
50
47
|
|
|
@@ -59,7 +56,7 @@ module.exports = function(RED) {
|
|
|
59
56
|
|
|
60
57
|
// Guard against invalid message
|
|
61
58
|
if (!msg) {
|
|
62
|
-
|
|
59
|
+
utils.setStatusError(node, "invalid message");
|
|
63
60
|
if (done) done();
|
|
64
61
|
return;
|
|
65
62
|
}
|
|
@@ -67,46 +64,45 @@ module.exports = function(RED) {
|
|
|
67
64
|
// Handle context updates
|
|
68
65
|
if (msg.hasOwnProperty("context")) {
|
|
69
66
|
if (!msg.hasOwnProperty("payload")) {
|
|
70
|
-
|
|
67
|
+
utils.setStatusError(node, "missing payload for reset");
|
|
71
68
|
if (done) done();
|
|
72
69
|
return;
|
|
73
70
|
}
|
|
74
71
|
if (msg.context === "reset") {
|
|
75
72
|
if (typeof msg.payload !== "boolean") {
|
|
76
|
-
|
|
73
|
+
utils.setStatusError(node, "invalid reset");
|
|
77
74
|
if (done) done();
|
|
78
75
|
return;
|
|
79
76
|
}
|
|
80
77
|
if (msg.payload === true) {
|
|
81
|
-
node.
|
|
82
|
-
node.
|
|
83
|
-
node.
|
|
84
|
-
node.
|
|
85
|
-
node.
|
|
86
|
-
node.
|
|
87
|
-
node.
|
|
88
|
-
node.
|
|
89
|
-
|
|
78
|
+
node.lastIn = false;
|
|
79
|
+
node.lastEdge = 0;
|
|
80
|
+
node.completeCycle = false;
|
|
81
|
+
node.ppm = 0;
|
|
82
|
+
node.pph = 0;
|
|
83
|
+
node.ppd = 0;
|
|
84
|
+
node.pulseHistory = [];
|
|
85
|
+
node.currentPulseStart = 0;
|
|
86
|
+
utils.setStatusOK(node, "reset");
|
|
90
87
|
}
|
|
91
88
|
if (done) done();
|
|
92
89
|
return;
|
|
93
90
|
} else {
|
|
94
|
-
|
|
91
|
+
utils.setStatusWarn(node, "unknown context");
|
|
95
92
|
if (done) done("Unknown context");
|
|
96
93
|
return;
|
|
97
94
|
}
|
|
98
95
|
}
|
|
99
96
|
|
|
100
97
|
// Validate input payload
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
98
|
+
let inputValue;
|
|
99
|
+
try {
|
|
100
|
+
inputValue = RED.util.getMessageProperty(msg, node.inputProperty);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
inputValue = undefined;
|
|
105
103
|
}
|
|
106
|
-
|
|
107
|
-
const inputValue = msg.payload;
|
|
108
104
|
if (typeof inputValue !== "boolean") {
|
|
109
|
-
|
|
105
|
+
utils.setStatusError(node, "invalid or missing input property");
|
|
110
106
|
if (done) done();
|
|
111
107
|
return;
|
|
112
108
|
}
|
|
@@ -114,18 +110,18 @@ module.exports = function(RED) {
|
|
|
114
110
|
const now = Date.now();
|
|
115
111
|
|
|
116
112
|
// Track pulse edges for duty cycle
|
|
117
|
-
if (inputValue && !node.
|
|
113
|
+
if (inputValue && !node.lastIn) {
|
|
118
114
|
// Rising edge - start new pulse
|
|
119
|
-
node.
|
|
120
|
-
} else if (!inputValue && node.
|
|
115
|
+
node.currentPulseStart = now;
|
|
116
|
+
} else if (!inputValue && node.lastIn) {
|
|
121
117
|
// Falling edge - record completed pulse
|
|
122
|
-
if (node.
|
|
123
|
-
const duration = now - node.
|
|
124
|
-
node.
|
|
125
|
-
start: node.
|
|
118
|
+
if (node.currentPulseStart > 0) {
|
|
119
|
+
const duration = now - node.currentPulseStart;
|
|
120
|
+
node.pulseHistory.push({
|
|
121
|
+
start: node.currentPulseStart,
|
|
126
122
|
duration: duration
|
|
127
123
|
});
|
|
128
|
-
node.
|
|
124
|
+
node.currentPulseStart = 0;
|
|
129
125
|
}
|
|
130
126
|
}
|
|
131
127
|
|
|
@@ -134,21 +130,21 @@ module.exports = function(RED) {
|
|
|
134
130
|
|
|
135
131
|
// Initialize output
|
|
136
132
|
let output = {
|
|
137
|
-
ppm: node.
|
|
138
|
-
pph: node.
|
|
139
|
-
ppd: node.
|
|
133
|
+
ppm: node.ppm,
|
|
134
|
+
pph: node.pph,
|
|
135
|
+
ppd: node.ppd,
|
|
140
136
|
dutyCycle: dutyData.dutyCycle.toFixed(2),
|
|
141
137
|
onTime: dutyData.onTime
|
|
142
138
|
};
|
|
143
139
|
|
|
144
140
|
// Detect rising edge
|
|
145
|
-
if (inputValue && !node.
|
|
141
|
+
if (inputValue && !node.lastIn) {
|
|
146
142
|
// Rising edge: true and lastIn was false
|
|
147
|
-
if (!node.
|
|
148
|
-
node.
|
|
143
|
+
if (!node.completeCycle) {
|
|
144
|
+
node.completeCycle = true;
|
|
149
145
|
} else {
|
|
150
146
|
// Compute period in minutes
|
|
151
|
-
let periodMs = now - node.
|
|
147
|
+
let periodMs = now - node.lastEdge;
|
|
152
148
|
let periodMin = periodMs / 60000;
|
|
153
149
|
if (periodMin > 0.001) {
|
|
154
150
|
// Minimum 0.6ms period (1000 pulses/sec)
|
|
@@ -161,29 +157,23 @@ module.exports = function(RED) {
|
|
|
161
157
|
output.pph = 60000;
|
|
162
158
|
output.ppd = 1440000;
|
|
163
159
|
}
|
|
164
|
-
node.
|
|
165
|
-
node.
|
|
166
|
-
node.
|
|
160
|
+
node.ppm = output.ppm;
|
|
161
|
+
node.pph = output.pph;
|
|
162
|
+
node.ppd = output.ppd;
|
|
167
163
|
}
|
|
168
|
-
node.
|
|
169
|
-
node.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
shape: "dot",
|
|
174
|
-
text: `input: ${inputValue}, ppm: ${output.ppm.toFixed(2)}, pph: ${output.pph.toFixed(2)}, ppd: ${output.ppd.toFixed(2)}, duty: ${output.dutyCycle}%`
|
|
175
|
-
});
|
|
164
|
+
node.lastEdge = now;
|
|
165
|
+
node.completeCycle = true;
|
|
166
|
+
|
|
167
|
+
const edgeText = `input: ${inputValue}, ppm: ${output.ppm.toFixed(2)}, pph: ${output.pph.toFixed(2)}, ppd: ${output.ppd.toFixed(2)}, duty: ${output.dutyCycle}%`;
|
|
168
|
+
utils.setStatusChanged(node, edgeText);
|
|
176
169
|
send({ payload: output });
|
|
177
170
|
} else {
|
|
178
|
-
node.
|
|
179
|
-
|
|
180
|
-
shape: "ring",
|
|
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}%`
|
|
182
|
-
});
|
|
171
|
+
const noEdgeText = `input: ${inputValue}, ppm: ${node.ppm.toFixed(2)}, pph: ${node.pph.toFixed(2)}, ppd: ${node.ppd.toFixed(2)}, duty: ${output.dutyCycle}%`;
|
|
172
|
+
utils.setStatusUnchanged(node, noEdgeText);
|
|
183
173
|
}
|
|
184
174
|
|
|
185
175
|
// Update lastIn
|
|
186
|
-
node.
|
|
176
|
+
node.lastIn = inputValue;
|
|
187
177
|
|
|
188
178
|
if (done) done();
|
|
189
179
|
});
|
package/nodes/global-getter.html
CHANGED
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
|
|
23
23
|
<div class="form-row">
|
|
24
24
|
<label for="node-input-outputProperty"><i class="fa fa-arrow-right"></i> Output</label>
|
|
25
|
-
<input type="text" id="node-input-outputProperty" placeholder="payload"
|
|
25
|
+
<input type="text" id="node-input-outputProperty" placeholder="payload">
|
|
26
|
+
<input type="hidden" id="node-input-outputPropertyType">
|
|
27
|
+
<input type="hidden" id="node-input-dropdownPath">
|
|
26
28
|
</div>
|
|
27
29
|
|
|
28
30
|
<div class="form-row">
|
|
@@ -34,7 +36,9 @@
|
|
|
34
36
|
</div>
|
|
35
37
|
|
|
36
38
|
<div class="form-tips">
|
|
37
|
-
<b>Note:</b>
|
|
39
|
+
<b>Note:</b> Source path may change without breaking this link. 'Reactive' events do not cause disk reads.
|
|
40
|
+
Setter nodes will always write, on change, to its configured store and 'default' to ensure data stays 'in memory' during operation (avoids disk IO hammering).
|
|
41
|
+
'Manual' reads from 'default' store in memory first. Will fallback to 'persistent'.
|
|
38
42
|
</div>
|
|
39
43
|
</script>
|
|
40
44
|
|
|
@@ -46,6 +50,8 @@
|
|
|
46
50
|
name: { value: "" },
|
|
47
51
|
targetNode: { value: "", required: true },
|
|
48
52
|
outputProperty: { value: "payload", required: true },
|
|
53
|
+
outputPropertyType: { value: "msg", required: true },
|
|
54
|
+
dropdownPath: { value: "", required: false },
|
|
49
55
|
updates: { value: "always", required: true },
|
|
50
56
|
detail: {value: "getObject", required: true }
|
|
51
57
|
},
|
|
@@ -76,9 +82,11 @@
|
|
|
76
82
|
if (displayPath.startsWith("#") && displayPath.includes(":")) {
|
|
77
83
|
displayPath = displayPath.substring(displayPath.indexOf(":") + 1);
|
|
78
84
|
}
|
|
85
|
+
|
|
79
86
|
candidateNodes.push({
|
|
80
87
|
value: n.id,
|
|
81
|
-
label: displayPath + (n.name ? ` (${n.name})` : "")
|
|
88
|
+
label: displayPath + (n.name ? ` (${n.name})` : ""),
|
|
89
|
+
path: displayPath
|
|
82
90
|
});
|
|
83
91
|
}
|
|
84
92
|
});
|
|
@@ -88,8 +96,34 @@
|
|
|
88
96
|
$("#node-input-targetNode").typedInput({
|
|
89
97
|
types: [{ value: "target", options: candidateNodes }]
|
|
90
98
|
});
|
|
99
|
+
|
|
100
|
+
$("#node-input-outputProperty").typedInput({
|
|
101
|
+
default: "msg",
|
|
102
|
+
types: ["msg", "flow",
|
|
103
|
+
{
|
|
104
|
+
value: "dropdown",
|
|
105
|
+
options: [
|
|
106
|
+
{ value: "sourceToFlow", label: "Source To Flow"}
|
|
107
|
+
]
|
|
108
|
+
}],
|
|
109
|
+
typeField: "#node-input-outputPropertyType"
|
|
110
|
+
}).typedInput("type", node.outputPropertyType || "msg").typedInput("value", node.outputProperty);
|
|
111
|
+
|
|
112
|
+
function updateOutputValue() {
|
|
113
|
+
const currentType = $("#node-input-outputProperty").typedInput("type");
|
|
114
|
+
|
|
115
|
+
if (currentType === "dropdown" && node.outputProperty === "sourceToFlow") {
|
|
116
|
+
const selectedSourceId = $("#node-input-targetNode").val();
|
|
117
|
+
const selectedOption = candidateNodes.find(opt => opt.value === selectedSourceId);
|
|
118
|
+
|
|
119
|
+
if (selectedOption && selectedOption.path) {
|
|
120
|
+
$("#node-input-dropdownPath").val(selectedOption.path);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
91
124
|
|
|
92
|
-
$("#node-input-
|
|
125
|
+
$("#node-input-targetNode").on("change", updateOutputValue);
|
|
126
|
+
$("#node-input-outputProperty").on("change", updateOutputValue);
|
|
93
127
|
|
|
94
128
|
$("#node-config-find-source").on("click", function() {
|
|
95
129
|
const selectedId = $("#node-input-targetNode").val();
|
|
@@ -101,32 +135,34 @@
|
|
|
101
135
|
</script>
|
|
102
136
|
|
|
103
137
|
<!-- Help Section -->
|
|
104
|
-
<script type="text/markdown" data-help-name="global-
|
|
105
|
-
|
|
138
|
+
<script type="text/markdown" data-help-name="global-getter">
|
|
139
|
+
Retrieve a global variable from a single source location.
|
|
106
140
|
|
|
107
141
|
### Inputs
|
|
108
|
-
|
|
142
|
+
: any : Triggers retrieval of the global variable. Input message is passed through unchanged unless Manual trigger is disabled.
|
|
109
143
|
|
|
110
144
|
### Outputs
|
|
111
|
-
: payload (object) : The
|
|
145
|
+
: payload (object) : The retrieved global variable object with values and metadata.
|
|
112
146
|
|
|
113
147
|
### Details
|
|
114
|
-
Global variables are meant to be retrieved in
|
|
148
|
+
Global variables are meant to be retrieved in multiple places, which necessitates managing the same path in multiple locations.
|
|
115
149
|
|
|
116
|
-
This node allows you to get a global variable anywhere
|
|
150
|
+
This node allows you to get a global variable anywhere by referencing one source node, while automatically supporting renames and deletions without breaking downstream nodes.
|
|
117
151
|
|
|
118
|
-
The
|
|
152
|
+
The Source reference will automatically resolve even if the source global-setter is renamed or moved to a different flow tab.
|
|
119
153
|
|
|
120
|
-
|
|
154
|
+
Trigger mode: Manual (On Input Only) reads from the in-memory 'default' store first, falling back to 'persistent' store. Reactive (On Input & Update) also triggers whenever the source variable is updated, without disk reads.
|
|
121
155
|
|
|
122
156
|
### Status
|
|
123
157
|
- Green (dot): Configuration update
|
|
124
|
-
- Blue (dot): State changed
|
|
158
|
+
- Blue (dot): State changed
|
|
125
159
|
- Blue (ring): State unchanged
|
|
126
160
|
- Red (ring): Error
|
|
127
161
|
- Yellow (ring): Warning
|
|
128
162
|
|
|
129
163
|
### References
|
|
130
|
-
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
164
|
+
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
131
165
|
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
|
132
|
-
</script>
|
|
166
|
+
</script>
|
|
167
|
+
|
|
168
|
+
|
package/nodes/global-getter.js
CHANGED
|
@@ -5,6 +5,7 @@ module.exports = function(RED) {
|
|
|
5
5
|
const node = this;
|
|
6
6
|
node.targetNodeId = config.targetNode;
|
|
7
7
|
node.outputProperty = config.outputProperty || "payload";
|
|
8
|
+
node.dropdownPath = config.dropdownPath || "";
|
|
8
9
|
node.updates = config.updates;
|
|
9
10
|
node.detail = config.detail;
|
|
10
11
|
|
|
@@ -18,7 +19,7 @@ module.exports = function(RED) {
|
|
|
18
19
|
|
|
19
20
|
// --- Output Helper ---
|
|
20
21
|
function sendValue(storedObject, msgToReuse, done) {
|
|
21
|
-
const msg = msgToReuse || {};
|
|
22
|
+
const msg = RED.util.cloneMessage(msgToReuse) || {};
|
|
22
23
|
|
|
23
24
|
if (storedObject !== undefined && storedObject !== null) {
|
|
24
25
|
// Check if this is our custom wrapper object
|
|
@@ -26,15 +27,39 @@ module.exports = function(RED) {
|
|
|
26
27
|
if (node.detail === "getObject") {
|
|
27
28
|
Object.assign(msg, storedObject);
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
+
if (config.outputPropertyType === "flow" || config.outputPropertyType === "dropdown") {
|
|
31
|
+
if (config.outputProperty === "sourceToFlow") {
|
|
32
|
+
node.context().flow.set(node.dropdownPath, storedObject.value);
|
|
33
|
+
} else {
|
|
34
|
+
node.context().flow.set(node.outputProperty, storedObject.value);
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
RED.util.setMessageProperty(msg, node.outputProperty, storedObject.value);
|
|
38
|
+
}
|
|
30
39
|
} else {
|
|
31
40
|
// Legacy/Raw values
|
|
32
|
-
|
|
41
|
+
if (config.outputPropertyType === "flow" || config.outputPropertyType === "dropdown") {
|
|
42
|
+
if (config.outputProperty === "sourceToFlow") {
|
|
43
|
+
node.context().flow.set(node.dropdownPath, storedObject);
|
|
44
|
+
} else {
|
|
45
|
+
node.context().flow.set(node.outputProperty, storedObject);
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
RED.util.setMessageProperty(msg, node.outputProperty, storedObject);
|
|
49
|
+
}
|
|
33
50
|
msg.metadata = { path: setterNode ? setterNode.varName : "unknown", legacy: true };
|
|
34
51
|
}
|
|
35
52
|
|
|
36
|
-
let valDisplay =
|
|
37
|
-
valDisplay
|
|
53
|
+
let valDisplay = storedObject.value;
|
|
54
|
+
if (valDisplay === null) valDisplay = "null";
|
|
55
|
+
else if (valDisplay === undefined) valDisplay = "undefined";
|
|
56
|
+
else if (typeof valDisplay === "object") valDisplay = JSON.stringify(valDisplay);
|
|
57
|
+
else valDisplay = typeof valDisplay === "number" ? valDisplay : valDisplay;
|
|
58
|
+
|
|
59
|
+
// Trim to 64 characters with ellipsis
|
|
60
|
+
if (valDisplay.length > 64) {
|
|
61
|
+
valDisplay = valDisplay.substring(0, 64) + "...";
|
|
62
|
+
}
|
|
38
63
|
|
|
39
64
|
utils.sendSuccess(node, msg, done, `get: ${valDisplay}`, null, "dot");
|
|
40
65
|
} else {
|
|
@@ -48,7 +73,7 @@ module.exports = function(RED) {
|
|
|
48
73
|
|
|
49
74
|
if (setterNode && setterNode.varName && node.updates === 'always') {
|
|
50
75
|
if (updateListener) {
|
|
51
|
-
RED.events.removeListener("bldgblocks
|
|
76
|
+
RED.events.removeListener("bldgblocks:global:value-changed", updateListener);
|
|
52
77
|
}
|
|
53
78
|
|
|
54
79
|
updateListener = function(evt) {
|
|
@@ -58,14 +83,14 @@ module.exports = function(RED) {
|
|
|
58
83
|
}
|
|
59
84
|
};
|
|
60
85
|
|
|
61
|
-
RED.events.on("bldgblocks
|
|
86
|
+
RED.events.on("bldgblocks:global:value-changed", updateListener);
|
|
62
87
|
|
|
63
88
|
if (retryAction) {
|
|
64
89
|
clearInterval(retryAction);
|
|
65
90
|
retryAction = null;
|
|
66
91
|
}
|
|
67
92
|
|
|
68
|
-
|
|
93
|
+
utils.setStatusOK(node, "Connected");
|
|
69
94
|
return true;
|
|
70
95
|
}
|
|
71
96
|
return false;
|
|
@@ -73,12 +98,12 @@ module.exports = function(RED) {
|
|
|
73
98
|
|
|
74
99
|
function startHealthCheck() {
|
|
75
100
|
const check = () => {
|
|
76
|
-
const listeners = RED.events.listeners("bldgblocks
|
|
101
|
+
const listeners = RED.events.listeners("bldgblocks:global:value-changed");
|
|
77
102
|
const hasOurListener = listeners.includes(updateListener);
|
|
78
103
|
if (!hasOurListener) {
|
|
79
104
|
node.warn("Event listener lost, reconnecting...");
|
|
80
105
|
if (establishListener()) {
|
|
81
|
-
|
|
106
|
+
utils.setStatusOK(node, "Reconnected");
|
|
82
107
|
}
|
|
83
108
|
}
|
|
84
109
|
setTimeout(check, 30000);
|
|
@@ -110,8 +135,13 @@ module.exports = function(RED) {
|
|
|
110
135
|
setterNode ??= RED.nodes.getNode(node.targetNodeId);
|
|
111
136
|
|
|
112
137
|
if (setterNode && setterNode.varName) {
|
|
113
|
-
// Async Get
|
|
114
|
-
|
|
138
|
+
// Async Get - required default store to keep values in memory for polled getter nodes.
|
|
139
|
+
// 'persistent' for cross reboot storage.
|
|
140
|
+
let storedObject = await utils.getGlobalState(node, setterNode.varName, 'default');
|
|
141
|
+
if (!storedObject) {
|
|
142
|
+
// Fallback to persistent store if not found in default. Should not happen normally.
|
|
143
|
+
storedObject = await utils.getGlobalState(node, setterNode.varName, setterNode.storeName);
|
|
144
|
+
}
|
|
115
145
|
sendValue(storedObject, msg, done);
|
|
116
146
|
} else {
|
|
117
147
|
node.warn("Source node not found or not configured.");
|
|
@@ -133,7 +163,7 @@ module.exports = function(RED) {
|
|
|
133
163
|
if (healthCheckAction) clearInterval(healthCheckAction);
|
|
134
164
|
if (retryAction) clearInterval(retryAction);
|
|
135
165
|
if (removed && updateListener) {
|
|
136
|
-
RED.events.removeListener("bldgblocks
|
|
166
|
+
RED.events.removeListener("bldgblocks:global:value-changed", updateListener);
|
|
137
167
|
}
|
|
138
168
|
done();
|
|
139
169
|
});
|
package/nodes/global-setter.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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"
|
|
8
|
+
<input type="text" id="node-input-path" placeholder="furnace/outputs/heat">
|
|
9
9
|
</div>
|
|
10
10
|
<div class="form-row">
|
|
11
11
|
<label for="node-input-property"><i class="fa fa-ellipsis-h"></i> Input</label>
|