@bldgblocks/node-red-contrib-control 0.1.34 → 0.1.37

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 (110) hide show
  1. package/nodes/accumulate-block.html +18 -8
  2. package/nodes/accumulate-block.js +39 -44
  3. package/nodes/add-block.html +1 -1
  4. package/nodes/add-block.js +18 -11
  5. package/nodes/alarm-collector.html +260 -0
  6. package/nodes/alarm-collector.js +292 -0
  7. package/nodes/alarm-config.html +129 -0
  8. package/nodes/alarm-config.js +126 -0
  9. package/nodes/alarm-service.html +96 -0
  10. package/nodes/alarm-service.js +142 -0
  11. package/nodes/analog-switch-block.js +25 -36
  12. package/nodes/and-block.js +44 -15
  13. package/nodes/average-block.js +46 -41
  14. package/nodes/boolean-switch-block.js +10 -28
  15. package/nodes/boolean-to-number-block.html +18 -5
  16. package/nodes/boolean-to-number-block.js +24 -16
  17. package/nodes/cache-block.js +24 -37
  18. package/nodes/call-status-block.html +91 -32
  19. package/nodes/call-status-block.js +398 -115
  20. package/nodes/changeover-block.html +5 -0
  21. package/nodes/changeover-block.js +167 -162
  22. package/nodes/comment-block.html +1 -1
  23. package/nodes/comment-block.js +14 -9
  24. package/nodes/compare-block.html +14 -4
  25. package/nodes/compare-block.js +23 -18
  26. package/nodes/contextual-label-block.html +5 -0
  27. package/nodes/contextual-label-block.js +6 -16
  28. package/nodes/convert-block.html +25 -39
  29. package/nodes/convert-block.js +31 -16
  30. package/nodes/count-block.html +11 -5
  31. package/nodes/count-block.js +34 -32
  32. package/nodes/delay-block.js +58 -53
  33. package/nodes/divide-block.js +43 -45
  34. package/nodes/edge-block.html +17 -10
  35. package/nodes/edge-block.js +43 -41
  36. package/nodes/enum-switch-block.js +6 -6
  37. package/nodes/frequency-block.html +6 -1
  38. package/nodes/frequency-block.js +64 -74
  39. package/nodes/global-getter.html +51 -15
  40. package/nodes/global-getter.js +43 -13
  41. package/nodes/global-setter.html +1 -1
  42. package/nodes/global-setter.js +40 -12
  43. package/nodes/history-buffer.html +96 -0
  44. package/nodes/history-buffer.js +464 -0
  45. package/nodes/history-collector.html +29 -1
  46. package/nodes/history-collector.js +46 -16
  47. package/nodes/history-config.html +13 -1
  48. package/nodes/history-service.html +84 -0
  49. package/nodes/history-service.js +66 -0
  50. package/nodes/hysteresis-block.html +5 -0
  51. package/nodes/hysteresis-block.js +13 -16
  52. package/nodes/interpolate-block.html +20 -2
  53. package/nodes/interpolate-block.js +39 -50
  54. package/nodes/join.html +78 -0
  55. package/nodes/join.js +78 -0
  56. package/nodes/latch-block.js +12 -14
  57. package/nodes/load-sequence-block.js +102 -110
  58. package/nodes/max-block.js +26 -26
  59. package/nodes/memory-block.js +57 -58
  60. package/nodes/min-block.js +26 -25
  61. package/nodes/minmax-block.js +35 -34
  62. package/nodes/modulo-block.js +45 -43
  63. package/nodes/multiply-block.js +43 -41
  64. package/nodes/negate-block.html +17 -7
  65. package/nodes/negate-block.js +25 -19
  66. package/nodes/network-point-read.html +128 -0
  67. package/nodes/network-point-read.js +230 -0
  68. package/nodes/{network-register.html → network-point-register.html} +94 -7
  69. package/nodes/{network-register.js → network-point-register.js} +18 -4
  70. package/nodes/network-point-write.html +149 -0
  71. package/nodes/network-point-write.js +222 -0
  72. package/nodes/network-service-bridge.html +131 -0
  73. package/nodes/network-service-bridge.js +376 -0
  74. package/nodes/network-service-read.html +81 -0
  75. package/nodes/{network-read.js → network-service-read.js} +4 -3
  76. package/nodes/{network-point-registry.html → network-service-registry.html} +19 -4
  77. package/nodes/{network-point-registry.js → network-service-registry.js} +7 -2
  78. package/nodes/network-service-write.html +89 -0
  79. package/nodes/{network-write.js → network-service-write.js} +3 -3
  80. package/nodes/nullify-block.js +13 -15
  81. package/nodes/on-change-block.html +17 -9
  82. package/nodes/on-change-block.js +49 -46
  83. package/nodes/oneshot-block.html +13 -10
  84. package/nodes/oneshot-block.js +57 -75
  85. package/nodes/or-block.js +44 -15
  86. package/nodes/pid-block.html +54 -4
  87. package/nodes/pid-block.js +459 -248
  88. package/nodes/priority-block.js +24 -35
  89. package/nodes/rate-limit-block.js +70 -72
  90. package/nodes/rate-of-change-block.html +33 -14
  91. package/nodes/rate-of-change-block.js +74 -62
  92. package/nodes/round-block.html +14 -9
  93. package/nodes/round-block.js +32 -25
  94. package/nodes/saw-tooth-wave-block.js +49 -76
  95. package/nodes/scale-range-block.html +12 -6
  96. package/nodes/scale-range-block.js +46 -39
  97. package/nodes/sine-wave-block.js +49 -57
  98. package/nodes/string-builder-block.js +6 -6
  99. package/nodes/subtract-block.js +38 -34
  100. package/nodes/thermistor-block.js +44 -44
  101. package/nodes/tick-tock-block.js +32 -32
  102. package/nodes/time-sequence-block.js +30 -42
  103. package/nodes/triangle-wave-block.js +49 -69
  104. package/nodes/tstat-block.js +34 -44
  105. package/nodes/units-block.html +90 -69
  106. package/nodes/units-block.js +22 -30
  107. package/nodes/utils.js +206 -3
  108. package/package.json +14 -6
  109. package/nodes/network-read.html +0 -56
  110. package/nodes/network-write.html +0 -65
