@bldgblocks/node-red-contrib-control 0.1.31 → 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 +43 -4
  22. package/nodes/global-getter.js +114 -34
  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,150 @@
1
+ module.exports = function(RED) {
2
+ function HistoryCollectorNode(config) {
3
+ RED.nodes.createNode(this, config);
4
+ this.historyConfig = RED.nodes.getNode(config.historyConfig);
5
+ this.seriesName = config.seriesName;
6
+ this.storageType = config.storageType || 'memory';
7
+ this.tags = config.tags || '';
8
+ const node = this;
9
+
10
+ // Parse tags into key-value object
11
+ function parseTags(tagsString) {
12
+ if (!tagsString) return {};
13
+ const tags = {};
14
+ const pairs = tagsString.split(',').map(t => t.trim());
15
+ tags["historyGroup"] = node.historyConfig.name;
16
+ pairs.forEach((pair, index) => {
17
+ if (pair.includes('=') || pair.includes(':')) {
18
+ const [key, value] = pair.includes('=') ? pair.split('=') : pair.split(':');
19
+ if (key && value) tags[key.trim()] = value.trim();
20
+ } else {
21
+ tags[`tag${index}`] = pair;
22
+ }
23
+ });
24
+ return tags;
25
+ }
26
+
27
+ node.on('input', function(msg) {
28
+ // Guard against invalid message
29
+ if (!msg) {
30
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
31
+ node.error('Invalid message received');
32
+ return;
33
+ }
34
+
35
+ // Validate configuration
36
+ if (!node.historyConfig) {
37
+ node.status({ fill: "red", shape: "ring", text: "missing history config" });
38
+ node.error('Missing history configuration', msg);
39
+ return;
40
+ }
41
+ if (!node.seriesName) {
42
+ node.status({ fill: "red", shape: "ring", text: "missing series name" });
43
+ node.error('Missing series name', msg);
44
+ return;
45
+ }
46
+ if (!node.historyConfig.name) {
47
+ node.status({ fill: "red", shape: "ring", text: "missing bucket name" });
48
+ node.error('Missing bucket name in history configuration', msg);
49
+ return;
50
+ }
51
+
52
+ // Validate payload
53
+ let payloadValue = msg.payload;
54
+ let formattedValue;
55
+ if (typeof payloadValue === 'number') {
56
+ formattedValue = isNaN(payloadValue) ? null : payloadValue;
57
+ } else if (typeof payloadValue === 'boolean') {
58
+ formattedValue = payloadValue;
59
+ } else if (typeof payloadValue === 'string') {
60
+ formattedValue = payloadValue;
61
+ if (payloadValue.endsWith('i') && !isNaN(parseInt(payloadValue))) {
62
+ formattedValue = parseInt(payloadValue); // Handle InfluxDB integer format
63
+ }
64
+ } else {
65
+ node.status({ fill: "red", shape: "ring", text: "invalid payload" });
66
+ node.warn(`Invalid payload type: ${typeof payloadValue}`);
67
+ return;
68
+ }
69
+
70
+ if (formattedValue === null) {
71
+ node.status({ fill: "red", shape: "ring", text: "invalid payload" });
72
+ node.warn(`Invalid payload value: ${msg.payload}`);
73
+ return;
74
+ }
75
+
76
+ // Construct line protocol
77
+ const escapedMeasurementName = node.seriesName.replace(/[, =]/g, '\\$&');
78
+ const msNow = Date.now();
79
+ const timestamp = msNow * 1e6;
80
+ const tagsObj = parseTags(node.tags);
81
+ const tagsString = Object.entries(tagsObj)
82
+ .map(([k, v]) => `${k.replace(/[, =]/g, '\\$&')}=${v.replace(/[, =]/g, '\\$&')}`)
83
+ .join(',');
84
+ const valueString = typeof formattedValue === 'string' ? `"${formattedValue}"` : formattedValue;
85
+ const line = `${escapedMeasurementName}${tagsString ? ',' + tagsString : ''} value=${valueString} ${timestamp}`;
86
+
87
+ // Set initial status
88
+ node.status({ fill: "green", shape: "dot", text: "configuration received" });
89
+
90
+ // Handle storage type
91
+ if (node.storageType === 'memory') {
92
+ const contextKey = `history_data_${node.historyConfig.name}`;
93
+ let bucketData = node.context().global.get(contextKey) || [];
94
+ bucketData.push(line);
95
+
96
+ const maxMemoryBytes = (node.historyConfig.maxMemoryMb || 10) * 1024 * 1024;
97
+ let totalSize = Buffer.byteLength(JSON.stringify(bucketData), 'utf8');
98
+ while (totalSize > maxMemoryBytes && bucketData.length > 0) {
99
+ bucketData.shift();
100
+ totalSize = Buffer.byteLength(JSON.stringify(bucketData), 'utf8');
101
+ }
102
+
103
+ node.context().global.set(contextKey, bucketData);
104
+ node.status({ fill: "blue", shape: "dot", text: `stored: ${valueString}` });
105
+ } else if (node.storageType === 'lineProtocol') {
106
+ msg.measurement = escapedMeasurementName;
107
+ msg.payload = line;
108
+ node.send(msg);
109
+ node.status({ fill: "blue", shape: "dot", text: `sent: ${valueString}` });
110
+ } else if (node.storageType === 'object') {
111
+ msg.measurement = escapedMeasurementName;
112
+ msg.payload = {
113
+ measurement: escapedMeasurementName,
114
+ tags: Object.entries(tagsObj).map(([k, v]) => `${k}=${v}`),
115
+ value: formattedValue,
116
+ timestamp: timestamp
117
+ };
118
+ node.send(msg);
119
+ node.status({ fill: "blue", shape: "dot", text: `sent: ${valueString}` });
120
+ } else if (node.storageType === 'objectArray') {
121
+ msg.measurement = escapedMeasurementName;
122
+ msg.timestamp = timestamp;
123
+ msg.payload = [
124
+ {
125
+ value: formattedValue
126
+ },
127
+ tagsObj
128
+ ]
129
+ node.send(msg);
130
+ node.status({ fill: "blue", shape: "dot", text: `sent: ${valueString}` });
131
+ } else if (node.storageType === 'batchObject') {
132
+ msg.payload = {
133
+ measurement: escapedMeasurementName,
134
+ timestamp: timestamp,
135
+ fields: {
136
+ value: formattedValue
137
+ },
138
+ tags: tagsObj
139
+ }
140
+ node.send(msg);
141
+ node.status({ fill: "blue", shape: "dot", text: `sent: ${valueString}` });
142
+ }
143
+ });
144
+
145
+ node.on("close", function(done) {
146
+ done();
147
+ });
148
+ }
149
+ RED.nodes.registerType("history-collector", HistoryCollectorNode);
150
+ };
@@ -0,0 +1,236 @@
1
+ <script type="text/html" data-template-name="history-config">
2
+ <div class="form-row">
3
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
4
+ <input type="text" id="node-config-input-name" placeholder="History Config">
5
+ </div>
6
+
7
+ <div class="form-row">
8
+ <label><i class="fa fa-tags"></i> Tags</label>
9
+ <div style="margin-top: 10px;">
10
+ <table id="node-config-tags-table" style="width:100%;">
11
+ <thead>
12
+ <tr>
13
+ <th>Key</th>
14
+ <th>Value</th>
15
+ <th></th>
16
+ </tr>
17
+ </thead>
18
+ <tbody></tbody>
19
+ </table>
20
+ <button id="node-config-add-tag" type="button" style="margin-top: 10px;">Add Tag</button>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="form-row">
25
+ <label><i class="fa fa-list"></i> Series Configurations</label>
26
+ <div style="margin-top: 10px;">
27
+ <table id="node-config-series-table" style="width:100%;">
28
+ <thead>
29
+ <tr>
30
+ <th>Series Name</th>
31
+ <th>Units</th>
32
+ <th></th>
33
+ </tr>
34
+ </thead>
35
+ <tbody></tbody>
36
+ </table>
37
+ <button id="node-config-add-series" type="button" style="margin-top: 10px;">Add Series</button>
38
+ </div>
39
+ </div>
40
+ </script>
41
+
42
+ <script type="text/javascript">
43
+ RED.nodes.registerType('history-config', {
44
+ category: 'config',
45
+ defaults: {
46
+ series: { value: [], required: true },
47
+ name: { value: "default", required: true },
48
+ tags: { value: [], required: true }
49
+ },
50
+ label: function() {
51
+ return this.name || "History Config";
52
+ },
53
+ paletteLabel: "History Config",
54
+ oneditprepare: function() {
55
+ const node = this;
56
+ const tableBody = $("#node-config-series-table tbody");
57
+ const tagsTableBody = $("#node-config-tags-table tbody");
58
+ const addTagButton = $("#node-config-add-tag");
59
+ const nameInput = $("#node-config-input-name");
60
+ node.tags = node.tags || [];
61
+ node.series = node.series || [];
62
+
63
+ // --- Naming Logic ---
64
+ function sanitizeName(input, fallback) {
65
+ const safeInput = (input || "").toString();
66
+ const cleaned = safeInput.trim().replace(/[^a-zA-Z0-9_]/g, '_');
67
+ return cleaned || fallback;
68
+ }
69
+
70
+ function handleNameInput() {
71
+ const cleanName = sanitizeName(nameInput.val());
72
+ nameInput.val(cleanName);
73
+ }
74
+
75
+ nameInput.val(sanitizeName(node.name));
76
+ nameInput.on("change", function() {
77
+ const cleanName = sanitizeName(nameInput.val());
78
+ nameInput.val(cleanName);
79
+ });
80
+
81
+ // --- Tags Table Logic ---
82
+
83
+ // Helper to ensure tags are always tag0, tag1, tag2...
84
+ function renumberTags() {
85
+ tagsTableBody.find("tr").each(function(index) {
86
+ $(this).find(".node-config-tag-label").text("tag" + index);
87
+ });
88
+ }
89
+
90
+ function addTagRow(data = {}) {
91
+ // Determine label based on current row count (though renumberTags fixes it anyway)
92
+ const currentIndex = tagsTableBody.find("tr").length;
93
+
94
+ const row = $(`
95
+ <tr>
96
+ <td style="vertical-align: middle; text-align: center;">
97
+ <span class="node-config-tag-label" style="font-weight: bold; color: #666;">tag${currentIndex}</span>
98
+ </td>
99
+ <td>
100
+ <input type="text" class="node-config-tag-value" value="${data.tagValue || ''}" style="width: 100%;" placeholder="value" required>
101
+ </td>
102
+ <td>
103
+ <button type="button" class="node-config-remove-tag" style="color: white; background-color: red; border-radius: 2px; border: none; padding: 2px 8px; cursor: pointer;">Remove</button>
104
+ </td>
105
+ </tr>
106
+ `);
107
+
108
+ tagsTableBody.append(row);
109
+
110
+ row.find(".node-config-remove-tag").on("click", () => {
111
+ row.remove();
112
+ renumberTags();
113
+ });
114
+
115
+ renumberTags(); // Ensure specific ordering
116
+ }
117
+
118
+ // Initialize tags table
119
+ (node.tags || []).forEach(addTagRow);
120
+ if (node.tags.length === 0) {
121
+ addTagRow({tagValue: "virtual"});
122
+ addTagRow({tagValue: "physical"});
123
+ addTagRow({tagValue: "input"});
124
+ addTagRow({tagValue: "output"});
125
+ }
126
+
127
+ addTagButton.on("click", () => addTagRow());
128
+
129
+ // --- Series Table Logic ---
130
+
131
+ function addRow(data = {}) {
132
+ const row = $(`
133
+ <tr>
134
+ <td><input type="text" class="node-config-series-name" value="${data.seriesName || ''}" style="width: 100%;" required></td>
135
+ <td><input type="text" class="node-config-series-units" value="${data.seriesUnits || ''}" style="width: 100%;"></td>
136
+ <td><button type="button" class="node-config-remove-row" style="color: white; background-color: red; border-radius: 2px; border: none; padding: 2px 8px; cursor: pointer;">Remove</button></td>
137
+ </tr>
138
+ `);
139
+
140
+ row.find(".node-config-series-name").on("change", function() {
141
+ $(this).val(sanitizeName($(this).val(), ""));
142
+ });
143
+
144
+ tableBody.append(row);
145
+ row.find(".node-config-remove-row").on("click", () => row.remove());
146
+ row.find(".node-config-remove-row").hover(
147
+ function() { $(this).css("background-color", "#cc0000"); },
148
+ function() { $(this).css("background-color", "red"); }
149
+ );
150
+ }
151
+
152
+ (node.series || []).forEach(addRow);
153
+ if (node.series.length === 0) {
154
+ addRow({seriesName: "OutsideTemp", seriesUnits: "°F"});
155
+ }
156
+
157
+ $("#node-config-add-series").on("click", () => addRow());
158
+ },
159
+ oneditsave: function() {
160
+ function cleanSaveStr(str) {
161
+ return (str || "").toString().trim().replace(/[^a-zA-Z0-9_]/g, '_');
162
+ }
163
+
164
+ let hasTagError = false;
165
+ // Save Tags
166
+ const tagsTableBody = $("#node-config-tags-table tbody");
167
+ const tags = [];
168
+
169
+ tagsTableBody.find("tr").each(function(index) {
170
+ const tagName = "tag" + index;
171
+ const tagValue = $(this).find(".node-config-tag-value").val().trim();
172
+
173
+ if (!tagValue) {
174
+ RED.notify(`Tag Value is required for ${tagName}`, "error");
175
+ hasTagError = true;
176
+ return false; // Break the loop
177
+ }
178
+
179
+ tags.push({
180
+ tagName: tagName,
181
+ tagValue: tagValue
182
+ });
183
+ });
184
+
185
+ // Stop save if tags are invalid
186
+ if (hasTagError) {
187
+ throw new Error("Tag validation failed");
188
+ }
189
+
190
+ this.tags = tags;
191
+
192
+ // Save Series
193
+ const tableBody = $("#node-config-series-table tbody");
194
+ const series = [];
195
+ const seriesNames = new Set();
196
+
197
+ let hasError = false;
198
+ tableBody.find("tr").each(function() {
199
+ const rawName = $(this).find(".node-config-series-name").val();
200
+ const seriesName = cleanSaveStr(rawName);
201
+
202
+ if (!seriesName) {
203
+ RED.notify("Series Name is required for all series", "error");
204
+ hasError = true;
205
+ return false;
206
+ }
207
+ if (seriesNames.has(seriesName)) {
208
+ RED.notify(`Duplicate Series Name: ${seriesName}`, "error");
209
+ hasError = true;
210
+ return false;
211
+ }
212
+ seriesNames.add(seriesName);
213
+ series.push({
214
+ seriesName: seriesName,
215
+ seriesUnits: $(this).find(".node-config-series-units").val(),
216
+ });
217
+ });
218
+
219
+ if (hasError) {
220
+ throw new Error("Validation failed");
221
+ }
222
+ if (series.length === 0) {
223
+ RED.notify("At least one valid series is required", "error");
224
+ throw new Error("No valid series");
225
+ }
226
+ this.series = series;
227
+ this.name = cleanSaveStr($("#node-config-input-name").val()) || 'default';
228
+ }
229
+ });
230
+ </script>
231
+
232
+ <script type="text/markdown" data-help-name="history-config">
233
+ Store configuration for history series selections.
234
+
235
+
236
+ </script>
@@ -0,0 +1,8 @@
1
+ module.exports = function(RED) {
2
+ function HistoryConfigNode(n) {
3
+ RED.nodes.createNode(this, n);
4
+ this.series = n.series || [];
5
+ this.name = n.name ? n.name.replace(/[^a-zA-Z0-9_]/g, '_') : 'default';
6
+ }
7
+ RED.nodes.registerType("history-config", HistoryConfigNode);
8
+ };
@@ -30,7 +30,7 @@
30
30
  <!-- JavaScript Section: Registers the node and handles editor logic -->
