@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.
Files changed (71) hide show
  1. package/nodes/accumulate-block.html +1 -1
  2. package/nodes/add-block.html +1 -1
  3. package/nodes/analog-switch-block.html +1 -1
  4. package/nodes/and-block.html +1 -1
  5. package/nodes/average-block.html +1 -1
  6. package/nodes/boolean-switch-block.html +1 -1
  7. package/nodes/boolean-to-number-block.html +1 -1
  8. package/nodes/cache-block.html +1 -1
  9. package/nodes/call-status-block.html +1 -1
  10. package/nodes/changeover-block.html +1 -1
  11. package/nodes/comment-block.html +1 -1
  12. package/nodes/compare-block.html +1 -1
  13. package/nodes/contextual-label-block.html +1 -1
  14. package/nodes/convert-block.html +1 -1
  15. package/nodes/count-block.html +2 -2
  16. package/nodes/delay-block.html +1 -1
  17. package/nodes/divide-block.html +1 -1
  18. package/nodes/edge-block.html +1 -1
  19. package/nodes/enum-switch-block.html +1 -1
  20. package/nodes/frequency-block.html +1 -1
  21. package/nodes/global-getter.html +34 -3
  22. package/nodes/global-getter.js +71 -45
  23. package/nodes/global-setter.html +21 -10
  24. package/nodes/global-setter.js +154 -79
  25. package/nodes/history-collector.html +283 -0
  26. package/nodes/history-collector.js +150 -0
  27. package/nodes/history-config.html +236 -0
  28. package/nodes/history-config.js +8 -0
  29. package/nodes/hysteresis-block.html +1 -1
  30. package/nodes/interpolate-block.html +1 -1
  31. package/nodes/latch-block.html +1 -1
  32. package/nodes/load-sequence-block.html +1 -1
  33. package/nodes/max-block.html +1 -1
  34. package/nodes/memory-block.html +1 -1
  35. package/nodes/min-block.html +1 -1
  36. package/nodes/minmax-block.html +1 -1
  37. package/nodes/modulo-block.html +1 -1
  38. package/nodes/multiply-block.html +1 -1
  39. package/nodes/negate-block.html +1 -1
  40. package/nodes/network-point-registry.html +86 -0
  41. package/nodes/network-point-registry.js +90 -0
  42. package/nodes/network-read.html +56 -0
  43. package/nodes/network-read.js +59 -0
  44. package/nodes/network-register.html +110 -0
  45. package/nodes/network-register.js +161 -0
  46. package/nodes/network-write.html +64 -0
  47. package/nodes/network-write.js +126 -0
  48. package/nodes/nullify-block.html +1 -1
  49. package/nodes/on-change-block.html +1 -1
  50. package/nodes/oneshot-block.html +1 -1
  51. package/nodes/or-block.html +1 -1
  52. package/nodes/pid-block.html +1 -1
  53. package/nodes/priority-block.html +1 -1
  54. package/nodes/rate-limit-block.html +2 -2
  55. package/nodes/rate-of-change-block.html +1 -1
  56. package/nodes/rate-of-change-block.js +5 -2
  57. package/nodes/round-block.html +6 -5
  58. package/nodes/round-block.js +5 -3
  59. package/nodes/saw-tooth-wave-block.html +2 -2
  60. package/nodes/scale-range-block.html +1 -1
  61. package/nodes/sine-wave-block.html +2 -2
  62. package/nodes/string-builder-block.html +1 -1
  63. package/nodes/subtract-block.html +1 -1
  64. package/nodes/thermistor-block.html +1 -1
  65. package/nodes/tick-tock-block.html +2 -2
  66. package/nodes/time-sequence-block.html +1 -1
  67. package/nodes/triangle-wave-block.html +2 -2
  68. package/nodes/tstat-block.html +1 -1
  69. package/nodes/units-block.html +8 -38
  70. package/nodes/units-block.js +3 -42
  71. 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
+ }
@@ -11,7 +11,7 @@
11
11
 
12
12
  <script type="text/javascript">
13
13
  RED.nodes.registerType("nullify-block", {
14
- category: "control",
14
+ category: "bldgblocks control",
15
15
  color: "#301934",
16
16
  defaults: {
17
17
  name: { value: "" },