@bldgblocks/node-red-contrib-control 0.1.32 → 0.1.34
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 +1 -1
- package/nodes/add-block.html +1 -1
- package/nodes/analog-switch-block.html +1 -1
- package/nodes/and-block.html +1 -1
- package/nodes/average-block.html +1 -1
- package/nodes/boolean-switch-block.html +1 -1
- package/nodes/boolean-to-number-block.html +1 -1
- package/nodes/cache-block.html +1 -1
- package/nodes/call-status-block.html +1 -1
- package/nodes/changeover-block.html +1 -1
- package/nodes/comment-block.html +1 -1
- package/nodes/compare-block.html +1 -1
- package/nodes/contextual-label-block.html +1 -1
- package/nodes/convert-block.html +1 -1
- package/nodes/count-block.html +2 -2
- package/nodes/delay-block.html +1 -1
- package/nodes/divide-block.html +1 -1
- package/nodes/edge-block.html +1 -1
- package/nodes/enum-switch-block.html +1 -1
- package/nodes/frequency-block.html +1 -1
- package/nodes/global-getter.html +34 -3
- package/nodes/global-getter.js +73 -70
- package/nodes/global-setter.html +21 -10
- package/nodes/global-setter.js +130 -103
- package/nodes/history-collector.html +283 -0
- package/nodes/history-collector.js +150 -0
- package/nodes/history-config.html +236 -0
- package/nodes/history-config.js +8 -0
- package/nodes/hysteresis-block.html +1 -1
- package/nodes/interpolate-block.html +1 -1
- package/nodes/latch-block.html +1 -1
- package/nodes/load-sequence-block.html +1 -1
- package/nodes/max-block.html +1 -1
- package/nodes/memory-block.html +1 -1
- package/nodes/min-block.html +1 -1
- package/nodes/minmax-block.html +1 -1
- package/nodes/modulo-block.html +1 -1
- package/nodes/multiply-block.html +1 -1
- package/nodes/negate-block.html +1 -1
- package/nodes/network-point-registry.html +86 -0
- package/nodes/network-point-registry.js +90 -0
- package/nodes/network-read.html +56 -0
- package/nodes/network-read.js +57 -0
- package/nodes/network-register.html +110 -0
- package/nodes/network-register.js +112 -0
- package/nodes/network-write.html +65 -0
- package/nodes/network-write.js +83 -0
- package/nodes/nullify-block.html +1 -1
- package/nodes/on-change-block.html +1 -1
- package/nodes/oneshot-block.html +1 -1
- package/nodes/or-block.html +1 -1
- package/nodes/pid-block.html +1 -1
- package/nodes/priority-block.html +1 -1
- package/nodes/rate-limit-block.html +2 -2
- package/nodes/rate-of-change-block.html +1 -1
- package/nodes/rate-of-change-block.js +5 -2
- package/nodes/round-block.html +6 -5
- package/nodes/round-block.js +5 -3
- package/nodes/saw-tooth-wave-block.html +2 -2
- package/nodes/scale-range-block.html +1 -1
- package/nodes/sine-wave-block.html +2 -2
- package/nodes/string-builder-block.html +1 -1
- package/nodes/subtract-block.html +1 -1
- package/nodes/thermistor-block.html +1 -1
- package/nodes/tick-tock-block.html +2 -2
- package/nodes/time-sequence-block.html +1 -1
- package/nodes/triangle-wave-block.html +2 -2
- package/nodes/tstat-block.html +1 -1
- package/nodes/units-block.html +8 -38
- package/nodes/units-block.js +3 -42
- package/nodes/utils.js +70 -1
- package/package.json +11 -4
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function NetworkPointRegistryNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
// The Map: { 101: { nodeId: "abc.123", writable: true, ... } }
|
|
7
|
+
node.points = new Map();
|
|
8
|
+
|
|
9
|
+
node.register = function(pointId, meta) {
|
|
10
|
+
const pid = parseInt(pointId);
|
|
11
|
+
if (isNaN(pid)) return false;
|
|
12
|
+
|
|
13
|
+
if (node.points.has(pid)) {
|
|
14
|
+
const existing = node.points.get(pid);
|
|
15
|
+
// Allow update if it's the same node
|
|
16
|
+
if (existing.nodeId !== meta.nodeId) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
// Merge updates
|
|
20
|
+
meta = Object.assign({}, existing, meta);
|
|
21
|
+
}
|
|
22
|
+
node.points.set(pid, meta);
|
|
23
|
+
return true;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
node.unregister = function(pointId, nodeId) {
|
|
27
|
+
const pid = parseInt(pointId);
|
|
28
|
+
if (node.points.has(pid) && node.points.get(pid).nodeId === nodeId) {
|
|
29
|
+
node.points.delete(pid);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
node.lookup = function(pointId) {
|
|
34
|
+
return node.points.get(parseInt(pointId));
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
RED.nodes.registerType("network-point-registry", NetworkPointRegistryNode);
|
|
38
|
+
|
|
39
|
+
// --- HTTP Endpoint for Editor Validation ---
|
|
40
|
+
// Route: /network-point-registry/check/<RegistryID>/<PointID>/<CurrentNodeID>
|
|
41
|
+
RED.httpAdmin.get('/network-point-registry/check/:registryId/:pointId/:nodeId', RED.auth.needsPermission('network-point-registry.read'), function(req, res) {
|
|
42
|
+
const registryId = req.params.registryId;
|
|
43
|
+
const checkId = parseInt(req.params.pointId);
|
|
44
|
+
const checkNodeId = req.params.nodeId;
|
|
45
|
+
|
|
46
|
+
// Find the specific Registry Config Node
|
|
47
|
+
const regNode = RED.nodes.getNode(registryId);
|
|
48
|
+
|
|
49
|
+
let entry = null;
|
|
50
|
+
let result = "unavailable";
|
|
51
|
+
let collision = false;
|
|
52
|
+
|
|
53
|
+
if (!regNode) {
|
|
54
|
+
// Registry exists in editor but not deployed yet, or doesn't exist
|
|
55
|
+
return res.json({ status: result, warning: "Registry not deployed" });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check that specific registry for the ID
|
|
59
|
+
if (regNode.points.has(checkId)) {
|
|
60
|
+
entry = regNode.points.get(checkId);
|
|
61
|
+
// Collision if ID exists AND belongs to a different node
|
|
62
|
+
if (entry.nodeId !== checkNodeId) {
|
|
63
|
+
collision = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (collision) {
|
|
68
|
+
result = "collision";
|
|
69
|
+
} else if (!collision && entry) {
|
|
70
|
+
result = "assigned";
|
|
71
|
+
} else{
|
|
72
|
+
result = "available";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
res.json({ status: result, details: entry });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
RED.httpAdmin.get('/network-point-registry/list/:registryId', RED.auth.needsPermission('network-point-registry.read'), function(req, res) {
|
|
80
|
+
const reg = RED.nodes.getNode(req.params.registryId);
|
|
81
|
+
if (!reg) return res.status(404).json({error:'not found'});
|
|
82
|
+
|
|
83
|
+
// Convert Map to array
|
|
84
|
+
const arr = [];
|
|
85
|
+
for (const [pid, meta] of reg.points.entries()) {
|
|
86
|
+
arr.push({ id: pid, ...meta });
|
|
87
|
+
}
|
|
88
|
+
res.json(arr);
|
|
89
|
+
});
|
|
90
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="network-read">
|
|
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-registry"><i class="fa fa-book"></i> Registry</label>
|
|
9
|
+
<input type="text" id="node-input-registry">
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="form-tips">
|
|
13
|
+
<b>Input Payload Format:</b><br>
|
|
14
|
+
<pre>
|
|
15
|
+
{
|
|
16
|
+
"action": "read",
|
|
17
|
+
"pointId": 101
|
|
18
|
+
}
|
|
19
|
+
</pre>
|
|
20
|
+
</div>
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<script type="text/javascript">
|
|
24
|
+
RED.nodes.registerType('network-read', {
|
|
25
|
+
category: 'bldgblocks network',
|
|
26
|
+
color: '#3090C7',
|
|
27
|
+
defaults: {
|
|
28
|
+
name: { value: "" },
|
|
29
|
+
registry: { value: "", type: "network-point-registry", required: true }
|
|
30
|
+
},
|
|
31
|
+
inputs: 1,
|
|
32
|
+
outputs: 1,
|
|
33
|
+
icon: "font-awesome/fa-database",
|
|
34
|
+
label: function() {
|
|
35
|
+
return "network read";
|
|
36
|
+
},
|
|
37
|
+
paletteLabel: "network read",
|
|
38
|
+
oneditprepare: function() {
|
|
39
|
+
const nodeId = this.id;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<script type="text/markdown" data-help-name="network-read">
|
|
45
|
+
Reads a network point by pointId.
|
|
46
|
+
|
|
47
|
+
### Input
|
|
48
|
+
: action (string) : Not used by the node, used for routing to the correct node over the network when received.
|
|
49
|
+
: pointId (number) : The integer ID of the point.
|
|
50
|
+
|
|
51
|
+
### Output
|
|
52
|
+
: payload (object) : Global data object
|
|
53
|
+
|
|
54
|
+
### Details
|
|
55
|
+
|
|
56
|
+
</script>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
const utils = require('./utils')(RED);
|
|
3
|
+
function NetworkReadNode(config) {
|
|
4
|
+
RED.nodes.createNode(this, config);
|
|
5
|
+
const node = this;
|
|
6
|
+
node.registry = RED.nodes.getNode(config.registry);
|
|
7
|
+
|
|
8
|
+
node.on("input", async function(msg, send, done) {
|
|
9
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
if (!node.registry) {
|
|
13
|
+
node.status({ fill: "red", shape: "ring", text: "Registry missing" });
|
|
14
|
+
if (done) done();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const currentEntry = node.registry.lookup(msg.pointId);
|
|
19
|
+
if (!currentEntry) {
|
|
20
|
+
return utils.sendError(node, msg, done, `Not Registered: ${msg.pointId}`, msg.pointId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const currentPath = currentEntry.path;
|
|
24
|
+
const currentStore = currentEntry.store || "default";
|
|
25
|
+
|
|
26
|
+
// Async Get
|
|
27
|
+
let globalData = await utils.getGlobalState(node, currentPath, currentStore);
|
|
28
|
+
|
|
29
|
+
if (!globalData || Object.keys(globalData).length === 0) {
|
|
30
|
+
return utils.sendError(node, msg, done, `Global Data Empty: ${msg.pointId}`, msg.pointId);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
msg = { ...globalData };
|
|
34
|
+
|
|
35
|
+
const ptName = msg.metadata?.name ?? "Unknown";
|
|
36
|
+
const ptVal = msg.value !== undefined ? msg.value : "No Value";
|
|
37
|
+
const ptId = msg.network?.pointId ?? msg.pointId;
|
|
38
|
+
|
|
39
|
+
const msgText = `Data Found. pointId: ${ptId} value: ${ptVal}`;
|
|
40
|
+
|
|
41
|
+
utils.sendSuccess(node, msg, done, msgText, ptId, "ring");
|
|
42
|
+
|
|
43
|
+
} catch (err) {
|
|
44
|
+
node.error(err);
|
|
45
|
+
utils.sendError(node, msg, done, `Internal Error: ${err.message}`, msg?.pointId);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
node.on('close', function(removed, done) {
|
|
50
|
+
if (removed && node.registry) {
|
|
51
|
+
node.registry.unregister(node.pointId, node.id);
|
|
52
|
+
}
|
|
53
|
+
done();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
RED.nodes.registerType("network-read", NetworkReadNode);
|
|
57
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="network-register">
|
|
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-registry" title="A group of id's for assignment organization"><i class="fa fa-book"></i> Registry</label>
|
|
8
|
+
<input type="text" id="node-input-registry">
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div class="form-row">
|
|
12
|
+
<label for="node-input-pointId" title="Simple, stable, and unique lookup id for network users."><i class="fa fa-hashtag"></i> Point ID</label>
|
|
13
|
+
<div style="display: inline-block; width: 70%;">
|
|
14
|
+
<input type="number" id="node-input-pointId" placeholder="e.g. 101" style="width: 100px;">
|
|
15
|
+
<span id="point-id-status" style="margin-left: 10px; display: none;"></span>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="form-row">
|
|
20
|
+
<label for="node-input-writable"><i class="fa fa-pencil"></i> Writable</label>
|
|
21
|
+
<input type="checkbox" id="node-input-writable" style="display: inline-block; width: auto; vertical-align: top;">
|
|
22
|
+
<span style="color: #666;"> Allow network to write to this point?</span>
|
|
23
|
+
</div>
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<script type="text/javascript">
|
|
27
|
+
RED.nodes.registerType('network-register', {
|
|
28
|
+
category: 'bldgblocks network',
|
|
29
|
+
color: '#3090C7',
|
|
30
|
+
defaults: {
|
|
31
|
+
name: { value: "" },
|
|
32
|
+
registry: { value: "", type: "network-point-registry", required: true },
|
|
33
|
+
pointId: { value: null, required: true, validate: RED.validators.number() },
|
|
34
|
+
writable: { value: false }
|
|
35
|
+
},
|
|
36
|
+
inputs: 1,
|
|
37
|
+
outputs: 1,
|
|
38
|
+
icon: "font-awesome/fa-book",
|
|
39
|
+
label: function() {
|
|
40
|
+
const id = this.pointId || "?";
|
|
41
|
+
return `(id:${id}) ${this.name || "network register"}`;
|
|
42
|
+
},
|
|
43
|
+
paletteLabel: "network register",
|
|
44
|
+
oneditprepare: function() {
|
|
45
|
+
const nodeId = this.id;
|
|
46
|
+
const statusSpan = $("#point-id-status");
|
|
47
|
+
const idInput = $("#node-input-pointId");
|
|
48
|
+
const regInput = $("#node-input-registry");
|
|
49
|
+
|
|
50
|
+
let timer = null;
|
|
51
|
+
|
|
52
|
+
function checkId() {
|
|
53
|
+
const node = this;
|
|
54
|
+
const pid = idInput.val();
|
|
55
|
+
const rid = regInput.val();
|
|
56
|
+
|
|
57
|
+
statusSpan.hide();
|
|
58
|
+
idInput.removeClass("input-error");
|
|
59
|
+
|
|
60
|
+
if (timer) clearTimeout(timer);
|
|
61
|
+
|
|
62
|
+
// Need both ID and Registry selected to validate
|
|
63
|
+
if (pid && rid && rid !== "_ADD_") {
|
|
64
|
+
timer = setTimeout(() => {
|
|
65
|
+
// Pass Registry ID in URL
|
|
66
|
+
$.getJSON(`network-point-registry/check/${rid}/${pid}/${nodeId}`, function(data) {
|
|
67
|
+
if (data.warning) {
|
|
68
|
+
// Registry exists in config but not deployed
|
|
69
|
+
statusSpan.html(`<i class="fa fa-question-circle" style="color: orange;" title="${data.warning}"></i> Not Deployed`).show();
|
|
70
|
+
} else if (data.status === "collision") {
|
|
71
|
+
// Conflict
|
|
72
|
+
statusSpan.html('<i class="fa fa-exclamation-triangle" style="color: red;"></i> ID Taken').show();
|
|
73
|
+
idInput.addClass("input-error");
|
|
74
|
+
} else if (data.status === "assigned") {
|
|
75
|
+
// Assigned here
|
|
76
|
+
statusSpan.html('<i class="fa fa-check" style="color: green;"></i> Assigned').show();
|
|
77
|
+
} else {
|
|
78
|
+
// Available
|
|
79
|
+
statusSpan.html('<i class="fa fa-check" style="color: green;"></i> Available').show();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}, 500);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function loadNextFreeId(registry) {
|
|
87
|
+
if (!registry || registry === "_ADD_") { return; }
|
|
88
|
+
$.getJSON(`/network-point-registry/list/${registry}`, function (data) {
|
|
89
|
+
const next = data.reduce((max, pt) => Math.max(max, pt.id), 0) + 1;
|
|
90
|
+
idInput.val(next);
|
|
91
|
+
}).fail(function () {
|
|
92
|
+
statusSpan.html('<i class="fa fa-exclamation-triangle" style="color:red;"></i> Cannot fetch points').show();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Trigger check on ID type or Registry Change
|
|
97
|
+
idInput.on("input", checkId);
|
|
98
|
+
regInput.on("change", function () {
|
|
99
|
+
checkId();
|
|
100
|
+
if (idInput.val().trim() === "") {
|
|
101
|
+
loadNextFreeId(regInput.val());
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (idInput.val().trim() === "") {
|
|
106
|
+
loadNextFreeId(regInput.val());
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
</script>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
const utils = require('./utils')(RED);
|
|
3
|
+
function NetworkRegisterNode(config) {
|
|
4
|
+
RED.nodes.createNode(this, config);
|
|
5
|
+
const node = this;
|
|
6
|
+
|
|
7
|
+
node.registry = RED.nodes.getNode(config.registry);
|
|
8
|
+
node.pointId = parseInt(config.pointId);
|
|
9
|
+
node.writable = !!config.writable;
|
|
10
|
+
node.isRegistered = false;
|
|
11
|
+
|
|
12
|
+
// Initial Registration
|
|
13
|
+
if (node.registry && !isNaN(node.pointId)) {
|
|
14
|
+
const success = node.registry.register(node.pointId, {
|
|
15
|
+
nodeId: node.id,
|
|
16
|
+
writable: node.writable,
|
|
17
|
+
path: "not ready",
|
|
18
|
+
store: "not ready"
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (success) {
|
|
22
|
+
node.isRegistered = true;
|
|
23
|
+
node.status({ fill: "yellow", shape: "ring", text: `ID: ${node.pointId} (Waiting)` });
|
|
24
|
+
} else {
|
|
25
|
+
node.error(`Point ID ${node.pointId} is already in use.`);
|
|
26
|
+
node.status({ fill: "red", shape: "dot", text: "ID Conflict" });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
node.on("input", async function(msg, send, done) {
|
|
31
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Pre-flight
|
|
35
|
+
if (!node.isRegistered) return utils.sendError(node, null, done, "Node not registered");
|
|
36
|
+
if (!msg || typeof msg !== "object") return utils.sendError(node, null, done, "Invalid msg object");
|
|
37
|
+
if (!node.registry) return utils.sendError(node, msg, done, "Registry config missing", node.pointId);
|
|
38
|
+
|
|
39
|
+
// Validate Fields
|
|
40
|
+
if (!msg.activePriority || !msg.metadata?.path || !msg.metadata?.store) {
|
|
41
|
+
return utils.sendError(node, msg, done, "Missing required fields (metadata.path/store, activePriority)", node.pointId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Logic & State Update
|
|
45
|
+
let pointData = node.registry.lookup(node.pointId);
|
|
46
|
+
|
|
47
|
+
const incoming = {
|
|
48
|
+
writable: node.writable,
|
|
49
|
+
path: msg.metadata.path,
|
|
50
|
+
store: msg.metadata.store
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const needsUpdate = !pointData
|
|
54
|
+
|| pointData.nodeId !== node.id
|
|
55
|
+
|| pointData.writable !== incoming.writable
|
|
56
|
+
|| pointData.path !== incoming.path
|
|
57
|
+
|| pointData.store !== incoming.store;
|
|
58
|
+
|
|
59
|
+
if (needsUpdate) {
|
|
60
|
+
node.registry.register(node.pointId, {
|
|
61
|
+
nodeId: node.id,
|
|
62
|
+
writable: node.writable,
|
|
63
|
+
path: incoming.path,
|
|
64
|
+
store: incoming.store
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
pointData = node.registry.lookup(node.pointId);
|
|
68
|
+
const currentStore = pointData.store || "default";
|
|
69
|
+
|
|
70
|
+
// Async Get
|
|
71
|
+
const globalData = await utils.getGlobalState(node, pointData.path, currentStore);
|
|
72
|
+
|
|
73
|
+
if (!globalData || Object.keys(globalData).length === 0) {
|
|
74
|
+
return utils.sendError(node, msg, done, `Global missing: (${currentStore})::${pointData.path}`, node.pointId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const networkObject = {
|
|
78
|
+
...globalData,
|
|
79
|
+
network: {
|
|
80
|
+
registry: node.registry.name,
|
|
81
|
+
pointId: node.pointId,
|
|
82
|
+
writable: node.writable
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Async Set
|
|
87
|
+
await utils.setGlobalState(node, pointData.path, currentStore, networkObject);
|
|
88
|
+
|
|
89
|
+
const statusText = `Registered: (${currentStore})::${pointData.path}::${node.pointId}`;
|
|
90
|
+
return utils.sendSuccess(node, networkObject, done, statusText, node.pointId, "dot");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Passthrough
|
|
94
|
+
const prefix = msg.activePriority === 'default' ? '' : 'P';
|
|
95
|
+
const statusText = `Passthrough: ${prefix}${msg.activePriority}:${msg.value}${msg.units}`;
|
|
96
|
+
utils.sendSuccess(node, msg, done, statusText, node.pointId, "ring");
|
|
97
|
+
|
|
98
|
+
} catch (err) {
|
|
99
|
+
node.error(err);
|
|
100
|
+
utils.sendError(node, msg, done, `Internal Error: ${err.message}`, node.pointId);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
node.on('close', function(removed, done) {
|
|
105
|
+
if (removed && node.registry && node.isRegistered) {
|
|
106
|
+
node.registry.unregister(node.pointId, node.pointId);
|
|
107
|
+
}
|
|
108
|
+
done();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
RED.nodes.registerType("network-register", NetworkRegisterNode);
|
|
112
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="network-write">
|
|
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-registry"><i class="fa fa-book"></i> Registry</label>
|
|
8
|
+
<input type="text" id="node-input-registry">
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div class="form-tips">
|
|
12
|
+
<b>Input Payload Format:</b><br>
|
|
13
|
+
<pre>
|
|
14
|
+
{
|
|
15
|
+
"action": "write",
|
|
16
|
+
"pointId": 101,
|
|
17
|
+
"priority": 8,
|
|
18
|
+
"value": 75.5 // or null || "null" to release
|
|
19
|
+
}
|
|
20
|
+
</pre>
|
|
21
|
+
</div>
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<script type="text/javascript">
|
|
25
|
+
RED.nodes.registerType('network-write', {
|
|
26
|
+
category: 'bldgblocks network',
|
|
27
|
+
color: '#3090C7',
|
|
28
|
+
defaults: {
|
|
29
|
+
name: { value: "" },
|
|
30
|
+
registry: { value: "", type: "network-point-registry", required: true }
|
|
31
|
+
},
|
|
32
|
+
inputs: 1,
|
|
33
|
+
outputs: 1,
|
|
34
|
+
icon: "font-awesome/fa-list-ol",
|
|
35
|
+
label: function() {
|
|
36
|
+
return this.name || "network write";
|
|
37
|
+
},
|
|
38
|
+
paletteLabel: "network write",
|
|
39
|
+
oneditprepare: function() {
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<script type="text/markdown" data-help-name="network-write">
|
|
46
|
+
Writes network commands to Global Variables using the Priority Array logic.
|
|
47
|
+
|
|
48
|
+
### Input
|
|
49
|
+
: payload (object) : A command object containing
|
|
50
|
+
* `action` (string): Not used by the node, used for routing to the correct node over the network when received.
|
|
51
|
+
* `pointId` (number): The integer ID of the point.
|
|
52
|
+
* `priority` (number): The priority level (1-16) to write to.
|
|
53
|
+
* `value` (any): The value to set. Send `null` to relinquish (clear) this priority level.
|
|
54
|
+
|
|
55
|
+
### Output
|
|
56
|
+
: payload (object) : Confirmation object containing status, pointId, and the new calculated "Winner" value.
|
|
57
|
+
|
|
58
|
+
### Details
|
|
59
|
+
This node acts as the inbound gateway.
|
|
60
|
+
1. It looks up the `pointId` in the selected **Registry** to find the corresponding Global Variable path.
|
|
61
|
+
2. It fetches the current State Object.
|
|
62
|
+
3. It updates the specific slot in the `priority` array based on the command.
|
|
63
|
+
4. It recalculates the "Present Value" (highest priority active).
|
|
64
|
+
5. It saves the Global Variable and emits an update event, triggering any reactive Getters immediately.
|
|
65
|
+
</script>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
const utils = require('./utils')(RED);
|
|
3
|
+
function NetworkWriteNode(config) {
|
|
4
|
+
RED.nodes.createNode(this, config);
|
|
5
|
+
const node = this;
|
|
6
|
+
node.registry = RED.nodes.getNode(config.registry);
|
|
7
|
+
|
|
8
|
+
node.on("input", async function(msg, send, done) {
|
|
9
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
// Validation
|
|
13
|
+
if (!msg || !msg.pointId || !msg.priority || msg.value === undefined) {
|
|
14
|
+
return utils.sendError(node, msg, done, "Invalid msg properties", msg?.pointId);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Registry Lookup
|
|
18
|
+
const entry = node.registry.lookup(msg.pointId);
|
|
19
|
+
if (!entry?.path) {
|
|
20
|
+
const store = entry?.store ?? "unknown";
|
|
21
|
+
return utils.sendError(node, msg, done, `Unknown ID: (${store})::${msg.pointId}`, msg.pointId);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { store = "default", path, writable } = entry;
|
|
25
|
+
|
|
26
|
+
if (!writable) {
|
|
27
|
+
return utils.sendError(node, msg, done, `Not Writable: (${store})::${path}::${msg.pointId}`, msg.pointId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Get State (Async)
|
|
31
|
+
let state = await utils.getGlobalState(node, path, store);
|
|
32
|
+
|
|
33
|
+
if (!state || !state.priority) {
|
|
34
|
+
return utils.sendError(node, msg, done, `Point Not Found: (${store})::${path}`, msg.pointId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Type Check
|
|
38
|
+
let newValue = msg.value === "null" || msg.value === null ? null : msg.value;
|
|
39
|
+
if (newValue !== null) {
|
|
40
|
+
const dataType = state.metadata?.type;
|
|
41
|
+
if (dataType && typeof newValue !== dataType) {
|
|
42
|
+
return utils.sendError(node, msg, done, `Type Mismatch: Expected ${dataType}`, msg.pointId);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Update Priority Logic
|
|
47
|
+
if (msg.priority === 'default') {
|
|
48
|
+
state.defaultValue = newValue ?? state.defaultValue;
|
|
49
|
+
} else {
|
|
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;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Calculate Winner
|
|
58
|
+
const result = utils.getHighestPriority(state);
|
|
59
|
+
state.value = result.value;
|
|
60
|
+
state.activePriority = result.priority;
|
|
61
|
+
state.metadata.lastSet = new Date().toISOString();
|
|
62
|
+
|
|
63
|
+
// Save (Async) & Emit
|
|
64
|
+
await utils.setGlobalState(node, path, store, state);
|
|
65
|
+
|
|
66
|
+
const prefixReq = msg.priority === 'default' ? '' : 'P';
|
|
67
|
+
const prefixAct = state.activePriority === 'default' ? '' : 'P';
|
|
68
|
+
const statusMsg = `Wrote: ${prefixReq}${msg.priority}:${newValue} > Active: ${prefixAct}${state.activePriority}:${state.value}`;
|
|
69
|
+
|
|
70
|
+
msg = { ...state, status: null };
|
|
71
|
+
|
|
72
|
+
RED.events.emit("bldgblocks-global-update", { key: path, store: store, data: state });
|
|
73
|
+
|
|
74
|
+
utils.sendSuccess(node, msg, done, statusMsg, msg.pointId, "ring");
|
|
75
|
+
|
|
76
|
+
} catch (err) {
|
|
77
|
+
node.error(err);
|
|
78
|
+
utils.sendError(node, msg, done, `Internal Error: ${err.message}`, msg?.pointId);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
RED.nodes.registerType("network-write", NetworkWriteNode);
|
|
83
|
+
}
|
package/nodes/nullify-block.html
CHANGED
package/nodes/oneshot-block.html
CHANGED
package/nodes/or-block.html
CHANGED
package/nodes/pid-block.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<!-- JavaScript Section: Registers the node and handles editor logic -->
|
|
10
10
|
<script type="text/javascript">
|
|
11
11
|
RED.nodes.registerType("priority-block", {
|
|
12
|
-
category: "control",
|
|
12
|
+
category: "bldgblocks control",
|
|
13
13
|
color: "#301934",
|
|
14
14
|
defaults: {
|
|
15
15
|
name: { value: "" }
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
<!-- JavaScript Section -->
|
|
30
30
|
<script type="text/javascript">
|
|
31
31
|
RED.nodes.registerType("rate-limit-block", {
|
|
32
|
-
category: "control",
|
|
32
|
+
category: "bldgblocks control",
|
|
33
33
|
color: "#301934",
|
|
34
34
|
defaults: {
|
|
35
35
|
name: { value: "" },
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
outputs: 1,
|
|
43
43
|
inputLabels: ["value"],
|
|
44
44
|
outputLabels: ["processed value"],
|
|
45
|
-
icon: "font-awesome/fa-
|
|
45
|
+
icon: "font-awesome/fa-circle-up",
|
|
46
46
|
paletteLabel: "rate limit",
|
|
47
47
|
label: function() {
|
|
48
48
|
return this.name || this.mode || "rate limit";
|