@@ -0,0 +1,230 @@
1
+ module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
3
+
4
+ function NetworkPointReadNode(config) {
5
+ RED.nodes.createNode(this, config);
6
+ const node = this;
7
+
8
+ // ====================================================================
9
+ // Initialize configuration
10
+ // ====================================================================
11
+ node.pointId = parseInt(config.pointId);
12
+ node.bridgeNodeId = config.bridgeNodeId;
13
+ node.outputProperty = config.outputProperty || "payload";
14
+
15
+ // Validate pointId
16
+ if (isNaN(node.pointId) || node.pointId < 0) {
17
+ utils.setStatusError(node, "invalid pointId");
18
+ return;
19
+ }
20
+
21
+ // ====================================================================
22
+ // Initialize state
23
+ // ====================================================================
24
+ node.cache = {
25
+ value: null,
26
+ name: null,
27
+ timestamp: null // When value was last updated
28
+ };
29
+
30
+ node.isPollPending = false;
31
+
32
+ // ====================================================================
33
+ // Helper: Format status text
34
+ // ====================================================================
35
+ const getStatusText = function() {
36
+ // Try to look up metadata (name/path) for this point
37
+ const meta = utils.lookupPointMetadata(node.pointId);
38
+ let label = meta && meta.name ? meta.name : (meta && meta.path ? meta.path : null);
39
+
40
+ // Use cached name from remote response if local lookup failed
41
+ if (!label && node.cache.name) {
42
+ label = node.cache.name;
43
+ }
44
+
45
+ if (!label) label = `Point #${node.pointId}`;
46
+
47
+ if (node.cache.value === null) {
48
+ return `${label}: waiting...`;
49
+ }
50
+ // Show value (truncate if too long)
51
+ let valDisplay = node.cache.value;
52
+ if (typeof valDisplay === 'object') {
53
+ valDisplay = JSON.stringify(valDisplay).substring(0, 20);
54
+ }
55
+ return `${label}: ${valDisplay}`;
56
+ };
57
+
58
+ // ====================================================================
59
+ // Send read request to bridge via event
60
+ // ====================================================================
61
+ const triggerRead = function() {
62
+ if (node.isPollPending) {
63
+ return; // Already waiting for response
64
+ }
65
+
66
+ node.isPollPending = true;
67
+ utils.setStatusUnchanged(node, `Fetching... ${getStatusText()}`);
68
+
69
+ // Send read request to bridge node via event (cross-flow communication)
70
+ const requestId = `${node.id}_${node.pointId}_${Date.now()}`;
71
+ RED.events.emit('pointReference:read', {
72
+ sourceNodeId: node.id,
73
+ bridgeNodeId: node.bridgeNodeId,
74
+ pointId: node.pointId,
75
+ requestId: requestId
76
+ });
77
+ };
78
+
79
+ // ====================================================================
80
+ // Main message handler - triggered by external inject/timer
81
+ // ====================================================================
82
+ node.on("input", function(msg, send, done) {
83
+ send = send || function() { node.send.apply(node, arguments); };
84
+
85
+ // Guard against invalid msg
86
+ if (!msg) {
87
+ utils.setStatusError(node, "invalid message");
88
+ if (done) done();
89
+ return;
90
+ }
91
+
92
+ // ================================================================
93
+ // Handle trigger: any input message triggers a read request
94
+ // This is called by external inject node (user controls polling)
95
+ // ================================================================
96
+ if (msg.action === undefined || msg.action === "poll" || msg.action === "read") {
97
+ triggerRead();
98
+ if (done) done();
99
+ return;
100
+ }
101
+
102
+ // ================================================================
103
+ // Handle getPoint requests (from logic flows wanting cached value)
104
+ // ================================================================
105
+ if (msg.action === "getPoint") {
106
+ // Return cached value immediately (non-blocking)
107
+ const cacheAge = node.cache.timestamp ? Date.now() - node.cache.timestamp : null;
108
+ const responseMsg = RED.util.cloneMessage(msg);
109
+ responseMsg.action = "getPointResponse";
110
+ responseMsg.pointId = node.pointId;
111
+ responseMsg.value = node.cache.value;
112
+ responseMsg.cached = true;
113
+ responseMsg.age = cacheAge;
114
+
115
+ send(responseMsg);
116
+ utils.setStatusUnchanged(node, `Served: ${getStatusText()}`);
117
+ if (done) done();
118
+ return;
119
+ }
120
+
121
+ // ================================================================
122
+ // Handle configuration commands
123
+ // ================================================================
124
+ if (msg.action === "resetCache") {
125
+ node.cache = { value: null, timestamp: null };
126
+ node.isPollPending = false;
127
+ utils.setStatusOK(node, "Cache reset");
128
+ if (done) done();
129
+ return;
130
+ }
131
+
132
+ // Unknown action - pass through (stack properties pattern)
133
+ send(msg);
134
+ if (done) done();
135
+ });
136
+
137
+ // ====================================================================
138
+ // Listen for responses from bridge via event
139
+ // ====================================================================
140
+ const responseHandler = function(data) {
141
+ // Only process responses meant for this node
142
+ if (data.sourceNodeId !== node.id) {
143
+ return;
144
+ }
145
+
146
+ node.isPollPending = false;
147
+
148
+ // Check for error response
149
+ if (data.error) {
150
+ const errorText = `Read failed for point #${node.pointId}: ${data.errorMessage || "Unknown error"}`;
151
+ utils.setStatusError(node, `Error: ${data.errorMessage || "Unknown error"}`);
152
+ node.error(errorText); // Show in debug panel
153
+ // Don't update cache on error, keep stale value
154
+ return;
155
+ }
156
+
157
+ let newValue = data.value;
158
+
159
+ // Extract metadata if available
160
+ if (data.message) {
161
+ if (data.message.metadata) {
162
+ node.cache.name = data.message.metadata.path || data.message.metadata.name;
163
+ }
164
+
165
+ // If message available, try to robustly extract scalar value
166
+ if (data.message.value !== undefined) {
167
+ newValue = data.message.value;
168
+ } else if (data.message.payload !== undefined && typeof data.message.payload !== 'object') {
169
+ newValue = data.message.payload;
170
+ }
171
+ }
172
+
173
+ const valueChanged = node.cache.value !== newValue;
174
+
175
+ // Update cache with new value
176
+ node.cache.value = newValue;
177
+ node.cache.timestamp = Date.now();
178
+
179
+ // Update status
180
+ if (valueChanged) {
181
+ utils.setStatusChanged(node, getStatusText());
182
+ } else {
183
+ utils.setStatusUnchanged(node, getStatusText());
184
+ }
185
+
186
+ // Emit downstream (for wired alarm/history/logic)
187
+ // Uses node.send directly since this is event-triggered, not input-triggered
188
+
189
+ const outMsg = {
190
+ pointId: node.pointId,
191
+ timestamp: node.cache.timestamp,
192
+ action: "pointUpdate"
193
+ };
194
+
195
+ // Set output property (default: payload)
196
+ RED.util.setMessageProperty(outMsg, node.outputProperty, newValue);
197
+
198
+ node.send(outMsg);
199
+ };
200
+
201
+ RED.events.on('pointReference:response', responseHandler);
202
+
203
+ // ====================================================================
204
+ // Node lifecycle
205
+ // ====================================================================
206
+ node.on("close", function(done) {
207
+ // Remove event listener
208
+ RED.events.off('pointReference:response', responseHandler);
209
+ done();
210
+ });
211
+
212
+ // Set initial status
213
+ utils.setStatusOK(node, getStatusText());
214
+
215
+ // Update status again after a short delay to allow registry nodes to initialize
216
+ // This solves the race condition where this node loads before point-register nodes
217
+ setTimeout(() => {
218
+ const text = getStatusText();
219
+ // Only update if we are still in "waiting" or "initial" state (no value yet)
220
+ if (node.cache.value === null) {
221
+ utils.setStatusOK(node, text);
222
+ } else {
223
+ // If we have a value, just update the label part
224
+ utils.setStatusChanged(node, text);
225
+ }
226
+ }, 2000);
227
+ }
228
+
229
+ RED.nodes.registerType("network-point-read", NetworkPointReadNode);
230
+ };
@@ -1,4 +1,4 @@
1
- <script type="text/html" data-template-name="network-register">
1
+ <script type="text/html" data-template-name="network-point-register">
2
2
  <div class="form-row">
