@bldgblocks/node-red-contrib-control 0.1.34 → 0.1.36
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 +18 -8
- package/nodes/accumulate-block.js +39 -44
- package/nodes/add-block.html +1 -1
- package/nodes/add-block.js +18 -11
- package/nodes/alarm-collector.html +260 -0
- package/nodes/alarm-collector.js +292 -0
- package/nodes/alarm-config.html +129 -0
- package/nodes/alarm-config.js +126 -0
- package/nodes/alarm-service.html +96 -0
- package/nodes/alarm-service.js +142 -0
- package/nodes/analog-switch-block.js +25 -36
- package/nodes/and-block.js +44 -15
- package/nodes/average-block.js +46 -41
- package/nodes/boolean-switch-block.js +10 -28
- package/nodes/boolean-to-number-block.html +18 -5
- package/nodes/boolean-to-number-block.js +24 -16
- package/nodes/cache-block.js +24 -37
- package/nodes/call-status-block.html +91 -32
- package/nodes/call-status-block.js +398 -115
- package/nodes/changeover-block.html +5 -0
- package/nodes/changeover-block.js +167 -162
- package/nodes/comment-block.html +1 -1
- package/nodes/comment-block.js +14 -9
- package/nodes/compare-block.html +14 -4
- package/nodes/compare-block.js +23 -18
- package/nodes/contextual-label-block.html +5 -0
- package/nodes/contextual-label-block.js +6 -16
- package/nodes/convert-block.html +25 -39
- package/nodes/convert-block.js +31 -16
- package/nodes/count-block.html +11 -5
- package/nodes/count-block.js +34 -32
- package/nodes/delay-block.js +58 -53
- package/nodes/divide-block.js +43 -45
- package/nodes/edge-block.html +17 -10
- package/nodes/edge-block.js +43 -41
- package/nodes/enum-switch-block.js +6 -6
- package/nodes/frequency-block.html +6 -1
- package/nodes/frequency-block.js +64 -74
- package/nodes/global-getter.html +51 -15
- package/nodes/global-getter.js +43 -13
- package/nodes/global-setter.html +1 -1
- package/nodes/global-setter.js +40 -12
- package/nodes/history-buffer.html +96 -0
- package/nodes/history-buffer.js +461 -0
- package/nodes/history-collector.html +29 -1
- package/nodes/history-collector.js +37 -16
- package/nodes/history-config.html +13 -1
- package/nodes/history-service.html +84 -0
- package/nodes/history-service.js +52 -0
- package/nodes/hysteresis-block.html +5 -0
- package/nodes/hysteresis-block.js +13 -16
- package/nodes/interpolate-block.html +20 -2
- package/nodes/interpolate-block.js +39 -50
- package/nodes/join.html +78 -0
- package/nodes/join.js +78 -0
- package/nodes/latch-block.js +12 -14
- package/nodes/load-sequence-block.js +102 -110
- package/nodes/max-block.js +26 -26
- package/nodes/memory-block.js +57 -58
- package/nodes/min-block.js +26 -25
- package/nodes/minmax-block.js +35 -34
- package/nodes/modulo-block.js +45 -43
- package/nodes/multiply-block.js +43 -41
- package/nodes/negate-block.html +17 -7
- package/nodes/negate-block.js +25 -19
- package/nodes/network-point-read.html +128 -0
- package/nodes/network-point-read.js +230 -0
- package/nodes/{network-register.html → network-point-register.html} +94 -7
- package/nodes/{network-register.js → network-point-register.js} +18 -4
- package/nodes/network-point-write.html +149 -0
- package/nodes/network-point-write.js +222 -0
- package/nodes/network-service-bridge.html +131 -0
- package/nodes/network-service-bridge.js +376 -0
- package/nodes/network-service-read.html +81 -0
- package/nodes/{network-read.js → network-service-read.js} +4 -3
- package/nodes/{network-point-registry.html → network-service-registry.html} +19 -4
- package/nodes/{network-point-registry.js → network-service-registry.js} +7 -2
- package/nodes/network-service-write.html +89 -0
- package/nodes/{network-write.js → network-service-write.js} +3 -3
- package/nodes/nullify-block.js +13 -15
- package/nodes/on-change-block.html +17 -9
- package/nodes/on-change-block.js +49 -46
- package/nodes/oneshot-block.html +13 -10
- package/nodes/oneshot-block.js +57 -75
- package/nodes/or-block.js +44 -15
- package/nodes/pid-block.html +54 -4
- package/nodes/pid-block.js +459 -248
- package/nodes/priority-block.js +24 -35
- package/nodes/rate-limit-block.js +70 -72
- package/nodes/rate-of-change-block.html +33 -14
- package/nodes/rate-of-change-block.js +74 -62
- package/nodes/round-block.html +14 -9
- package/nodes/round-block.js +32 -25
- package/nodes/saw-tooth-wave-block.js +49 -76
- package/nodes/scale-range-block.html +12 -6
- package/nodes/scale-range-block.js +46 -39
- package/nodes/sine-wave-block.js +49 -57
- package/nodes/string-builder-block.js +6 -6
- package/nodes/subtract-block.js +38 -34
- package/nodes/thermistor-block.js +44 -44
- package/nodes/tick-tock-block.js +32 -32
- package/nodes/time-sequence-block.js +30 -42
- package/nodes/triangle-wave-block.js +49 -69
- package/nodes/tstat-block.js +34 -44
- package/nodes/units-block.html +90 -69
- package/nodes/units-block.js +22 -30
- package/nodes/utils.js +206 -3
- package/package.json +14 -6
- package/nodes/network-read.html +0 -56
- 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(
|
|
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-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
+
utils.setStatusBusy(node, `ID: ${node.pointId} (Waiting)`);
|
|
24
25
|
} else {
|
|
25
26
|
node.error(`Point ID ${node.pointId} is already in use.`);
|
|
26
|
-
|
|
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",
|
|
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>
|