@bldgblocks/node-red-contrib-control 0.2.1 → 0.2.3
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/alarm-collector.html +2 -2
- package/nodes/alarm-collector.js +33 -50
- package/nodes/alarm-service.js +1 -1
- package/nodes/boolean-to-number-block.html +13 -4
- package/nodes/boolean-to-number-block.js +6 -2
- package/nodes/call-status-block.js +29 -3
- package/nodes/changeover-block.html +31 -25
- package/nodes/changeover-block.js +26 -7
- package/nodes/comment-block.html +48 -9
- package/nodes/comment-block.js +65 -6
- package/nodes/contextual-label-block.js +3 -2
- package/nodes/convert-block.js +1 -1
- package/nodes/edge-block.html +6 -3
- package/nodes/edge-block.js +4 -3
- package/nodes/enum-switch-block.html +1 -1
- package/nodes/global-setter.html +18 -11
- package/nodes/global-setter.js +71 -53
- package/nodes/hysteresis-block.js +1 -1
- package/nodes/interpolate-block.js +2 -2
- package/nodes/negate-block.js +2 -2
- package/nodes/network-point-register.js +11 -2
- package/nodes/network-service-registry.html +25 -3
- package/nodes/network-service-write.js +7 -11
- package/nodes/on-change-block.js +1 -8
- package/nodes/priority-block.js +6 -6
- package/nodes/rate-limit-block.js +6 -6
- package/nodes/round-block.js +1 -1
- package/nodes/scale-range-block.js +1 -1
- package/nodes/tstat-block.html +21 -13
- package/nodes/tstat-block.js +32 -1
- package/nodes/units-block.js +1 -1
- package/nodes/utils.js +11 -1
- package/package.json +1 -1
package/nodes/comment-block.js
CHANGED
|
@@ -9,14 +9,26 @@ module.exports = function(RED) {
|
|
|
9
9
|
node.name = config.name;
|
|
10
10
|
node.comment = config.comment || "";
|
|
11
11
|
node.statusDisplay = config.statusDisplay;
|
|
12
|
+
node.statusProperty = config.statusProperty || "";
|
|
13
|
+
node.statusPropertyType = config.statusPropertyType || "msg";
|
|
14
|
+
|
|
15
|
+
// Pre-compile JSONata expression if configured
|
|
16
|
+
let jsonataExpr = null;
|
|
17
|
+
if (node.statusPropertyType === "jsonata" && node.statusProperty) {
|
|
18
|
+
try {
|
|
19
|
+
jsonataExpr = RED.util.prepareJSONataExpression(node.statusProperty, node);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
node.error(`Invalid JSONata expression: ${err.message}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
12
24
|
|
|
13
25
|
// Ensure comment is within 100 characters
|
|
14
26
|
if (node.comment && node.comment.length > 100) {
|
|
15
27
|
node.comment = node.comment.substring(0, 100);
|
|
16
28
|
}
|
|
17
29
|
|
|
18
|
-
// Update status based on configuration
|
|
19
|
-
const
|
|
30
|
+
// Update status based on configuration (static — no msg available)
|
|
31
|
+
const updateStaticStatus = function() {
|
|
20
32
|
switch (node.statusDisplay) {
|
|
21
33
|
case "default":
|
|
22
34
|
utils.setStatusOK(node, node.comment || "No comment set");
|
|
@@ -24,16 +36,54 @@ module.exports = function(RED) {
|
|
|
24
36
|
case "name":
|
|
25
37
|
utils.setStatusOK(node, node.name || "comment");
|
|
26
38
|
break;
|
|
39
|
+
case "property":
|
|
40
|
+
// Can't resolve msg properties without a message — show placeholder
|
|
41
|
+
utils.setStatusOK(node, "waiting for input");
|
|
42
|
+
break;
|
|
27
43
|
case "none":
|
|
28
44
|
default:
|
|
29
|
-
// No status for "none"
|
|
30
45
|
break;
|
|
31
46
|
}
|
|
32
47
|
};
|
|
33
48
|
|
|
34
|
-
|
|
49
|
+
// Resolve and display the configured status property from a message
|
|
50
|
+
function resolveStatusProperty(msg) {
|
|
51
|
+
return new Promise(function(resolve) {
|
|
52
|
+
if (node.statusPropertyType === "jsonata" && jsonataExpr) {
|
|
53
|
+
RED.util.evaluateJSONataExpression(jsonataExpr, msg, function(err, result) {
|
|
54
|
+
if (err) {
|
|
55
|
+
resolve(undefined);
|
|
56
|
+
} else {
|
|
57
|
+
resolve(result);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
// msg type — use getMessageProperty
|
|
62
|
+
try {
|
|
63
|
+
resolve(RED.util.getMessageProperty(msg, node.statusProperty));
|
|
64
|
+
} catch (err) {
|
|
65
|
+
resolve(undefined);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatValue(val) {
|
|
72
|
+
if (val === undefined) return "undefined";
|
|
73
|
+
if (val === null) return "null";
|
|
74
|
+
if (typeof val === "number") return val % 1 === 0 ? String(val) : val.toFixed(2);
|
|
75
|
+
if (typeof val === "boolean") return String(val);
|
|
76
|
+
if (typeof val === "string") return val.length > 40 ? val.substring(0, 40) + "…" : val;
|
|
77
|
+
if (typeof val === "object") {
|
|
78
|
+
const s = JSON.stringify(val);
|
|
79
|
+
return s.length > 40 ? s.substring(0, 40) + "…" : s;
|
|
80
|
+
}
|
|
81
|
+
return String(val);
|
|
82
|
+
}
|
|
35
83
|
|
|
36
|
-
|
|
84
|
+
updateStaticStatus();
|
|
85
|
+
|
|
86
|
+
node.on("input", async function(msg, send, done) {
|
|
37
87
|
send = send || function() { node.send.apply(node, arguments); };
|
|
38
88
|
|
|
39
89
|
// Guard against invalid msg
|
|
@@ -43,7 +93,16 @@ module.exports = function(RED) {
|
|
|
43
93
|
return;
|
|
44
94
|
}
|
|
45
95
|
|
|
46
|
-
|
|
96
|
+
if (node.statusDisplay === "property" && node.statusProperty) {
|
|
97
|
+
const val = await resolveStatusProperty(msg);
|
|
98
|
+
if (val === undefined) {
|
|
99
|
+
utils.setStatusWarn(node, `${node.statusPropertyType === "jsonata" ? "expr" : node.statusProperty}: not found`);
|
|
100
|
+
} else {
|
|
101
|
+
utils.setStatusChanged(node, formatValue(val));
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
updateStaticStatus();
|
|
105
|
+
}
|
|
47
106
|
|
|
48
107
|
send(msg);
|
|
49
108
|
if (done) done();
|
|
@@ -6,6 +6,7 @@ module.exports = function(RED) {
|
|
|
6
6
|
const node = this;
|
|
7
7
|
|
|
8
8
|
node.contextPropertyName = config.contextPropertyName || "in1";
|
|
9
|
+
node.inputProperty = config.inputProperty || "payload";
|
|
9
10
|
node.removeLabel = config.removeLabel || false;
|
|
10
11
|
|
|
11
12
|
utils.setStatusOK(node, node.removeLabel ? "remove" : `set -> ${node.contextPropertyName}`);
|
|
@@ -23,10 +24,10 @@ module.exports = function(RED) {
|
|
|
23
24
|
// Set or remove context property
|
|
24
25
|
if (node.removeLabel) {
|
|
25
26
|
delete msg.context;
|
|
26
|
-
utils.setStatusChanged(node, `${msg.
|
|
27
|
+
utils.setStatusChanged(node, `${msg[node.inputProperty]} -> removed`);
|
|
27
28
|
} else {
|
|
28
29
|
msg.context = node.contextPropertyName;
|
|
29
|
-
utils.setStatusChanged(node, `${msg.
|
|
30
|
+
utils.setStatusChanged(node, `${msg[node.inputProperty]} -> ${node.contextPropertyName}`);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
send(msg);
|
package/nodes/convert-block.js
CHANGED
|
@@ -287,7 +287,7 @@ module.exports = function(RED) {
|
|
|
287
287
|
const outDisplay = output % 1 === 0 ? output : output.toFixed(2);
|
|
288
288
|
|
|
289
289
|
// Update status and send output
|
|
290
|
-
utils.setStatusOK(node, `${
|
|
290
|
+
utils.setStatusOK(node, `${outDisplay} ${outUnit}`);
|
|
291
291
|
|
|
292
292
|
msg.payload = output;
|
|
293
293
|
send(msg);
|
package/nodes/edge-block.html
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
outputs: 1,
|
|
32
32
|
inputLabels: ["input"],
|
|
33
33
|
outputLabels: ["transition"],
|
|
34
|
-
icon: "font-awesome/fa-
|
|
34
|
+
icon: "font-awesome/fa-bolt",
|
|
35
35
|
paletteLabel: "edge",
|
|
36
36
|
label: function() {
|
|
37
37
|
return this.name || "edge";
|
|
@@ -49,7 +49,9 @@ Detects boolean state transitions (true-to-false or false-to-true).
|
|
|
49
49
|
: payload (string | boolean) : Transition type (`"true-to-false"`, `"false-to-true"`) for `"algorithm"`, true for `"reset"`.
|
|
50
50
|
|
|
51
51
|
### Outputs
|
|
52
|
-
:
|
|
52
|
+
: *entire message* : The original input message is forwarded **only** when the specified transition occurs. No output otherwise.
|
|
53
|
+
: edge (boolean) : `true` is added to the message to flag that a transition was detected.
|
|
54
|
+
: payload : Preserved from the input message (unchanged).
|
|
53
55
|
|
|
54
56
|
### Properties
|
|
55
57
|
: name (string) : Display name in editor.
|
|
@@ -58,8 +60,9 @@ Detects boolean state transitions (true-to-false or false-to-true).
|
|
|
58
60
|
|
|
59
61
|
### Details
|
|
60
62
|
Detects transitions in boolean input (read from the configured **Input Property**, default: `msg.payload`) based on the configured `algorithm`.
|
|
61
|
-
|
|
63
|
+
Forwards the **entire original message** only when the specified transition occurs (true-to-false or false-to-true), with `msg.edge = true` added.
|
|
62
64
|
No output on first input after reset, if no transition occurs, or for non-boolean inputs.
|
|
65
|
+
Because the full message passes through, this node can be used inline to conditionally gate messages based on boolean state changes while preserving all upstream properties.
|
|
63
66
|
|
|
64
67
|
Configuration via `msg.context`:
|
|
65
68
|
- `"algorithm"`: Sets transition type, no output.
|
package/nodes/edge-block.js
CHANGED
|
@@ -103,10 +103,11 @@ module.exports = function(RED) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
if (isTransition) {
|
|
106
|
-
utils.setStatusChanged(node, `
|
|
107
|
-
|
|
106
|
+
utils.setStatusChanged(node, `true`);
|
|
107
|
+
msg.edge = true;
|
|
108
|
+
send(msg);
|
|
108
109
|
} else {
|
|
109
|
-
utils.setStatusUnchanged(node, `
|
|
110
|
+
utils.setStatusUnchanged(node, `none`);
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
node.lastValue = currentValue;
|
package/nodes/global-setter.html
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
<input type="text" id="node-input-property" style="width:70%;">
|
|
13
13
|
</div>
|
|
14
14
|
<div class="form-row">
|
|
15
|
-
<label for="node-input-writePriority" title="
|
|
16
|
-
<input type="text" id="node-input-writePriority" placeholder="
|
|
15
|
+
<label for="node-input-writePriority" title="Network priority slot (1-16) for this message. Network writes from external control systems use these slots."><i class="fa fa-sort-numeric-asc"></i> Network Priority</label>
|
|
16
|
+
<input type="text" id="node-input-writePriority" placeholder="16">
|
|
17
17
|
<input type="hidden" id="node-input-writePriorityType">
|
|
18
18
|
</div>
|
|
19
19
|
|
|
@@ -29,13 +29,13 @@
|
|
|
29
29
|
|
|
30
30
|
<div class="form-row">
|
|
31
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
|
|
33
|
-
<i class="fa fa-eraser"></i> Clear
|
|
32
|
+
<button type="button" id="node-btn-clear-priorities" class="editor-button" style="width: calc(70% - 3px);" title="Clear all 16 network priority slots on the live node (does not affect the default or fallback values)">
|
|
33
|
+
<i class="fa fa-eraser"></i> Clear Network Priorities
|
|
34
34
|
</button>
|
|
35
35
|
</div>
|
|
36
36
|
|
|
37
37
|
<div class="form-tips">
|
|
38
|
-
<b>Note:</b> This node writes to the
|
|
38
|
+
<b>Note:</b> This node writes to the <b>Network Priority</b> slot (1-16), manually, by msg or flow. The actual value is determined by the highest active source: Priorities 1-16 → Fallback → Default.
|
|
39
39
|
</div>
|
|
40
40
|
</script>
|
|
41
41
|
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
property: { value: "payload", required: true },
|
|
50
50
|
defaultValue: { value: "", required: true },
|
|
51
51
|
defaultValueType: { value: "num", required: true },
|
|
52
|
-
writePriority: { value: "
|
|
52
|
+
writePriority: { value: "fallback", required: true },
|
|
53
53
|
writePriorityType: { value: "dropdown" }
|
|
54
54
|
},
|
|
55
55
|
inputs: 1,
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
{ value: "14", label: "14"},
|
|
102
102
|
{ value: "15", label: "15"},
|
|
103
103
|
{ value: "16", label: "16 (Network Logic)"},
|
|
104
|
-
{ value: "
|
|
104
|
+
{ value: "fallback", label: "fallback (Internal Logic)"},
|
|
105
105
|
]
|
|
106
106
|
}, "msg", "flow"],
|
|
107
107
|
typeField: "#node-input-writePriorityType"
|
|
@@ -141,12 +141,12 @@ Manage a global variable in a repeatable way.
|
|
|
141
141
|
### Inputs
|
|
142
142
|
: payload (any) : Input payload is passed through unchanged.
|
|
143
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
|
|
145
|
-
: context (string) : _Optional_. Tagged-input
|
|
144
|
+
: priority (number|string) : _Optional_. Overrides the configured Network Priority at runtime. Accepts `1`–`16`. For example, `msg.priority = 8` writes to priority slot 8.
|
|
145
|
+
: context (string) : _Optional_. Tagged-input routing. Accepts `"fallback"` (writes to the fallback slot) or `"priority1"`–`"priority16"` (writes to network priority slots), or `"reload"` (reload state without writing). When both `msg.priority` and `msg.context` are present, `msg.priority` takes precedence.
|
|
146
146
|
: units (string) : The units associated with the value, if any. Also supports nested units at `msg.<inputProperty>.units`.
|
|
147
147
|
|
|
148
148
|
### Outputs
|
|
149
|
-
: payload (any) : Original payload.
|
|
149
|
+
: payload (any) : Original payload plus full state object (value, activePriority, all priority slots, fallback, metadata).
|
|
150
150
|
|
|
151
151
|
### Details
|
|
152
152
|
Global variables are meant to be retrieved in other places, this necessarily means managing the same string in multiple places.
|
|
@@ -155,7 +155,12 @@ This node allows you to set a global variable in one place, and retrieve it else
|
|
|
155
155
|
|
|
156
156
|
When this node is deleted or the flow is redeployed, it will automatically remove (prune) the variable from the selected Context Store.
|
|
157
157
|
|
|
158
|
-
**
|
|
158
|
+
**Priority Hierarchy**: The active value is determined by the highest active source:
|
|
159
|
+
1. **Network Priorities (1-16)**: Used by external automation systems (e.g., BACnet, network service). Priority 16 is typical for network logic, 1 for life safety.
|
|
160
|
+
2. **Fallback**: A dynamic input slot updated via `{context: "fallback", payload: value}`. Useful for internal logic to write a secondary value (e.g., persistent context setpoints).
|
|
161
|
+
3. **Default**: A static configuration value. Always used as the ultimate fallback when all other sources are null/empty.
|
|
162
|
+
|
|
163
|
+
**Clear Network Priorities** button (in editor): Resets all 16 priority slots to `null` on the live running node. The fallback and default values are preserved. The active value recalculates based on the hierarchy. The node must be deployed first.
|
|
159
164
|
|
|
160
165
|
### Status
|
|
161
166
|
- Green (dot): Configuration update
|
|
@@ -164,6 +169,8 @@ When this node is deleted or the flow is redeployed, it will automatically remov
|
|
|
164
169
|
- Red (ring): Error
|
|
165
170
|
- Yellow (ring): Warning
|
|
166
171
|
|
|
172
|
+
Status prefix: Empty = default active, `F` = fallback active, `P` = network priority active (e.g., `P3` = priority 3).
|
|
173
|
+
|
|
167
174
|
### References
|
|
168
175
|
- [Node-RED Documentation](https://nodered.org/docs/)
|
|
169
176
|
- [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
|
package/nodes/global-setter.js
CHANGED
|
@@ -24,6 +24,7 @@ module.exports = function(RED) {
|
|
|
24
24
|
value: node.defaultValue,
|
|
25
25
|
defaultValue: node.defaultValue,
|
|
26
26
|
activePriority: "default",
|
|
27
|
+
fallback: null,
|
|
27
28
|
units: null,
|
|
28
29
|
priority: { 1: null, 2: null, 3: null, 4: null, 5: null, 6: null, 7: null, 8: null, 9: null, 10: null, 11: null, 12: null, 13: null, 14: null, 15: null, 16: null },
|
|
29
30
|
metadata: {
|
|
@@ -93,44 +94,64 @@ module.exports = function(RED) {
|
|
|
93
94
|
node.isBusy = true;
|
|
94
95
|
|
|
95
96
|
// Resolve write priority — three sources, in order of precedence:
|
|
96
|
-
// 1. msg.priority (number 1-16
|
|
97
|
-
// 2. msg.context ("priority1"–"priority16"
|
|
97
|
+
// 1. msg.priority (number 1-16) — explicit per-message override
|
|
98
|
+
// 2. msg.context ("priority1"–"priority16", "fallback", "reload") — tagged-input pattern
|
|
98
99
|
// 3. Configured writePriority (dropdown / msg / flow typed-input)
|
|
100
|
+
// Use local variable — never mutate node.writePriority so the config default is preserved across messages
|
|
101
|
+
let activePrioritySlot = null;
|
|
102
|
+
let isFallbackWrite = false;
|
|
103
|
+
|
|
99
104
|
try {
|
|
100
|
-
if (msg.hasOwnProperty("
|
|
101
|
-
//
|
|
105
|
+
if (msg.hasOwnProperty("context") && typeof msg.context === "string") {
|
|
106
|
+
// Check for special contexts first
|
|
107
|
+
const ctx = msg.context;
|
|
108
|
+
if (ctx === "fallback") {
|
|
109
|
+
isFallbackWrite = true;
|
|
110
|
+
} else if (ctx === "reload") {
|
|
111
|
+
// Handled separately below
|
|
112
|
+
activePrioritySlot = "reload";
|
|
113
|
+
} else {
|
|
114
|
+
// Check for priority context
|
|
115
|
+
const priorityMatch = /^priority([1-9]|1[0-6])$/.exec(ctx);
|
|
116
|
+
if (priorityMatch) {
|
|
117
|
+
activePrioritySlot = priorityMatch[1];
|
|
118
|
+
}
|
|
119
|
+
// Unknown contexts leave activePrioritySlot null → falls to config
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (msg.hasOwnProperty("priority") && (typeof msg.priority === "number" || typeof msg.priority === "string")) {
|
|
124
|
+
// Source 1: msg.priority (direct number 1-16) — skip objects (e.g. priority array from state)
|
|
102
125
|
const mp = msg.priority;
|
|
103
|
-
|
|
104
|
-
|
|
126
|
+
const p = parseInt(mp, 10);
|
|
127
|
+
if (isNaN(p) || p < 1 || p > 16) {
|
|
128
|
+
node.isBusy = false;
|
|
129
|
+
return utils.sendError(node, msg, done, `Invalid msg.priority: ${mp} (must be 1-16)`);
|
|
130
|
+
}
|
|
131
|
+
activePrioritySlot = String(p);
|
|
132
|
+
isFallbackWrite = false; // msg.priority overrides fallback context
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Source 3: Fall back to configured writePriority only if no message override matched
|
|
136
|
+
if (!isFallbackWrite && activePrioritySlot === null) {
|
|
137
|
+
let configuredSlot;
|
|
138
|
+
if (utils.requiresEvaluation(config.writePriorityType)) {
|
|
139
|
+
configuredSlot = await utils.evaluateNodeProperty(config.writePriority, config.writePriorityType, node, msg);
|
|
140
|
+
} else {
|
|
141
|
+
configuredSlot = config.writePriority;
|
|
142
|
+
}
|
|
143
|
+
// Allow "fallback" as a configured value
|
|
144
|
+
if (configuredSlot === "fallback") {
|
|
145
|
+
isFallbackWrite = true;
|
|
105
146
|
} else {
|
|
106
|
-
|
|
107
|
-
|
|
147
|
+
// Validate configured priority (must be 1-16)
|
|
148
|
+
const cp = parseInt(configuredSlot, 10);
|
|
149
|
+
if (isNaN(cp) || cp < 1 || cp > 16) {
|
|
108
150
|
node.isBusy = false;
|
|
109
|
-
return utils.sendError(node, msg, done, `Invalid
|
|
151
|
+
return utils.sendError(node, msg, done, `Invalid configured writePriority: ${configuredSlot} (must be 1-16 or fallback)`);
|
|
110
152
|
}
|
|
111
|
-
|
|
153
|
+
activePrioritySlot = String(cp);
|
|
112
154
|
}
|
|
113
|
-
} else if (msg.hasOwnProperty("context") && typeof msg.context === "string") {
|
|
114
|
-
// Source 2: msg.context tagged-input ("priority8", "default", etc.)
|
|
115
|
-
// "reload" is handled separately below — skip it here
|
|
116
|
-
const ctx = msg.context;
|
|
117
|
-
const priorityMatch = /^priority([1-9]|1[0-6])$/.exec(ctx);
|
|
118
|
-
if (priorityMatch) {
|
|
119
|
-
node.writePriority = priorityMatch[1];
|
|
120
|
-
} else if (ctx === "default") {
|
|
121
|
-
node.writePriority = "default";
|
|
122
|
-
}
|
|
123
|
-
// Other contexts (e.g. "reload") fall through — config stays as-is
|
|
124
|
-
} else {
|
|
125
|
-
// Source 3: Configured typed-input (dropdown, msg path, flow variable)
|
|
126
|
-
const evaluations = [];
|
|
127
|
-
evaluations.push(
|
|
128
|
-
utils.requiresEvaluation(config.writePriorityType)
|
|
129
|
-
? utils.evaluateNodeProperty(config.writePriority, config.writePriorityType, node, msg)
|
|
130
|
-
: Promise.resolve(node.writePriority)
|
|
131
|
-
);
|
|
132
|
-
const results = await Promise.all(evaluations);
|
|
133
|
-
node.writePriority = results[0];
|
|
134
155
|
}
|
|
135
156
|
} catch (err) {
|
|
136
157
|
throw new Error(`Property Eval Error: ${err.message}`);
|
|
@@ -146,12 +167,12 @@ module.exports = function(RED) {
|
|
|
146
167
|
}
|
|
147
168
|
|
|
148
169
|
// Handle Reload
|
|
149
|
-
if (
|
|
170
|
+
if (activePrioritySlot === "reload") {
|
|
150
171
|
RED.events.emit("bldgblocks:global:value-changed", { key: node.varName, store: node.storeName, data: state });
|
|
151
172
|
await utils.setGlobalState(node, node.varName, node.storeName, state);
|
|
152
173
|
|
|
153
|
-
|
|
154
|
-
const statusText = `reload: ${
|
|
174
|
+
const activeLabel = state.activePriority === 'default' ? 'default' : (state.activePriority === 'fallback' ? 'fallback' : `P${state.activePriority}`);
|
|
175
|
+
const statusText = `reload: ${activeLabel}:${state.value}${state.units || ''}`;
|
|
155
176
|
|
|
156
177
|
return utils.sendSuccess(node, { ...state }, done, statusText, null, "dot");
|
|
157
178
|
}
|
|
@@ -167,38 +188,34 @@ module.exports = function(RED) {
|
|
|
167
188
|
return utils.sendError(node, msg, done, `msg.${node.inputProperty} not found or invalid property path`);
|
|
168
189
|
}
|
|
169
190
|
|
|
170
|
-
// Update State
|
|
171
|
-
if (
|
|
172
|
-
state.
|
|
191
|
+
// Update State: either fallback or priority slot
|
|
192
|
+
if (isFallbackWrite) {
|
|
193
|
+
state.fallback = inputValue === null || inputValue === "null" ? null : inputValue;
|
|
173
194
|
} else {
|
|
174
|
-
const priority = parseInt(
|
|
195
|
+
const priority = parseInt(activePrioritySlot, 10);
|
|
175
196
|
if (isNaN(priority) || priority < 1 || priority > 16) {
|
|
176
|
-
return utils.sendError(node, msg, done, `Invalid priority: ${
|
|
197
|
+
return utils.sendError(node, msg, done, `Invalid priority: ${activePrioritySlot}`);
|
|
177
198
|
}
|
|
178
199
|
if (inputValue !== undefined) {
|
|
179
|
-
state.priority[
|
|
200
|
+
state.priority[activePrioritySlot] = inputValue;
|
|
180
201
|
}
|
|
181
202
|
}
|
|
182
|
-
|
|
183
|
-
if (state.defaultValue === null || state.defaultValue === "null" || state.defaultValue === undefined) {
|
|
184
|
-
state.defaultValue = node.defaultValue;
|
|
185
|
-
}
|
|
186
203
|
|
|
187
|
-
// Calculate Winner
|
|
204
|
+
// Calculate Winner (includes priorities 1-16, then fallback, then default)
|
|
188
205
|
const { value, priority } = utils.getHighestPriority(state);
|
|
189
206
|
|
|
190
207
|
// Check for change
|
|
191
208
|
if (value === state.value && priority === state.activePriority) {
|
|
192
209
|
// Ensure payload stays in sync with value
|
|
193
210
|
state.payload = state.value;
|
|
194
|
-
// Persist even when output unchanged — the priority array itself changed
|
|
211
|
+
// Persist even when output unchanged — the priority/fallback array itself changed
|
|
195
212
|
await utils.setGlobalState(node, node.varName, node.storeName, state);
|
|
196
213
|
if (node.storeName !== 'default') {
|
|
197
214
|
await utils.setGlobalState(node, node.varName, 'default', state);
|
|
198
215
|
}
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
const noChangeText = `no change: ${
|
|
216
|
+
const writeSlotLabel = isFallbackWrite ? 'fallback' : `P${activePrioritySlot}`;
|
|
217
|
+
const activeLabel = state.activePriority === 'default' ? 'default' : (state.activePriority === 'fallback' ? 'fallback' : `P${state.activePriority}`);
|
|
218
|
+
const noChangeText = `no change: ${writeSlotLabel}:${inputValue} > active: ${activeLabel}:${state.value}${state.units || ''}`;
|
|
202
219
|
utils.setStatusUnchanged(node, noChangeText);
|
|
203
220
|
// Pass message through even if no context change
|
|
204
221
|
send({ ...state });
|
|
@@ -236,9 +253,9 @@ module.exports = function(RED) {
|
|
|
236
253
|
await utils.setGlobalState(node, node.varName, 'default', state);
|
|
237
254
|
}
|
|
238
255
|
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
const statusText = `write: ${
|
|
256
|
+
const writeSlotLabel = isFallbackWrite ? 'fallback' : `P${activePrioritySlot}`;
|
|
257
|
+
const activeLabel = state.activePriority === 'default' ? 'default' : (state.activePriority === 'fallback' ? 'fallback' : `P${state.activePriority}`);
|
|
258
|
+
const statusText = `write: ${writeSlotLabel}:${inputValue}${state.units || ''} > active: ${activeLabel}:${state.value}${state.units || ''}`;
|
|
242
259
|
|
|
243
260
|
RED.events.emit("bldgblocks:global:value-changed", {
|
|
244
261
|
key: node.varName,
|
|
@@ -301,7 +318,8 @@ module.exports = function(RED) {
|
|
|
301
318
|
store: targetNode.storeName,
|
|
302
319
|
data: state
|
|
303
320
|
});
|
|
304
|
-
|
|
321
|
+
const activeLabel = state.activePriority === 'default' ? 'default' : (state.activePriority === 'fallback' ? 'fallback' : `P${state.activePriority}`);
|
|
322
|
+
utils.setStatusOK(targetNode, `cleared: active: ${activeLabel}:${state.value}`);
|
|
305
323
|
targetNode.send({ ...state });
|
|
306
324
|
|
|
307
325
|
res.status(200).json({ message: "Priorities cleared", value: state.value, activePriority: state.activePriority });
|
|
@@ -180,7 +180,7 @@ module.exports = function(RED) {
|
|
|
180
180
|
{ payload: newState === "below" }
|
|
181
181
|
];
|
|
182
182
|
|
|
183
|
-
utils.setStatusChanged(node,
|
|
183
|
+
utils.setStatusChanged(node, `${inputValue.toFixed(2)} -> ${newState}`);
|
|
184
184
|
|
|
185
185
|
node.state = newState;
|
|
186
186
|
send(output);
|
|
@@ -110,8 +110,8 @@ module.exports = function(RED) {
|
|
|
110
110
|
// Check if output value has changed
|
|
111
111
|
const isUnchanged = outputValue === node.lastOutput;
|
|
112
112
|
const statusShape = isUnchanged ? "ring" : "dot";
|
|
113
|
-
utils.setStatusOK(node,
|
|
114
|
-
if (statusShape === "ring") utils.setStatusUnchanged(node,
|
|
113
|
+
utils.setStatusOK(node, `${outputValue.toFixed(2)}`);
|
|
114
|
+
if (statusShape === "ring") utils.setStatusUnchanged(node, `${outputValue.toFixed(2)}`);
|
|
115
115
|
|
|
116
116
|
if (!isUnchanged) {
|
|
117
117
|
node.lastOutput = outputValue;
|
package/nodes/negate-block.js
CHANGED
|
@@ -44,10 +44,10 @@ module.exports = function(RED) {
|
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
outputValue = -inputValue;
|
|
47
|
-
statusText =
|
|
47
|
+
statusText = `${inputValue.toFixed(2)} -> ${outputValue.toFixed(2)}`;
|
|
48
48
|
} else if (typeof inputValue === "boolean") {
|
|
49
49
|
outputValue = !inputValue;
|
|
50
|
-
statusText =
|
|
50
|
+
statusText = `${inputValue} -> ${outputValue}`;
|
|
51
51
|
} else {
|
|
52
52
|
utils.setStatusError(node, "Unsupported type");
|
|
53
53
|
if (done) done();
|
|
@@ -104,8 +104,17 @@ module.exports = function(RED) {
|
|
|
104
104
|
RED.events.emit("bldgblocks:network:point-update", pointUpdateData);
|
|
105
105
|
|
|
106
106
|
// Passthrough
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
let statusLabel;
|
|
108
|
+
if (msg.activePriority === 'default') {
|
|
109
|
+
statusLabel = 'default';
|
|
110
|
+
} else if (msg.activePriority === 'fallback') {
|
|
111
|
+
statusLabel = 'fallback';
|
|
112
|
+
} else if (typeof msg.activePriority === 'number' || /^\d+$/.test(msg.activePriority)) {
|
|
113
|
+
statusLabel = `P:${msg.activePriority}`;
|
|
114
|
+
} else {
|
|
115
|
+
statusLabel = String(msg.activePriority);
|
|
116
|
+
}
|
|
117
|
+
const statusText = `Passthrough: ${statusLabel}:${msg.value}${msg.units === null ? "" : ` ${msg.units}`}`;
|
|
109
118
|
utils.sendSuccess(node, msg, done, statusText, node.pointId, "ring");
|
|
110
119
|
|
|
111
120
|
} catch (err) {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
<tr style="border-bottom:2px solid #ccc;text-align:left;">
|
|
30
30
|
<th style="padding:4px;width:28px;"><input type="checkbox" id="node-input-select-all" title="Select all"></th>
|
|
31
31
|
<th style="padding:4px;width:70px;">ID</th>
|
|
32
|
+
<th style="padding:4px;width:30px;" title="Writable"><i class="fa fa-pencil"></i></th>
|
|
32
33
|
<th style="padding:4px;">Path</th>
|
|
33
34
|
<th style="padding:4px;width:30px;"></th>
|
|
34
35
|
</tr>
|
|
@@ -106,7 +107,7 @@
|
|
|
106
107
|
$selectAll.prop('checked', false);
|
|
107
108
|
|
|
108
109
|
if (!points.length) {
|
|
109
|
-
$tbody.append('<tr><td colspan="
|
|
110
|
+
$tbody.append('<tr><td colspan="5" style="padding:8px;color:#999;">No points defined (deploy first if newly added)</td></tr>');
|
|
110
111
|
$status.text("");
|
|
111
112
|
return;
|
|
112
113
|
}
|
|
@@ -141,6 +142,13 @@
|
|
|
141
142
|
|
|
142
143
|
const $tdId = $('<td>').css({ padding: '3px 4px' }).append($idInput);
|
|
143
144
|
|
|
145
|
+
// Writable checkbox
|
|
146
|
+
const $wrCb = $('<input type="checkbox" class="point-writable">')
|
|
147
|
+
.prop('checked', !!pt.writable)
|
|
148
|
+
.attr('data-original-writable', pt.writable ? '1' : '0')
|
|
149
|
+
.attr('title', 'Allow network writes to this point');
|
|
150
|
+
const $tdWr = $('<td>').css({ padding: '3px 4px', textAlign: 'center' }).append($wrCb);
|
|
151
|
+
|
|
144
152
|
// Path display
|
|
145
153
|
const displayPath = pt.path && pt.path !== "not ready" ? pt.path : (pt.editorName || 'not deployed');
|
|
146
154
|
const $tdPath = $('<td>').css({ padding: '3px 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '200px' })
|
|
@@ -157,7 +165,7 @@
|
|
|
157
165
|
});
|
|
158
166
|
const $tdBtn = $('<td>').css({ padding: '3px 4px' }).append($btn);
|
|
159
167
|
|
|
160
|
-
$tr.append($tdCb, $tdId, $tdPath, $tdBtn);
|
|
168
|
+
$tr.append($tdCb, $tdId, $tdWr, $tdPath, $tdBtn);
|
|
161
169
|
$tbody.append($tr);
|
|
162
170
|
});
|
|
163
171
|
|
|
@@ -267,11 +275,25 @@
|
|
|
267
275
|
changeCount++;
|
|
268
276
|
}
|
|
269
277
|
}
|
|
278
|
+
|
|
279
|
+
// Check writable change
|
|
280
|
+
const $wrCb = $(this).find('.point-writable');
|
|
281
|
+
const newWr = $wrCb.is(':checked');
|
|
282
|
+
const origWr = $wrCb.attr('data-original-writable') === '1';
|
|
283
|
+
if (newWr !== origWr) {
|
|
284
|
+
const editorNode = RED.nodes.node(nid);
|
|
285
|
+
if (editorNode) {
|
|
286
|
+
editorNode.writable = newWr;
|
|
287
|
+
editorNode.changed = true;
|
|
288
|
+
editorNode.dirty = true;
|
|
289
|
+
changeCount++;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
270
292
|
});
|
|
271
293
|
|
|
272
294
|
if (changeCount > 0) {
|
|
273
295
|
RED.nodes.dirty(true);
|
|
274
|
-
RED.notify(changeCount + "
|
|
296
|
+
RED.notify(changeCount + " change" + (changeCount !== 1 ? "s" : "") + " applied. Deploy to take effect.", "success");
|
|
275
297
|
}
|
|
276
298
|
}
|
|
277
299
|
});
|
|
@@ -43,16 +43,12 @@ module.exports = function(RED) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Update Priority Logic
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const priority = parseInt(msg.priority, 10);
|
|
51
|
-
if (isNaN(priority) || priority < 1 || priority > 16) {
|
|
52
|
-
return utils.sendError(node, msg, done, `Invalid Priority: ${msg.priority}`, msg.pointId);
|
|
53
|
-
}
|
|
54
|
-
state.priority[msg.priority] = newValue;
|
|
46
|
+
// Update Priority Logic (only allow priorities 1-16, no default)
|
|
47
|
+
const priority = parseInt(msg.priority, 10);
|
|
48
|
+
if (isNaN(priority) || priority < 1 || priority > 16) {
|
|
49
|
+
return utils.sendError(node, msg, done, `Invalid Priority: ${msg.priority} (must be 1-16)`, msg.pointId);
|
|
55
50
|
}
|
|
51
|
+
state.priority[msg.priority] = newValue;
|
|
56
52
|
|
|
57
53
|
// Calculate Winner
|
|
58
54
|
const result = utils.getHighestPriority(state);
|
|
@@ -63,8 +59,8 @@ module.exports = function(RED) {
|
|
|
63
59
|
// Save (Async) & Emit
|
|
64
60
|
await utils.setGlobalState(node, path, store, state);
|
|
65
61
|
|
|
66
|
-
const prefixReq =
|
|
67
|
-
const prefixAct = state.activePriority === 'default' ? '' : 'P';
|
|
62
|
+
const prefixReq = 'P';
|
|
63
|
+
const prefixAct = state.activePriority === 'default' ? '' : (state.activePriority === 'fallback' ? 'F' : 'P');
|
|
68
64
|
const statusMsg = `Wrote: ${prefixReq}${msg.priority}:${newValue} > Active: ${prefixAct}${state.activePriority}:${state.value}`;
|
|
69
65
|
|
|
70
66
|
msg = { ...state, status: null };
|