3
3
  <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
4
4
  <input type="text" id="node-input-name" placeholder="Name">
@@ -24,12 +24,12 @@
24
24
  </script>
25
25
 
26
26
  <script type="text/javascript">
27
- RED.nodes.registerType('network-register', {
27
+ RED.nodes.registerType("network-point-register", {
28
28
  category: 'bldgblocks network',
29
29
  color: '#3090C7',
30
30
  defaults: {
31
31
  name: { value: "" },
32
- registry: { value: "", type: "network-point-registry", required: true },
32
+ registry: { value: "", type: "network-service-registry", required: true },
33
33
  pointId: { value: null, required: true, validate: RED.validators.number() },
34
34
  writable: { value: false }
35
35
  },
@@ -38,9 +38,37 @@
38
38
  icon: "font-awesome/fa-book",
39
39
  label: function() {
40
40
  const id = this.pointId || "?";
41
- return `(id:${id}) ${this.name || "network register"}`;
41
+ return `(id:${id}) ${this.name || "network point register"}`;
42
+ },
43
+ paletteLabel: "network point register",
44
+ onadd: function() {
45
+ // Fires when node is added to workspace (including paste/import)
46
+ const node = this;
47
+ const registry = node.registry;
48
+
49
+ if (!registry || registry === "_ADD_") return;
50
+
51
+ // Find max pointId and check for collision
52
+ let maxId = 0;
53
+ let collision = false;
54
+ const myId = parseInt(node.pointId);
55
+
56
+ RED.nodes.eachNode(function(n) {
57
+ if (n.type === "network-point-register" && n.registry === registry && n.id !== node.id) {
58
+ const pid = parseInt(n.pointId);
59
+ if (!isNaN(pid)) {
60
+ if (pid > maxId) maxId = pid;
61
+ if (pid === myId) collision = true;
62
+ }
63
+ }
64
+ });
65
+
66
+ // If collision or no pointId, assign next available
67
+ if (collision || isNaN(myId) || myId === null) {
68
+ node.pointId = maxId + 1;
69
+ node.changed = true;
70
+ }
42
71
  },
43
- paletteLabel: "network register",
44
72
  oneditprepare: function() {
45
73
  const nodeId = this.id;
46
74
  const statusSpan = $("#point-id-status");
@@ -85,11 +113,30 @@
85
113
 
86
114
  function loadNextFreeId(registry) {
87
115
  if (!registry || registry === "_ADD_") { return; }
116
+
117
+ // First, find highest pointId from ALL nodes in editor (deployed + not deployed)
118
+ let maxEditorId = 0;
119
+ RED.nodes.eachNode(function(n) {
120
+ if (n.type === "network-point-register" && n.registry === registry && n.id !== nodeId) {
121
+ const pid = parseInt(n.pointId);
122
+ if (!isNaN(pid) && pid > maxEditorId) {
123
+ maxEditorId = pid;
124
+ }
125
+ }
126
+ });
127
+
128
+ // Also check deployed registry for any points not visible in editor
88
129
  $.getJSON(`/network-point-registry/list/${registry}`, function (data) {
89
- const next = data.reduce((max, pt) => Math.max(max, pt.id), 0) + 1;
130
+ const maxDeployedId = data.reduce((max, pt) => Math.max(max, pt.id), 0);
131
+ const next = Math.max(maxEditorId, maxDeployedId) + 1;
90
132
  idInput.val(next);
91
133
  }).fail(function () {
92
- statusSpan.html('<i class="fa fa-exclamation-triangle" style="color:red;"></i> Cannot fetch points').show();
134
+ // Fallback to editor-only max if server unavailable
135
+ const next = maxEditorId + 1;
136
+ idInput.val(next);
137
+ if (maxEditorId === 0) {
138
+ statusSpan.html('<i class="fa fa-exclamation-triangle" style="color:orange;"></i> Registry not deployed').show();
139
+ }
93
140
  });
94
141
  }
95
142
 
@@ -108,3 +155,43 @@
108
155
  }
109
156
  });
110
157
  </script>
158
+
159
+ <!-- Help Section -->
160
+ <script type="text/markdown" data-help-name="network-point-register">
161
+ Register a data point in a network registry for shared access across devices, similar to BACnet usage.
162
+
163
+ ### Inputs
164
+ : payload (object) : Expected flow from global-setter containing full network object properties.
165
+
166
+ ### Outputs
167
+ : payload (any) : Current value of the registered network point.
168
+
169
+ ### Details
170
+ Network Register nodes make data points discoverable from anywhere by assigning them a stable numeric ID within a registry.
171
+
172
+ Each point requires a unique Point ID within its registry. The Point ID is used by other nodes (network-read, network-write) to reference this point without needing node links.
173
+
174
+ Registry must be configured via a network-point-registry config node. Each registry is a separate namespace for point IDs.
175
+
176
+ The Writable checkbox controls whether network-write nodes can update this point's value. Disabled by default.
177
+
178
+ Usage: Set up your networking implementation, maybe websockets communication, discover available points using `action: "discover"` from that device for a list you can build from and set up network read flows to retrieve the data.
179
+
180
+ Point ID Status:
181
+ - Green check: Available or assigned to this node
182
+ - Orange question: Registry not deployed
183
+ - Red exclamation: Point ID is taken by another node in the same registry
184
+
185
+ The editor will auto-suggest the next available Point ID when creating a new node.
186
+
187
+ ### Status
188
+ - Green (dot): Configuration update
189
+ - Blue (dot): State changed
190
+ - Blue (ring): State unchanged
191
+ - Red (ring): Error
192
+ - Yellow (ring): Warning
193
+
194
+ ### References
195
+ - [Node-RED Documentation](https://nodered.org/docs/)
196
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
197
+ </script>
@@ -1,6 +1,7 @@
1
1
  module.exports = function(RED) {
2
2
  const utils = require('./utils')(RED);
3
- function NetworkRegisterNode(config) {
3
+
4
+ function NetworkPointRegisterNode(config) {
4
5
  RED.nodes.createNode(this, config);
5
6
  const node = this;
6
7
 
@@ -20,10 +21,10 @@ module.exports = function(RED) {
20
21
 
21
22
  if (success) {
22
23
  node.isRegistered = true;
23
- node.status({ fill: "yellow", shape: "ring", text: `ID: ${node.pointId} (Waiting)` });
24
+ utils.setStatusBusy(node, `ID: ${node.pointId} (Waiting)`);
24
25
  } else {
25
26
  node.error(`Point ID ${node.pointId} is already in use.`);
26
- node.status({ fill: "red", shape: "dot", text: "ID Conflict" });
27
+ utils.setStatusError(node, "ID Conflict");
27
28
  }
28
29
  }
29
30
 
@@ -90,6 +91,18 @@ module.exports = function(RED) {
90
91
  return utils.sendSuccess(node, networkObject, done, statusText, node.pointId, "dot");
91
92
  }
92
93
 
94
+ // Emit point-update event for reactive listeners (e.g., alarm-collector)
95
+ const pointUpdateData = {
96
+ pointId: node.pointId,
97
+ nodeId: node.id,
98
+ value: msg.value,
99
+ activePriority: msg.activePriority,
100
+ units: msg.units || "",
101
+ metadata: msg.metadata,
102
+ timestamp: new Date().toISOString()
103
+ };
104
+ RED.events.emit("bldgblocks:network:point-update", pointUpdateData);
105
+
93
106
  // Passthrough
94
107
  const prefix = msg.activePriority === 'default' ? '' : 'P';
95
108
  const statusText = `Passthrough: ${prefix}${msg.activePriority}:${msg.value}${msg.units}`;
@@ -108,5 +121,6 @@ module.exports = function(RED) {
108
121
  done();
109
122
  });
110
123
  }
111
- RED.nodes.registerType("network-register", NetworkRegisterNode);
124
+ RED.nodes.registerType("network-point-register", NetworkPointRegisterNode);
125
+ RED.nodes.registerType("network-register", NetworkPointRegisterNode);
112
126
  }
@@ -0,0 +1,149 @@
1
+ <script type="text/html" data-template-name="network-point-write">
2
+ <div class="form-row">
3
+ <label for="node-input-name" title="Display name shown on canvas"><i class="fa fa-tag"></i> Name</label>
4
+ <input type="text" id="node-input-name" placeholder="Write Zone Temp SP">
5
+ </div>
6
+ <div class="form-row">
7
+ <label for="node-input-pointId" title="Point ID to write to (e.g., 301)"><i class="fa fa-hashtag"></i> Point ID</label>
8
+ <input type="number" id="node-input-pointId" placeholder="301" min="0" required>
9
+ </div>
10
+ <div class="form-row">
11
+ <label for="node-input-priority" title="Write priority (1=highest, 16=lowest)"><i class="fa fa-sort-numeric-asc"></i> Priority</label>
12
+ <input type="number" id="node-input-priority" placeholder="16" min="1" max="16" required>
13
+ </div>
14
+ <div class="form-row">
15
+ <label for="node-input-inputProperty" title="Property to read value from (e.g., payload)"><i class="fa fa-sign-in"></i> Input Property</label>
16
+ <input type="text" id="node-input-inputProperty" placeholder="payload">
17
+ </div>
18
+ <hr style="margin: 10px 0;">
19
+ <div class="form-row">
20
+ <label for="node-input-bridgeNodeId" title="Select which network bridge to use"><i class="fa fa-link"></i> Network Bridge</label>
21
+ <input type="text" id="node-input-bridgeNodeId" style="width: calc(70% - 45px);">
22
+ <button id="node-config-find-bridge" class="editor-button" style="margin-left: 5px; width: 40px;" title="Find Bridge Node">
23
+ <i class="fa fa-search"></i>
24
+ </button>
25
+ </div>
26
+ </script>
27
+
28
+ <script type="text/javascript">
29
+ RED.nodes.registerType("network-point-write", {
30
+ category: "bldgblocks network",
31
+ color: "#3090C7",
32
+ defaults: {
33
+ name: { value: "" },
34
+ pointId: {
35
+ value: 0,
36
+ required: true,
37
+ validate: function(v) {
38
+ return !isNaN(parseInt(v)) && parseInt(v) >= 0;
39
+ }
40
+ },
41
+ priority: {
42
+ value: 16,
43
+ required: true,
44
+ validate: function(v) {
45
+ var p = parseInt(v);
46
+ return !isNaN(p) && p >= 1 && p <= 16;
47
+ }
48
+ },
49
+ inputProperty: {
50
+ value: "payload",
51
+ required: true
52
+ },
53
+ bridgeNodeId: {
54
+ value: "",
55
+ required: true
56
+ }
57
+ },
58
+ inputs: 1,
59
+ outputs: 1,
60
+ inputLabels: ["value to write"],
61
+ outputLabels: ["write confirmation"],
62
+ icon: "font-awesome/fa-pencil",
63
+ paletteLabel: "network point write",
64
+ label: function() {
65
+ const id = this.pointId ? ` #${this.pointId}` : " (unconfigured)";
66
+ const pri = this.priority ? ` @${this.priority}` : "";
67
+ return this.name ? `${this.name}${id}${pri}` : `network point write${id}${pri}`;
68
+ },
69
+ oneditprepare: function() {
70
+ const node = this;
71
+
72
+ let candidateNodes = [];
73
+ RED.nodes.eachNode(function(n) {
74
+ if (n.type === 'network-service-bridge') {
75
+ candidateNodes.push({
76
+ value: n.id,
77
+ label: n.name || "(unnamed bridge)"
78
+ });
79
+ }
80
+ });
81
+
82
+ candidateNodes.sort((a, b) => a.label.localeCompare(b.label));
83
+
84
+ $("#node-input-bridgeNodeId").typedInput({
85
+ types: [{ value: "bridge", options: candidateNodes }]
86
+ });
87
+
88
+ // Find button
89
+ $("#node-config-find-bridge").on("click", function() {
90
+ if (node.bridgeNodeId) {
91
+ RED.viewport.reveal(node.bridgeNodeId);
92
+ }
93
+ });
94
+ },
95
+ oneditsave: function() {
96
+ // Optional: log configuration
97
+ }
98
+ });
99
+ </script>
100
+
101
+ <script type="text/markdown" data-help-name="network-point-write">
102
+ Sends control commands to remote network points via service bridge.
103
+
104
+ Validates payload before sending to prevent invalid network updates.
105
+
106
+ ### Inputs
107
+ : payload (number | string | boolean) : Value to write from configured input property.
108
+ : pointId (number) : Target remote point ID (e.g., 401).
109
+ : priority (number) : Priority level (1-16, higher wins).
110
+ : bridge (node) : Network service bridge for communication.
111
+
112
+ ### Outputs
113
+ : payload (number | string | boolean) : Echo of sent value.
114
+ : pointId (number) : Target point ID.
115
+ : priority (number) : Priority used for this write.
116
+ : timestamp (number) : Write timestamp (milliseconds since epoch).
117
+ : action (string) : writeConfirmed or writeError.
118
+
119
+ ### Properties
120
+ : name (string) : Display name in editor.
121
+ : pointId (number) : The remote point ID to write to.
122
+ : priority (number) : Write priority (1=highest, 16=lowest).
123
+ : inputProperty (string) : Property to read value from (default payload).
124
+ : bridgeNodeId (node) : The bridge node for network communication.
125
+
126
+ ### Details
127
+ BACnet-style priority array (1-16):
128
+ - 1: Manual Life Safety
129
+ - 2: Automatic Life Safety
130
+ - 5: Critical Equipment Control
131
+ - 8: Manual Operator
132
+ - 16: Default/Available (lowest)
133
+
134
+ Configuration Commands:
135
+ - pointId: Change target point
136
+ - priority: Change write priority
137
+ - release: Release priority (write null)
138
+
139
+ ### Status
140
+ - Green (dot): Configuration update
141
+ - Blue (dot): State changed
142
+ - Blue (ring): State unchanged
143
+ - Red (ring): Error
144
+ - Yellow (ring): Warning
145
+
146
+ ### References
147
+ - [Node-RED Documentation](https://nodered.org/docs/)
148
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
149
+ </script>