@bldgblocks/node-red-contrib-control 0.1.32 → 0.1.33
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 +71 -45
- package/nodes/global-setter.html +21 -10
- package/nodes/global-setter.js +154 -79
- 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 +59 -0
- package/nodes/network-register.html +110 -0
- package/nodes/network-register.js +161 -0
- package/nodes/network-write.html +64 -0
- package/nodes/network-write.js +126 -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/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
|
+
: payload (object) : A command object containing
|
|
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,59 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function NetworkReadNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
node.registry = RED.nodes.getNode(config.registry);
|
|
7
|
+
|
|
8
|
+
node.on("input", function(msg, send, done) {
|
|
9
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
10
|
+
|
|
11
|
+
let currentPath = null;
|
|
12
|
+
let currentStore = "default";
|
|
13
|
+
|
|
14
|
+
if (node.registry) {
|
|
15
|
+
let currentEntry = node.registry.lookup(msg.pointId);
|
|
16
|
+
|
|
17
|
+
if (!currentEntry) {
|
|
18
|
+
node.status({ fill: "red", shape: "ring", text: `Requested pointId not registered` });
|
|
19
|
+
msg.status = { status: "fail", pointId: msg.pointId, error: `Point Not Registered: ${msg.pointId}` };
|
|
20
|
+
node.send(msg);
|
|
21
|
+
if (done) done();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
currentPath = currentEntry.path;
|
|
26
|
+
currentStore = currentEntry.store || "default";
|
|
27
|
+
let globalData = node.context().global.get(currentPath, currentStore) || {};
|
|
28
|
+
|
|
29
|
+
if (globalData === null || Object.keys(globalData).length === 0) {
|
|
30
|
+
node.status({ fill: "red", shape: "ring", text: `Global data doesn't exist, waiting...` });
|
|
31
|
+
msg.status = { status: "fail", pointId: msg.pointId, error: `Point Not Found: ${msg.pointId}` };
|
|
32
|
+
node.send(msg);
|
|
33
|
+
if (done) done();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
msg = { ...globalData };
|
|
38
|
+
node.status({ fill: "blue", shape: "ring", text: `Read (${currentStore})::${msg.metadata.name}::${msg.network.pointId} ` });
|
|
39
|
+
msg.status = { status: "ok", pointId: msg.network.pointId, message: `Data Found. pointId: ${msg.network.pointId} value: ${msg.value}` };
|
|
40
|
+
node.send(msg);
|
|
41
|
+
|
|
42
|
+
if (done) done();
|
|
43
|
+
} else {
|
|
44
|
+
node.status({ fill: "red", shape: "ring", text: `Registry not found. Create config node.` });
|
|
45
|
+
if (done) done();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Cleanup
|
|
51
|
+
node.on('close', function(removed, done) {
|
|
52
|
+
if (removed && node.registry) {
|
|
53
|
+
node.registry.unregister(node.pointId, node.id);
|
|
54
|
+
}
|
|
55
|
+
done();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
RED.nodes.registerType("network-read", NetworkReadNode);
|
|
59
|
+
}
|
|
@@ -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,161 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function NetworkRegisterNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
// Config
|
|
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, // for point registry collision checks
|
|
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: "blue", 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", function(msg, send, done) {
|
|
31
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
32
|
+
|
|
33
|
+
// Nothing to do. Return.
|
|
34
|
+
if (!node.isRegistered) {
|
|
35
|
+
node.status({ fill: "red", shape: "ring", text: `Not registered` });
|
|
36
|
+
if (done) done();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!msg || typeof msg !== "object") {
|
|
41
|
+
const message = `Invalid msg.`;
|
|
42
|
+
node.status({ fill: "red", shape: "ring", text: `${message}` });
|
|
43
|
+
if (done) done();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!node.registry) {
|
|
48
|
+
const message = `Registry not found. Create config node.`;
|
|
49
|
+
node.status({ fill: "red", shape: "ring", text: `${message}` });
|
|
50
|
+
msg.status = { status: "fail", pointId: node.pointId, error: `${message}` };
|
|
51
|
+
node.send(msg);
|
|
52
|
+
if (done) done();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Message should contain data & metadata from a global setter node
|
|
57
|
+
const missingFields = [];
|
|
58
|
+
|
|
59
|
+
if (!msg.metadata) missingFields.push("metadata");
|
|
60
|
+
if (msg.value === undefined) missingFields.push("value");
|
|
61
|
+
if (msg.units === undefined) missingFields.push("units");
|
|
62
|
+
if (!msg.activePriority) missingFields.push("activePriority");
|
|
63
|
+
|
|
64
|
+
// Check nested metadata properties
|
|
65
|
+
if (msg.metadata) {
|
|
66
|
+
if (!msg.metadata.path) missingFields.push("metadata.path");
|
|
67
|
+
if (!msg.metadata.store) missingFields.push("metadata.store");
|
|
68
|
+
if (!msg.metadata.sourceId) missingFields.push("metadata.sourceId");
|
|
69
|
+
} else {
|
|
70
|
+
missingFields.push("metadata (entire object)");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (missingFields.length > 0) {
|
|
74
|
+
const specificMessage = `Missing required fields: ${missingFields.join(', ')}`;
|
|
75
|
+
node.status({
|
|
76
|
+
fill: "red",
|
|
77
|
+
shape: "ring",
|
|
78
|
+
text: `${missingFields.length} missing: ${missingFields.slice(0, 3).join(', ')}${missingFields.length > 3 ? '...' : ''}`
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
node.send(msg);
|
|
82
|
+
if (done) done();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
// Lookup current registration
|
|
88
|
+
let pointData = node.registry.lookup(node.pointId);
|
|
89
|
+
|
|
90
|
+
const incoming = {
|
|
91
|
+
writable: node.writable,
|
|
92
|
+
path: msg.metadata.path,
|
|
93
|
+
store: msg.metadata.store
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Update Registry on change
|
|
97
|
+
if (!pointData
|
|
98
|
+
|| pointData.nodeId !== node.nodeId
|
|
99
|
+
|| pointData.writable !== incoming.writable
|
|
100
|
+
|| pointData.path !== incoming.path
|
|
101
|
+
|| pointData.store !== incoming.store) {
|
|
102
|
+
|
|
103
|
+
node.registry.register(node.pointId, {
|
|
104
|
+
nodeId: node.id, // for point registry collision checks
|
|
105
|
+
writable: node.writable,
|
|
106
|
+
path: msg.metadata.path,
|
|
107
|
+
store: msg.metadata.store
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
pointData = node.registry.lookup(node.pointId);
|
|
111
|
+
|
|
112
|
+
let globalData = {};
|
|
113
|
+
globalData = node.context().global.get(pointData.path, pointData.store);
|
|
114
|
+
|
|
115
|
+
if (globalData === null || Object.keys(globalData).length === 0) {
|
|
116
|
+
const message = `Global data doesn't exist for (${pointData.store ?? "default"})::${pointData.path}::${node.pointId}`;
|
|
117
|
+
node.status({ fill: "red", shape: "ring", text: `${message}` });
|
|
118
|
+
msg.status = { status: "fail", pointId: node.pointId, error: `${message}` };
|
|
119
|
+
if (done) done();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let network = {
|
|
124
|
+
registry: node.registry.name,
|
|
125
|
+
pointId: node.pointId,
|
|
126
|
+
writable: node.writable
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const networkObject = { ...globalData, network: network};
|
|
130
|
+
const message = `Registered: (${pointData.store ?? "default"})::${pointData.path}::${node.pointId}`;
|
|
131
|
+
|
|
132
|
+
node.context().global.set(pointData.path, networkObject, pointData.store);
|
|
133
|
+
node.status({ fill: "blue", shape: "dot", text: `${message}` });
|
|
134
|
+
msg.status = { status: "success", pointId: node.pointId, error: `${message}` };
|
|
135
|
+
|
|
136
|
+
node.send(networkObject);
|
|
137
|
+
if (done) done();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Make it here, then message should match global and ready to go
|
|
142
|
+
// Pass through msg
|
|
143
|
+
const prefix = msg.activePriority === 'default' ? '' : 'P';
|
|
144
|
+
const message = `Passthrough: ${prefix}${msg.activePriority}:${msg.value}${msg.units}`;
|
|
145
|
+
node.status({ fill: "blue", shape: "ring", text: message });
|
|
146
|
+
|
|
147
|
+
node.send(msg);
|
|
148
|
+
if (done) done();
|
|
149
|
+
return;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Cleanup
|
|
153
|
+
node.on('close', function(removed, done) {
|
|
154
|
+
if (removed && node.registry && node.isRegistered) {
|
|
155
|
+
node.registry.unregister(node.pointId, node.pointId);
|
|
156
|
+
}
|
|
157
|
+
done();
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
RED.nodes.registerType("network-register", NetworkRegisterNode);
|
|
161
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
* `pointId` (number): The integer ID of the point.
|
|
51
|
+
* `priority` (number): The priority level (1-16) to write to.
|
|
52
|
+
* `value` (any): The value to set. Send `null` to relinquish (clear) this priority level.
|
|
53
|
+
|
|
54
|
+
### Output
|
|
55
|
+
: payload (object) : Confirmation object containing status, pointId, and the new calculated "Winner" value.
|
|
56
|
+
|
|
57
|
+
### Details
|
|
58
|
+
This node acts as the inbound gateway.
|
|
59
|
+
1. It looks up the `pointId` in the selected **Registry** to find the corresponding Global Variable path.
|
|
60
|
+
2. It fetches the current State Object.
|
|
61
|
+
3. It updates the specific slot in the `priority` array based on the command.
|
|
62
|
+
4. It recalculates the "Present Value" (highest priority active).
|
|
63
|
+
5. It saves the Global Variable and emits an update event, triggering any reactive Getters immediately.
|
|
64
|
+
</script>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
module.exports = function(RED) {
|
|
2
|
+
function NetworkWriteNode(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
const node = this;
|
|
5
|
+
|
|
6
|
+
node.registry = RED.nodes.getNode(config.registry);
|
|
7
|
+
|
|
8
|
+
node.on("input", function(msg, send, done) {
|
|
9
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
10
|
+
|
|
11
|
+
// Expecting: msg.payload = { pointId, priority, value }
|
|
12
|
+
if (!msg || !msg.pointId || !msg.priority || msg.value === undefined) {
|
|
13
|
+
node.status({ fill: "red", shape: "dot", text: "Invalid msg properties" });
|
|
14
|
+
msg.status = { status: "fail", pointId: msg.pointId, error: `Invalid msg properties` };
|
|
15
|
+
|
|
16
|
+
node.send(msg);
|
|
17
|
+
if (done) done();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Lookup Path
|
|
22
|
+
const entry = node.registry.lookup(msg.pointId);
|
|
23
|
+
const store = entry.store ?? "default";
|
|
24
|
+
const path = entry.path;
|
|
25
|
+
if (!entry || !path) {
|
|
26
|
+
node.status({ fill: "red", shape: "dot", text: `Unknown ID: (${store})::${path}::${msg.pointId}` });
|
|
27
|
+
msg.status = { status: "fail", pointId: msg.pointId, error: `Unknown ID: (${store})::${path}::${msg.pointId}` };
|
|
28
|
+
|
|
29
|
+
node.send(msg);
|
|
30
|
+
if (done) done();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check Writable
|
|
35
|
+
if (!entry.writable) {
|
|
36
|
+
node.status({ fill: "red", shape: "dot", text: `Not Writable: (${store})::${path}::${msg.pointId}` });
|
|
37
|
+
msg.status = { status: "fail", pointId: msg.pointId, error: `Not Writable: (${store})::${path}::${msg.pointId}` };
|
|
38
|
+
|
|
39
|
+
node.send(msg);
|
|
40
|
+
if (done) done();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Get State
|
|
45
|
+
const globalContext = node.context().global;
|
|
46
|
+
let state = globalContext.get(path, store);
|
|
47
|
+
|
|
48
|
+
if (!state || !state.priority) {
|
|
49
|
+
node.status({ fill: "red", shape: "ring", text: `Point Not Found: (${store})::${path}::${msg.pointId}` });
|
|
50
|
+
msg.status = { status: "fail", pointId: msg.pointId, error: `Point Not Found: (${store})::${path}::${msg.pointId}` };
|
|
51
|
+
|
|
52
|
+
node.send(msg);
|
|
53
|
+
if (done) done();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check Type
|
|
58
|
+
if (msg.value === "null" || msg.value === null) {
|
|
59
|
+
msg.value = null;
|
|
60
|
+
} else {
|
|
61
|
+
const inputType = typeof msg.value;
|
|
62
|
+
const dataType = state.metadata.type;
|
|
63
|
+
if (inputType !== dataType) {
|
|
64
|
+
node.status({ fill: "red", shape: "ring", text: `Mismatch type error: ${store}:${path} ID: ${msg.pointId}, ${inputType} !== ${dataType}` });
|
|
65
|
+
msg.status = { status: "fail", pointId: msg.pointId, error: `Mismatch type error: ${store}:${path} ID: ${msg.pointId}, ${inputType} !== ${dataType}` };
|
|
66
|
+
|
|
67
|
+
node.send(msg);
|
|
68
|
+
if (done) done();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Update Priority
|
|
74
|
+
if (msg.priority === 'default') {
|
|
75
|
+
state.defaultValue = msg.value ?? state.defaultValue;
|
|
76
|
+
} else {
|
|
77
|
+
const priority = parseInt(msg.priority, 10);
|
|
78
|
+
if (isNaN(priority) || priority < 1 || priority > 16) {
|
|
79
|
+
node.status({ fill: "red", shape: "ring", text: `Invalid priority: ${msg.priority}` });
|
|
80
|
+
msg.status = { status: "fail", pointId: msg.pointId, error: `Invalid Priority: (${store})::${path}::${msg.pointId}` };
|
|
81
|
+
|
|
82
|
+
node.send(msg);
|
|
83
|
+
if (done) done();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
state.priority[msg.priority] = msg.value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Calculate Winner (Same logic as Setter)
|
|
91
|
+
let winnerValue = state.defaultValue;
|
|
92
|
+
let winnerPriority = 'default'
|
|
93
|
+
for (let i = 1; i <= 16; i++) {
|
|
94
|
+
if (state.priority[i] !== undefined && state.priority[i] !== null) {
|
|
95
|
+
winnerValue = state.priority[i];
|
|
96
|
+
winnerPriority = `${i}`
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
state.value = winnerValue;
|
|
101
|
+
state.activePriority = winnerPriority;
|
|
102
|
+
state.metadata.lastSet = new Date().toISOString();
|
|
103
|
+
|
|
104
|
+
// Save & Emit
|
|
105
|
+
const prefix1 = msg.priority === 'default' ? '' : 'P';
|
|
106
|
+
const prefix2 = state.activePriority === 'default' ? '' : 'P';
|
|
107
|
+
const message = `Wrote: ${prefix1}${msg.priority}:${msg.value} > (${store})::${path}::${msg.pointId} Active: ${prefix2}${winnerPriority}:${winnerValue}`;
|
|
108
|
+
node.status({ fill: "blue", shape: "ring", text: message });
|
|
109
|
+
|
|
110
|
+
globalContext.set(path, state, store);
|
|
111
|
+
msg = { ...state };
|
|
112
|
+
msg.status = { status: "ok", pointId: msg.pointId, message: message };
|
|
113
|
+
|
|
114
|
+
// Trigger global getters to update on new value
|
|
115
|
+
RED.events.emit("bldgblocks-global-update", {
|
|
116
|
+
key: path,
|
|
117
|
+
store: store,
|
|
118
|
+
data: state
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
node.send(msg);
|
|
122
|
+
if (done) done();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
RED.nodes.registerType("network-write", NetworkWriteNode);
|
|
126
|
+
}
|