31
31
  <script type="text/javascript">
32
32
  RED.nodes.registerType("hysteresis-block", {
33
- category: "control",
33
+ category: "bldgblocks control",
34
34
  color: "#301934",
35
35
  defaults: {
36
36
  name: { value: "" },
@@ -11,7 +11,7 @@
11
11
 
12
12
  <script type="text/javascript">
13
13
  RED.nodes.registerType("interpolate-block", {
14
- category: "control",
14
+ category: "bldgblocks control",
15
15
  color: "#301934",
16
16
  defaults: {
17
17
  name: { value: "" },
@@ -9,7 +9,7 @@
9
9
  <!-- JavaScript Section -->
10
10
  <script type="text/javascript">
11
11
  RED.nodes.registerType("latch-block", {
12
- category: "control",
12
+ category: "bldgblocks control",
13
13
  color: "#301934",
14
14
  defaults: {
15
15
  name: { value: "" },
@@ -49,7 +49,7 @@
49
49
  <!-- JavaScript Section: Registers the node and handles editor logic -->
50
50
  <script type="text/javascript">
51
51
  RED.nodes.registerType("load-sequence-block", {
52
- category: "control",
52
+ category: "bldgblocks control",
53
53
  color: "#301934",
54
54
  defaults: {
55
55
  name: { value: "" },
@@ -14,7 +14,7 @@
14
14
  <!-- JavaScript Section: Registers the node and handles editor logic -->
15
15
  <script type="text/javascript">
16
16
  RED.nodes.registerType("max-block", {
17
- category: "control",
17
+ category: "bldgblocks control",
18
18
  color: "#301934",
19
19
  defaults: {
20
20
  name: { value: "" },
@@ -22,7 +22,7 @@
22
22
  <!-- JavaScript Section -->
23
23
  <script type="text/javascript">
24
24
  RED.nodes.registerType("memory-block", {
25
- category: "control",
25
+ category: "bldgblocks control",
26
26
  color: "#301934",
27
27
  defaults: {
28
28
  name: { value: "" },
@@ -14,7 +14,7 @@
14
14
  <!-- JavaScript Section: Registers the node and handles editor logic -->
15
15
  <script type="text/javascript">
16
16
  RED.nodes.registerType("min-block", {
17
- category: "control",
17
+ category: "bldgblocks control",
18
18
  color: "#301934",
19
19
  defaults: {
20
20
  name: { value: "" },
@@ -19,7 +19,7 @@
19
19
  <!-- JavaScript Section: Registers the node and handles editor logic -->
20
20
  <script type="text/javascript">
21
21
  RED.nodes.registerType("minmax-block", {
22
- category: "control",
22
+ category: "bldgblocks control",
23
23
  color: "#301934",
24
24
  defaults: {
25
25
  name: { value: "" },
@@ -13,7 +13,7 @@
13
13
  <!-- JavaScript Section -->
14
14
  <script type="text/javascript">
15
15
  RED.nodes.registerType("modulo-block", {
16
- category: "control",
16
+ category: "bldgblocks control",
17
17
  color: "#301934",
18
18
  defaults: {
19
19
  name: { value: "" },
@@ -11,7 +11,7 @@
11
11
 
12
12
  <script type="text/javascript">
13
13
  RED.nodes.registerType("multiply-block", {
14
- category: "control",
14
+ category: "bldgblocks control",
15
15
  color: "#301934",
16
16
  defaults: {
17
17
  name: { value: "" },
@@ -7,7 +7,7 @@
7
7
 
8
8
  <script type="text/javascript">
9
9
  RED.nodes.registerType("negate-block", {
10
- category: "control",
10
+ category: "bldgblocks control",
11
11
  color: "#301934",
12
12
  defaults: {
13
13
  name: { value: "" }
@@ -0,0 +1,86 @@
1
+ <script type="text/html" data-template-name="network-point-registry">
2
+ <div class="form-row">
3
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
4
+ <input type="text" id="node-config-input-name" placeholder="Main Registry">
5
+ </div>
6
+ <div class="form-tips">
7
+ <p><b>Point Registry</b></p>
8
+ <p>This node maintains the mapping between Network Point IDs (integers) and Global Variables.</p>
9
+ </div>
10
+
11
+ <div class="form-row">
12
+ <label><i class="fa fa-list-ul"></i> Points</label>
13
+ <div id="node-input-point-list-div"
14
+ style="
15
+ border:1px solid #ccc;
16
+ height:200px; /* set a fixed height */
17
+ overflow-y:auto; /* enable vertical scrolling */
18
+ padding:5px;
19
+ box-sizing:border-box;">
20
+ <!-- The list will be injected by the JavaScript below -->
21
+ <ul id="node-input-point-list" style="margin:0;padding-left:1.2em;"></ul>
22
+ </div>
23
+ </div>
24
+ </script>
25
+
26
+ <script type="text/javascript">
27
+ RED.nodes.registerType('network-point-registry', {
28
+ category: 'config',
29
+ defaults: {
30
+ name: { value: "" }
31
+ },
32
+ label: function() {
33
+ return this.name || "Point Registry";
34
+ },
35
+ oneditprepare: function() {
36
+ const node = this;
37
+ const $list = $("#node-input-point-list");
38
+
39
+ function loadPoints() {
40
+ $.getJSON(`/network-point-registry/list/${node.id}`, function(data) {
41
+ $list.empty();
42
+ if (!data.length) return $list.append('<li>No points defined</li>');
43
+
44
+
45
+ data.sort((a, b) => parseInt(a.id, 10) - parseInt(b.id, 10));
46
+
47
+ data.forEach(pt => {
48
+ const li = $('<li>').css({ display:'flex', alignItems:'center' });
49
+
50
+ // Text part: ID + path
51
+ const txt = $('<span>')
52
+ .text(`ID ${pt.id}: ${pt.path ?? 'not set'}`)
53
+ .css({ flexGrow:1 }); // push button to the right
54
+
55
+ // Button part
56
+ const btn = $('<button type="button" class="editor-button">')
57
+ .attr('title','Find node')
58
+ .html('<i class="fa fa-search"></i>')
59
+ .on('click', function (e) {
60
+ e.stopPropagation();
61
+
62
+ RED.view.reveal(pt.nodeId);
63
+ });
64
+
65
+ li.append(txt, btn);
66
+ $list.append(li);
67
+ });
68
+ }).fail(function() {
69
+ $list.html('<li style="color:red;">Could not load points</li>');
70
+ });
71
+ }
72
+
73
+ loadPoints();
74
+ }
75
+ });
76
+ </script>
77
+
78
+ <script type="text/markdown" data-help-name="network-point-registry">
79
+ Central database mapping Point IDs to Global Variables.
80
+
81
+ ### Details
82
+
83
+ **API for Developers:**
84
+ * `register(id, meta)`: Claim an ID.
85
+ * `lookup(id)`: Find the path/store for an ID.
86
+ </script>