@bldgblocks/node-red-contrib-control 0.1.37 → 0.2.0
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/LICENSE.md +38 -5
- package/README.md +10 -0
- package/nodes/accumulate-block.html +3 -3
- package/nodes/alarm-collector.html +11 -0
- package/nodes/alarm-collector.js +31 -5
- package/nodes/alarm-config.html +11 -6
- package/nodes/alarm-config.js +34 -35
- package/nodes/alarm-service.js +2 -2
- package/nodes/and-block.js +1 -1
- package/nodes/boolean-switch-block.html +27 -14
- package/nodes/boolean-switch-block.js +22 -12
- package/nodes/call-status-block.html +83 -56
- package/nodes/call-status-block.js +335 -248
- package/nodes/changeover-block.html +30 -31
- package/nodes/changeover-block.js +287 -389
- package/nodes/contextual-label-block.js +3 -3
- package/nodes/delay-block.js +74 -13
- package/nodes/global-getter.html +173 -12
- package/nodes/global-getter.js +29 -14
- package/nodes/global-setter.html +37 -0
- package/nodes/global-setter.js +96 -14
- package/nodes/history-buffer.js +32 -27
- package/nodes/history-collector.html +3 -1
- package/nodes/history-collector.js +4 -4
- package/nodes/history-config.html +8 -2
- package/nodes/network-point-read.js +6 -1
- package/nodes/network-point-register.html +1 -1
- package/nodes/network-service-bridge.js +43 -11
- package/nodes/network-service-registry.html +236 -27
- package/nodes/network-service-registry.js +1 -1
- package/nodes/or-block.js +1 -1
- package/nodes/priority-block.js +1 -1
- package/nodes/tstat-block.html +34 -79
- package/nodes/tstat-block.js +223 -345
- package/nodes/utils.js +1 -1
- package/package.json +90 -75
|
@@ -8,7 +8,7 @@ module.exports = function(RED) {
|
|
|
8
8
|
node.contextPropertyName = config.contextPropertyName || "in1";
|
|
9
9
|
node.removeLabel = config.removeLabel || false;
|
|
10
10
|
|
|
11
|
-
utils.setStatusOK(node,
|
|
11
|
+
utils.setStatusOK(node, node.removeLabel ? "remove" : `set -> ${node.contextPropertyName}`);
|
|
12
12
|
|
|
13
13
|
node.on("input", function(msg, send, done) {
|
|
14
14
|
send = send || function() { node.send.apply(node, arguments); };
|
|
@@ -23,10 +23,10 @@ module.exports = function(RED) {
|
|
|
23
23
|
// Set or remove context property
|
|
24
24
|
if (node.removeLabel) {
|
|
25
25
|
delete msg.context;
|
|
26
|
-
utils.setStatusChanged(node,
|
|
26
|
+
utils.setStatusChanged(node, `${msg.payload} -> removed`);
|
|
27
27
|
} else {
|
|
28
28
|
msg.context = node.contextPropertyName;
|
|
29
|
-
utils.setStatusChanged(node,
|
|
29
|
+
utils.setStatusChanged(node, `${msg.payload} -> ${node.contextPropertyName}`);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
send(msg);
|
package/nodes/delay-block.js
CHANGED
|
@@ -5,14 +5,19 @@ module.exports = function(RED) {
|
|
|
5
5
|
RED.nodes.createNode(this, config);
|
|
6
6
|
const node = this;
|
|
7
7
|
|
|
8
|
+
// Unit multipliers (constant, computed once)
|
|
9
|
+
const delayOnMultiplier = config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1;
|
|
10
|
+
const delayOffMultiplier = config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1;
|
|
11
|
+
|
|
8
12
|
// Initialize state
|
|
9
13
|
node.name = config.name;
|
|
10
14
|
node.state = false;
|
|
11
15
|
node.desired = false;
|
|
12
|
-
node.delayOn = parseFloat(config.delayOn) *
|
|
13
|
-
node.delayOff = parseFloat(config.delayOff) *
|
|
16
|
+
node.delayOn = parseFloat(config.delayOn) * delayOnMultiplier;
|
|
17
|
+
node.delayOff = parseFloat(config.delayOff) * delayOffMultiplier;
|
|
14
18
|
|
|
15
19
|
let timeoutId = null;
|
|
20
|
+
let pendingDone = null; // Track deferred done() for in-flight timers
|
|
16
21
|
node.isBusy = false;
|
|
17
22
|
|
|
18
23
|
node.on("input", async function(msg, send, done) {
|
|
@@ -46,21 +51,21 @@ module.exports = function(RED) {
|
|
|
46
51
|
utils.requiresEvaluation(config.delayOnType)
|
|
47
52
|
? utils.evaluateNodeProperty(config.delayOn, config.delayOnType, node, msg)
|
|
48
53
|
.then(val => parseFloat(val))
|
|
49
|
-
: Promise.resolve(
|
|
54
|
+
: Promise.resolve(parseFloat(config.delayOn)),
|
|
50
55
|
);
|
|
51
56
|
|
|
52
57
|
evaluations.push(
|
|
53
58
|
utils.requiresEvaluation(config.delayOffType)
|
|
54
59
|
? utils.evaluateNodeProperty(config.delayOff, config.delayOffType, node, msg)
|
|
55
60
|
.then(val => parseFloat(val))
|
|
56
|
-
: Promise.resolve(
|
|
61
|
+
: Promise.resolve(parseFloat(config.delayOff)),
|
|
57
62
|
);
|
|
58
63
|
|
|
59
64
|
const results = await Promise.all(evaluations);
|
|
60
65
|
|
|
61
|
-
// Update runtime with evaluated values
|
|
62
|
-
if (!isNaN(results[0])) node.delayOn = results[0] *
|
|
63
|
-
if (!isNaN(results[1])) node.delayOff = results[1] *
|
|
66
|
+
// Update runtime with evaluated values (apply unit multiplier to raw config value)
|
|
67
|
+
if (!isNaN(results[0])) node.delayOn = results[0] * delayOnMultiplier;
|
|
68
|
+
if (!isNaN(results[1])) node.delayOff = results[1] * delayOffMultiplier;
|
|
64
69
|
} catch (err) {
|
|
65
70
|
node.error(`Error evaluating properties: ${err.message}`);
|
|
66
71
|
if (done) done();
|
|
@@ -98,7 +103,13 @@ module.exports = function(RED) {
|
|
|
98
103
|
clearTimeout(timeoutId);
|
|
99
104
|
timeoutId = null;
|
|
100
105
|
}
|
|
106
|
+
// Complete any deferred done from a cancelled timer
|
|
107
|
+
if (pendingDone) {
|
|
108
|
+
pendingDone();
|
|
109
|
+
pendingDone = null;
|
|
110
|
+
}
|
|
101
111
|
node.state = false;
|
|
112
|
+
node.desired = false;
|
|
102
113
|
utils.setStatusOK(node, "reset");
|
|
103
114
|
}
|
|
104
115
|
if (done) done();
|
|
@@ -160,44 +171,89 @@ module.exports = function(RED) {
|
|
|
160
171
|
|
|
161
172
|
if (!node.state && inputValue === true) {
|
|
162
173
|
if (node.desired) {
|
|
174
|
+
// Already awaiting true, ignore duplicate
|
|
163
175
|
if (done) done();
|
|
164
176
|
return;
|
|
165
177
|
}
|
|
166
178
|
if (timeoutId) {
|
|
167
179
|
clearTimeout(timeoutId);
|
|
180
|
+
timeoutId = null;
|
|
181
|
+
}
|
|
182
|
+
// Complete any prior deferred done before starting new timer
|
|
183
|
+
if (pendingDone) {
|
|
184
|
+
pendingDone();
|
|
185
|
+
pendingDone = null;
|
|
168
186
|
}
|
|
169
187
|
utils.setStatusUnchanged(node, "awaiting true");
|
|
170
188
|
node.desired = true;
|
|
189
|
+
|
|
190
|
+
// Clone msg for the timer callback so we don't hold the original
|
|
191
|
+
const delayedMsg = RED.util.cloneMessage(msg);
|
|
192
|
+
// Defer done — this message isn't complete until the timer fires or is cancelled
|
|
193
|
+
pendingDone = done;
|
|
194
|
+
|
|
171
195
|
timeoutId = setTimeout(() => {
|
|
172
196
|
node.state = true;
|
|
173
|
-
|
|
174
|
-
delete
|
|
197
|
+
delayedMsg.payload = true;
|
|
198
|
+
delete delayedMsg.context;
|
|
175
199
|
utils.setStatusChanged(node, "in: true, out: true");
|
|
176
|
-
send(
|
|
200
|
+
send(delayedMsg);
|
|
177
201
|
timeoutId = null;
|
|
202
|
+
if (pendingDone) {
|
|
203
|
+
pendingDone();
|
|
204
|
+
pendingDone = null;
|
|
205
|
+
}
|
|
178
206
|
}, node.delayOn);
|
|
207
|
+
|
|
208
|
+
// Don't call done() here — it's deferred to the timer callback
|
|
209
|
+
return;
|
|
179
210
|
} else if (node.state && inputValue === false) {
|
|
180
211
|
if (node.desired === false) {
|
|
212
|
+
// Already awaiting false, ignore duplicate
|
|
181
213
|
if (done) done();
|
|
182
214
|
return;
|
|
183
215
|
}
|
|
184
216
|
if (timeoutId) {
|
|
185
217
|
clearTimeout(timeoutId);
|
|
218
|
+
timeoutId = null;
|
|
219
|
+
}
|
|
220
|
+
// Complete any prior deferred done before starting new timer
|
|
221
|
+
if (pendingDone) {
|
|
222
|
+
pendingDone();
|
|
223
|
+
pendingDone = null;
|
|
186
224
|
}
|
|
187
225
|
utils.setStatusUnchanged(node, "awaiting false");
|
|
188
226
|
node.desired = false;
|
|
227
|
+
|
|
228
|
+
// Clone msg for the timer callback so we don't hold the original
|
|
229
|
+
const delayedMsg = RED.util.cloneMessage(msg);
|
|
230
|
+
// Defer done — this message isn't complete until the timer fires or is cancelled
|
|
231
|
+
pendingDone = done;
|
|
232
|
+
|
|
189
233
|
timeoutId = setTimeout(() => {
|
|
190
234
|
node.state = false;
|
|
191
|
-
|
|
192
|
-
delete
|
|
235
|
+
delayedMsg.payload = false;
|
|
236
|
+
delete delayedMsg.context;
|
|
193
237
|
utils.setStatusChanged(node, "in: false, out: false");
|
|
194
|
-
send(
|
|
238
|
+
send(delayedMsg);
|
|
195
239
|
timeoutId = null;
|
|
240
|
+
if (pendingDone) {
|
|
241
|
+
pendingDone();
|
|
242
|
+
pendingDone = null;
|
|
243
|
+
}
|
|
196
244
|
}, node.delayOff);
|
|
245
|
+
|
|
246
|
+
// Don't call done() here — it's deferred to the timer callback
|
|
247
|
+
return;
|
|
197
248
|
} else {
|
|
198
249
|
if (timeoutId) {
|
|
199
250
|
clearTimeout(timeoutId);
|
|
200
251
|
timeoutId = null;
|
|
252
|
+
// Complete deferred done from the cancelled timer's message
|
|
253
|
+
if (pendingDone) {
|
|
254
|
+
pendingDone();
|
|
255
|
+
pendingDone = null;
|
|
256
|
+
}
|
|
201
257
|
utils.setStatusUnchanged(node, `canceled awaiting ${node.state}`);
|
|
202
258
|
} else {
|
|
203
259
|
utils.setStatusUnchanged(node, "no change");
|
|
@@ -217,6 +273,11 @@ module.exports = function(RED) {
|
|
|
217
273
|
clearTimeout(timeoutId);
|
|
218
274
|
timeoutId = null;
|
|
219
275
|
}
|
|
276
|
+
// Complete any deferred done on shutdown
|
|
277
|
+
if (pendingDone) {
|
|
278
|
+
pendingDone();
|
|
279
|
+
pendingDone = null;
|
|
280
|
+
}
|
|
220
281
|
done();
|
|
221
282
|
});
|
|
222
283
|
}
|
package/nodes/global-getter.html
CHANGED
|
@@ -4,12 +4,22 @@
|
|
|
4
4
|
<input type="text" id="node-input-name" placeholder="Name">
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
|
-
<div class="form-row">
|
|
8
|
-
<label
|
|
9
|
-
<input type="
|
|
10
|
-
<
|
|
11
|
-
<i class="fa fa-
|
|
12
|
-
</
|
|
7
|
+
<div class="form-row" style="margin-bottom: 0px;">
|
|
8
|
+
<label><i class="fa fa-crosshairs"></i> Source</label>
|
|
9
|
+
<input type="hidden" id="node-input-targetNode">
|
|
10
|
+
<span id="node-input-source-toggle" style="float: right; cursor: pointer; padding: 2px 6px; font-size: 14px;" title="Collapse/Expand source list">
|
|
11
|
+
<i class="fa fa-caret-down"></i>
|
|
12
|
+
</span>
|
|
13
|
+
</div>
|
|
14
|
+
<div id="node-input-source-summary" style="display: none; padding: 4px 8px; margin-bottom: 5px; border: 1px solid #ddd; border-radius: 4px; background: transparent; color: #888; font-style: italic; cursor: pointer;" title="Click to expand">No source selected</div>
|
|
15
|
+
<div id="node-input-source-expanded">
|
|
16
|
+
<div style="position: relative; height: 30px; text-align: right;">
|
|
17
|
+
<div style="display: inline-block; vertical-align: middle;"><input type="text" id="node-input-source-filter"></div>
|
|
18
|
+
<button id="node-config-find-source" class="editor-button" style="width: 28px; height: 28px; vertical-align: middle; margin-left: 4px; display: inline-flex; align-items: center; justify-content: center;" title="Reveal Source Node">
|
|
19
|
+
<i class="fa fa-search"></i>
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="form-row node-input-source-row"></div>
|
|
13
23
|
</div>
|
|
14
24
|
|
|
15
25
|
<div class="form-row">
|
|
@@ -74,6 +84,7 @@
|
|
|
74
84
|
paletteLabel: "global get",
|
|
75
85
|
oneditprepare: function() {
|
|
76
86
|
const node = this;
|
|
87
|
+
let initComplete = false;
|
|
77
88
|
|
|
78
89
|
let candidateNodes = [];
|
|
79
90
|
RED.nodes.eachNode(function(n) {
|
|
@@ -93,10 +104,137 @@
|
|
|
93
104
|
|
|
94
105
|
candidateNodes.sort((a, b) => a.label.localeCompare(b.label));
|
|
95
106
|
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
// Build treeList grouped by flow tab (like link-in node)
|
|
108
|
+
const sourceList = $("<div>").css({width: "100%", height: "100%"}).appendTo(".node-input-source-row")
|
|
109
|
+
.treeList({autoSelect: false});
|
|
110
|
+
|
|
111
|
+
const flowMap = {};
|
|
112
|
+
const flows = [];
|
|
113
|
+
RED.nodes.eachWorkspace(function(ws) {
|
|
114
|
+
flowMap[ws.id] = {
|
|
115
|
+
id: ws.id,
|
|
116
|
+
class: 'red-ui-palette-header',
|
|
117
|
+
label: (ws.label || ws.id) + (node.z === ws.id ? " *" : ""),
|
|
118
|
+
expanded: true,
|
|
119
|
+
children: []
|
|
120
|
+
};
|
|
121
|
+
flows.push(flowMap[ws.id]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let candidateNodesCount = 0;
|
|
125
|
+
candidateNodes.forEach(function(opt) {
|
|
126
|
+
if (flowMap[RED.nodes.node(opt.value)?.z]) {
|
|
127
|
+
const targetZ = RED.nodes.node(opt.value).z;
|
|
128
|
+
flowMap[targetZ].children.push({
|
|
129
|
+
id: opt.value,
|
|
130
|
+
node: RED.nodes.node(opt.value),
|
|
131
|
+
label: opt.label,
|
|
132
|
+
radio: true,
|
|
133
|
+
radioGroup: "source-select",
|
|
134
|
+
selected: opt.value === node.targetNode
|
|
135
|
+
});
|
|
136
|
+
candidateNodesCount++;
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const flowsFiltered = flows.filter(function(f) { return f.children.length > 0; });
|
|
141
|
+
sourceList.treeList('data', flowsFiltered);
|
|
142
|
+
|
|
143
|
+
// Show current node's flow tab
|
|
144
|
+
setTimeout(function() { sourceList.treeList('show', node.z); }, 100);
|
|
145
|
+
|
|
146
|
+
// searchBox filter (same pattern as link-in)
|
|
147
|
+
const search = $("#node-input-source-filter").searchBox({
|
|
148
|
+
style: "compact",
|
|
149
|
+
delay: 300,
|
|
150
|
+
change: function() {
|
|
151
|
+
const val = $(this).val().trim().toLowerCase();
|
|
152
|
+
if (val === "") {
|
|
153
|
+
sourceList.treeList("filter", null);
|
|
154
|
+
search.searchBox("count", "");
|
|
155
|
+
} else {
|
|
156
|
+
const count = sourceList.treeList("filter", function(item) {
|
|
157
|
+
return item.label && item.label.toLowerCase().indexOf(val) > -1;
|
|
158
|
+
});
|
|
159
|
+
search.searchBox("count", count + " / " + candidateNodesCount);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
98
162
|
});
|
|
99
163
|
|
|
164
|
+
// Helper to get the currently selected source from the treeList
|
|
165
|
+
function getSelectedSourceId() {
|
|
166
|
+
const items = sourceList.treeList('data');
|
|
167
|
+
for (let f = 0; f < items.length; f++) {
|
|
168
|
+
const children = items[f].children || [];
|
|
169
|
+
for (let c = 0; c < children.length; c++) {
|
|
170
|
+
if (children[c].selected) return children[c].id;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// --- Collapse / Expand toggle ---
|
|
177
|
+
const toggleBtn = $("#node-input-source-toggle");
|
|
178
|
+
const expandedSection = $("#node-input-source-expanded");
|
|
179
|
+
const summaryBar = $("#node-input-source-summary");
|
|
180
|
+
let sourceExpanded = true;
|
|
181
|
+
|
|
182
|
+
function getSelectedLabel() {
|
|
183
|
+
const id = getSelectedSourceId();
|
|
184
|
+
if (!id) return "No source selected";
|
|
185
|
+
const opt = candidateNodes.find(o => o.value === id);
|
|
186
|
+
return opt ? opt.label : "Unknown";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function resizeSourceList() {
|
|
190
|
+
if (!sourceExpanded) return;
|
|
191
|
+
var dialogForm = $("#dialog-form");
|
|
192
|
+
var height = dialogForm.height();
|
|
193
|
+
dialogForm.children().each(function() {
|
|
194
|
+
var $el = $(this);
|
|
195
|
+
if ($el.attr("id") === "node-input-source-expanded") {
|
|
196
|
+
// Subtract the search/button bar inside expanded, but not the source-row itself
|
|
197
|
+
$el.children().each(function() {
|
|
198
|
+
if (!$(this).hasClass("node-input-source-row")) {
|
|
199
|
+
height -= $(this).outerHeight(true);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
} else {
|
|
203
|
+
height -= $el.outerHeight(true);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
$(".node-input-source-row").css("height", Math.max(200, height) + "px");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function setSourceExpanded(expanded) {
|
|
210
|
+
sourceExpanded = expanded;
|
|
211
|
+
if (expanded) {
|
|
212
|
+
summaryBar.hide();
|
|
213
|
+
expandedSection.show();
|
|
214
|
+
toggleBtn.find("i").removeClass("fa-caret-right").addClass("fa-caret-down");
|
|
215
|
+
setTimeout(resizeSourceList, 50);
|
|
216
|
+
} else {
|
|
217
|
+
expandedSection.hide();
|
|
218
|
+
summaryBar.text(getSelectedLabel()).show();
|
|
219
|
+
toggleBtn.find("i").removeClass("fa-caret-down").addClass("fa-caret-right");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
toggleBtn.on("click", function() { setSourceExpanded(!sourceExpanded); });
|
|
224
|
+
summaryBar.on("click", function() { setSourceExpanded(true); });
|
|
225
|
+
|
|
226
|
+
// Update summary text and hidden input when selection changes
|
|
227
|
+
sourceList.on('treelistselect', function() {
|
|
228
|
+
if (!initComplete) return;
|
|
229
|
+
$("#node-input-targetNode").val(getSelectedSourceId());
|
|
230
|
+
if (!sourceExpanded) summaryBar.text(getSelectedLabel());
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Start collapsed if a source is already selected
|
|
234
|
+
if (node.targetNode) {
|
|
235
|
+
setSourceExpanded(false);
|
|
236
|
+
}
|
|
237
|
+
|
|
100
238
|
$("#node-input-outputProperty").typedInput({
|
|
101
239
|
default: "msg",
|
|
102
240
|
types: ["msg", "flow",
|
|
@@ -110,10 +248,12 @@
|
|
|
110
248
|
}).typedInput("type", node.outputPropertyType || "msg").typedInput("value", node.outputProperty);
|
|
111
249
|
|
|
112
250
|
function updateOutputValue() {
|
|
251
|
+
if (!initComplete) return;
|
|
113
252
|
const currentType = $("#node-input-outputProperty").typedInput("type");
|
|
253
|
+
const currentValue = $("#node-input-outputProperty").typedInput("value");
|
|
114
254
|
|
|
115
|
-
if (currentType === "dropdown" &&
|
|
116
|
-
const selectedSourceId =
|
|
255
|
+
if (currentType === "dropdown" && currentValue === "sourceToFlow") {
|
|
256
|
+
const selectedSourceId = getSelectedSourceId();
|
|
117
257
|
const selectedOption = candidateNodes.find(opt => opt.value === selectedSourceId);
|
|
118
258
|
|
|
119
259
|
if (selectedOption && selectedOption.path) {
|
|
@@ -122,14 +262,35 @@
|
|
|
122
262
|
}
|
|
123
263
|
}
|
|
124
264
|
|
|
125
|
-
|
|
265
|
+
sourceList.on('treelistselect', updateOutputValue);
|
|
126
266
|
$("#node-input-outputProperty").on("change", updateOutputValue);
|
|
127
267
|
|
|
128
268
|
$("#node-config-find-source").on("click", function() {
|
|
129
|
-
const selectedId =
|
|
269
|
+
const selectedId = getSelectedSourceId();
|
|
130
270
|
if (selectedId) { RED.view.reveal(selectedId); }
|
|
131
271
|
else { RED.notify("Please select a source node first.", "warning"); }
|
|
132
272
|
});
|
|
273
|
+
|
|
274
|
+
// Mark init complete - all event handlers now active
|
|
275
|
+
setTimeout(function() { initComplete = true; }, 200);
|
|
276
|
+
},
|
|
277
|
+
oneditresize: function(size) {
|
|
278
|
+
if ($("#node-input-source-expanded").is(":hidden")) return;
|
|
279
|
+
var dialogForm = $("#dialog-form");
|
|
280
|
+
var height = dialogForm.height();
|
|
281
|
+
dialogForm.children().each(function() {
|
|
282
|
+
var $el = $(this);
|
|
283
|
+
if ($el.attr("id") === "node-input-source-expanded") {
|
|
284
|
+
$el.children().each(function() {
|
|
285
|
+
if (!$(this).hasClass("node-input-source-row")) {
|
|
286
|
+
height -= $(this).outerHeight(true);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
} else {
|
|
290
|
+
height -= $el.outerHeight(true);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
$(".node-input-source-row").css("height", Math.max(200, height) + "px");
|
|
133
294
|
}
|
|
134
295
|
});
|
|
135
296
|
</script>
|
package/nodes/global-getter.js
CHANGED
|
@@ -10,10 +10,11 @@ module.exports = function(RED) {
|
|
|
10
10
|
node.detail = config.detail;
|
|
11
11
|
|
|
12
12
|
let setterNode = null;
|
|
13
|
-
let
|
|
14
|
-
let
|
|
13
|
+
let retryTimeout = null;
|
|
14
|
+
let healthCheckTimeout = null;
|
|
15
15
|
let updateListener = null;
|
|
16
16
|
let retryCount = 0;
|
|
17
|
+
let closed = false;
|
|
17
18
|
const retryDelays = [0, 100, 500, 1000, 2000, 4000, 8000, 16000];
|
|
18
19
|
const maxRetries = retryDelays.length - 1;
|
|
19
20
|
|
|
@@ -85,9 +86,9 @@ module.exports = function(RED) {
|
|
|
85
86
|
|
|
86
87
|
RED.events.on("bldgblocks:global:value-changed", updateListener);
|
|
87
88
|
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
if (retryTimeout) {
|
|
90
|
+
clearTimeout(retryTimeout);
|
|
91
|
+
retryTimeout = null;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
utils.setStatusOK(node, "Connected");
|
|
@@ -98,6 +99,11 @@ module.exports = function(RED) {
|
|
|
98
99
|
|
|
99
100
|
function startHealthCheck() {
|
|
100
101
|
const check = () => {
|
|
102
|
+
if (closed) return;
|
|
103
|
+
if (!updateListener) {
|
|
104
|
+
healthCheckTimeout = setTimeout(check, 30000);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
101
107
|
const listeners = RED.events.listeners("bldgblocks:global:value-changed");
|
|
102
108
|
const hasOurListener = listeners.includes(updateListener);
|
|
103
109
|
if (!hasOurListener) {
|
|
@@ -106,13 +112,14 @@ module.exports = function(RED) {
|
|
|
106
112
|
utils.setStatusOK(node, "Reconnected");
|
|
107
113
|
}
|
|
108
114
|
}
|
|
109
|
-
setTimeout(check, 30000);
|
|
115
|
+
healthCheckTimeout = setTimeout(check, 30000);
|
|
110
116
|
};
|
|
111
|
-
setTimeout(check, 30000);
|
|
117
|
+
healthCheckTimeout = setTimeout(check, 30000);
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
function subscribeWithRetry() {
|
|
115
|
-
|
|
121
|
+
function attempt() {
|
|
122
|
+
if (closed) return;
|
|
116
123
|
if (retryCount >= maxRetries) {
|
|
117
124
|
utils.sendError(node, null, null, "Connection failed");
|
|
118
125
|
return;
|
|
@@ -122,9 +129,9 @@ module.exports = function(RED) {
|
|
|
122
129
|
return;
|
|
123
130
|
}
|
|
124
131
|
retryCount++;
|
|
125
|
-
setTimeout(
|
|
126
|
-
}
|
|
127
|
-
setTimeout(
|
|
132
|
+
retryTimeout = setTimeout(attempt, retryDelays[Math.min(retryCount, maxRetries - 1)]);
|
|
133
|
+
}
|
|
134
|
+
retryTimeout = setTimeout(attempt, retryDelays[0]);
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
// --- INPUT HANDLER ---
|
|
@@ -160,10 +167,18 @@ module.exports = function(RED) {
|
|
|
160
167
|
}
|
|
161
168
|
|
|
162
169
|
node.on('close', function(removed, done) {
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
|
|
170
|
+
closed = true;
|
|
171
|
+
if (healthCheckTimeout) {
|
|
172
|
+
clearTimeout(healthCheckTimeout);
|
|
173
|
+
healthCheckTimeout = null;
|
|
174
|
+
}
|
|
175
|
+
if (retryTimeout) {
|
|
176
|
+
clearTimeout(retryTimeout);
|
|
177
|
+
retryTimeout = null;
|
|
178
|
+
}
|
|
179
|
+
if (updateListener) {
|
|
166
180
|
RED.events.removeListener("bldgblocks:global:value-changed", updateListener);
|
|
181
|
+
updateListener = null;
|
|
167
182
|
}
|
|
168
183
|
done();
|
|
169
184
|
});
|
package/nodes/global-setter.html
CHANGED
|
@@ -25,6 +25,15 @@
|
|
|
25
25
|
<input type="hidden" id="node-input-defaultValueType">
|
|
26
26
|
</div>
|
|
27
27
|
|
|
28
|
+
<hr>
|
|
29
|
+
|
|
30
|
+
<div class="form-row">
|
|
31
|
+
<label> </label>
|
|
32
|
+
<button type="button" id="node-btn-clear-priorities" class="editor-button" style="width: calc(70% - 3px);" title="Clear all 16 priority slots on the live node (does not affect the default value)">
|
|
33
|
+
<i class="fa fa-eraser"></i> Clear All Priorities
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
28
37
|
<div class="form-tips">
|
|
29
38
|
<b>Note:</b> This node writes to the selected <b>Priority</b>, manually, by msg or flow. The actual Global Variable value will be the highest active priority.
|
|
30
39
|
</div>
|
|
@@ -97,6 +106,30 @@
|
|
|
97
106
|
}, "msg", "flow"],
|
|
98
107
|
typeField: "#node-input-writePriorityType"
|
|
99
108
|
}).typedInput("type", node.writePriorityType).typedInput("value", node.writePriority);
|
|
109
|
+
|
|
110
|
+
// Clear Priorities button — calls the admin endpoint on the live runtime node
|
|
111
|
+
$("#node-btn-clear-priorities").on("click", function() {
|
|
112
|
+
const btn = $(this);
|
|
113
|
+
if (!node.id) {
|
|
114
|
+
RED.notify("Node must be deployed before clearing priorities.", "warning");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
btn.prop("disabled", true).find("i").removeClass("fa-eraser").addClass("fa-spinner fa-spin");
|
|
118
|
+
$.ajax({
|
|
119
|
+
url: "global-setter/" + node.id + "/clear-priorities",
|
|
120
|
+
type: "POST",
|
|
121
|
+
success: function(data) {
|
|
122
|
+
RED.notify("Priorities cleared — active: " + (data.activePriority || "default") + ":" + data.value, "success");
|
|
123
|
+
},
|
|
124
|
+
error: function(jqXHR) {
|
|
125
|
+
const errMsg = jqXHR.responseJSON ? jqXHR.responseJSON.error : "Unknown error";
|
|
126
|
+
RED.notify("Failed to clear priorities: " + errMsg, "error");
|
|
127
|
+
},
|
|
128
|
+
complete: function() {
|
|
129
|
+
btn.prop("disabled", false).find("i").removeClass("fa-spinner fa-spin").addClass("fa-eraser");
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
100
133
|
}
|
|
101
134
|
});
|
|
102
135
|
</script>
|
|
@@ -108,6 +141,8 @@ Manage a global variable in a repeatable way.
|
|
|
108
141
|
### Inputs
|
|
109
142
|
: payload (any) : Input payload is passed through unchanged.
|
|
110
143
|
: property (string) : The input property where the value is taken from.
|
|
144
|
+
: priority (number|string) : _Optional_. Overrides the configured Priority at runtime. Accepts `1`–`16` or `"default"`. For example, `msg.priority = 8` writes to priority slot 8.
|
|
145
|
+
: context (string) : _Optional_. Tagged-input priority routing (matches priority-block conventions). Accepts `"priority1"`–`"priority16"`, `"default"`, or `"reload"`. When both `msg.priority` and `msg.context` are present, `msg.priority` takes precedence.
|
|
111
146
|
: units (string) : The units associated with the value, if any. Also supports nested units at `msg.<inputProperty>.units`.
|
|
112
147
|
|
|
113
148
|
### Outputs
|
|
@@ -120,6 +155,8 @@ This node allows you to set a global variable in one place, and retrieve it else
|
|
|
120
155
|
|
|
121
156
|
When this node is deleted or the flow is redeployed, it will automatically remove (prune) the variable from the selected Context Store.
|
|
122
157
|
|
|
158
|
+
**Clear All Priorities** button (in editor): Resets all 16 priority slots to `null` on the live running node. The default value is preserved. The active value recalculates to the highest remaining priority (or falls back to default). The node must be deployed first.
|
|
159
|
+
|
|
123
160
|
### Status
|
|
124
161
|
- Green (dot): Configuration update
|
|
125
162
|
- Blue (dot): State changed
|