@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,222 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Point Write - Write values to remote points via network bridge
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Sends write requests to a remote point through the selected network bridge.
|
|
5
|
+
// Supports priority levels (1-16) for BACnet-style priority arrays.
|
|
6
|
+
// Configurable input property to read value from msg.
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
module.exports = function(RED) {
|
|
10
|
+
const utils = require('./utils')(RED);
|
|
11
|
+
|
|
12
|
+
function NetworkPointWriteNode(config) {
|
|
13
|
+
RED.nodes.createNode(this, config);
|
|
14
|
+
const node = this;
|
|
15
|
+
|
|
16
|
+
// ====================================================================
|
|
17
|
+
// Configuration
|
|
18
|
+
// ====================================================================
|
|
19
|
+
node.pointId = parseInt(config.pointId) || 0;
|
|
20
|
+
node.priority = parseInt(config.priority) || 16; // Default to lowest priority
|
|
21
|
+
node.inputProperty = config.inputProperty || "payload";
|
|
22
|
+
node.bridgeNodeId = config.bridgeNodeId || "";
|
|
23
|
+
|
|
24
|
+
// ====================================================================
|
|
25
|
+
// State tracking
|
|
26
|
+
// ====================================================================
|
|
27
|
+
let pendingWrite = null;
|
|
28
|
+
let lastWriteTime = null;
|
|
29
|
+
let lastWriteValue = null;
|
|
30
|
+
|
|
31
|
+
// ====================================================================
|
|
32
|
+
// Helper: Get status text
|
|
33
|
+
// ====================================================================
|
|
34
|
+
function getStatusText() {
|
|
35
|
+
if (lastWriteValue !== null) {
|
|
36
|
+
return `#${node.pointId} @${node.priority}: ${lastWriteValue}`;
|
|
37
|
+
}
|
|
38
|
+
return `#${node.pointId} @${node.priority}: (no writes)`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ====================================================================
|
|
42
|
+
// Response handler - receives write confirmations from bridge
|
|
43
|
+
// ====================================================================
|
|
44
|
+
function responseHandler(response) {
|
|
45
|
+
// Only process responses for this node
|
|
46
|
+
if (response.sourceNodeId !== node.id) return;
|
|
47
|
+
if (response.pointId !== node.pointId) return;
|
|
48
|
+
|
|
49
|
+
pendingWrite = null;
|
|
50
|
+
|
|
51
|
+
if (response.error) {
|
|
52
|
+
const errorText = `Write failed for point #${node.pointId}: ${response.error}`;
|
|
53
|
+
utils.setStatusError(node, `#${node.pointId}: ${response.error}`);
|
|
54
|
+
node.error(errorText); // Show in debug panel
|
|
55
|
+
node.send({
|
|
56
|
+
payload: lastWriteValue,
|
|
57
|
+
pointId: node.pointId,
|
|
58
|
+
priority: node.priority,
|
|
59
|
+
action: "writeError",
|
|
60
|
+
error: response.error,
|
|
61
|
+
timestamp: Date.now()
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
lastWriteTime = Date.now();
|
|
65
|
+
utils.setStatusChanged(node, getStatusText());
|
|
66
|
+
node.send({
|
|
67
|
+
payload: lastWriteValue,
|
|
68
|
+
pointId: node.pointId,
|
|
69
|
+
priority: node.priority,
|
|
70
|
+
action: "writeConfirmed",
|
|
71
|
+
timestamp: lastWriteTime
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Register response listener
|
|
77
|
+
RED.events.on("pointWrite:response", responseHandler);
|
|
78
|
+
|
|
79
|
+
// Initial status
|
|
80
|
+
utils.setStatusOK(node, getStatusText());
|
|
81
|
+
|
|
82
|
+
// ====================================================================
|
|
83
|
+
// Input handler
|
|
84
|
+
// ====================================================================
|
|
85
|
+
node.on("input", function(msg, send, done) {
|
|
86
|
+
send = send || function() { node.send.apply(node, arguments); };
|
|
87
|
+
|
|
88
|
+
// Guard against invalid msg
|
|
89
|
+
if (!msg) {
|
|
90
|
+
utils.setStatusError(node, "invalid message");
|
|
91
|
+
if (done) done();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ================================================================
|
|
96
|
+
// Handle configuration commands via msg.context
|
|
97
|
+
// ================================================================
|
|
98
|
+
if (msg.hasOwnProperty("context")) {
|
|
99
|
+
if (!msg.hasOwnProperty("payload")) {
|
|
100
|
+
utils.setStatusError(node, `missing payload for ${msg.context}`);
|
|
101
|
+
if (done) done();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (msg.context === "pointId") {
|
|
106
|
+
const newPointId = parseInt(msg.payload);
|
|
107
|
+
if (isNaN(newPointId) || newPointId < 0) {
|
|
108
|
+
utils.setStatusError(node, "invalid pointId");
|
|
109
|
+
if (done) done();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
node.pointId = newPointId;
|
|
113
|
+
utils.setStatusOK(node, getStatusText());
|
|
114
|
+
if (done) done();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (msg.context === "priority") {
|
|
119
|
+
const newPriority = parseInt(msg.payload);
|
|
120
|
+
if (isNaN(newPriority) || newPriority < 1 || newPriority > 16) {
|
|
121
|
+
utils.setStatusError(node, "priority must be 1-16");
|
|
122
|
+
if (done) done();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
node.priority = newPriority;
|
|
126
|
+
utils.setStatusOK(node, getStatusText());
|
|
127
|
+
if (done) done();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (msg.context === "release") {
|
|
132
|
+
// Release priority (write null to clear priority slot)
|
|
133
|
+
if (!node.bridgeNodeId) {
|
|
134
|
+
utils.setStatusError(node, "no bridge configured");
|
|
135
|
+
if (done) done();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const requestId = `${node.id}-${Date.now()}`;
|
|
140
|
+
pendingWrite = requestId;
|
|
141
|
+
lastWriteValue = null;
|
|
142
|
+
|
|
143
|
+
utils.setStatusWarn(node, `#${node.pointId} @${node.priority}: releasing...`);
|
|
144
|
+
|
|
145
|
+
RED.events.emit("pointWrite:write", {
|
|
146
|
+
sourceNodeId: node.id,
|
|
147
|
+
bridgeNodeId: node.bridgeNodeId,
|
|
148
|
+
pointId: node.pointId,
|
|
149
|
+
priority: node.priority,
|
|
150
|
+
value: null, // null = release
|
|
151
|
+
requestId: requestId
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (done) done();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
utils.setStatusWarn(node, "unknown context");
|
|
159
|
+
if (done) done();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ================================================================
|
|
164
|
+
// Normal write - read value from configured input property
|
|
165
|
+
// ================================================================
|
|
166
|
+
if (!node.bridgeNodeId) {
|
|
167
|
+
utils.setStatusError(node, "no bridge configured");
|
|
168
|
+
if (done) done();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Read value from input property
|
|
173
|
+
let writeValue;
|
|
174
|
+
try {
|
|
175
|
+
writeValue = RED.util.getMessageProperty(msg, node.inputProperty);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
utils.setStatusError(node, `invalid input property: ${node.inputProperty}`);
|
|
178
|
+
if (done) done();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (writeValue === undefined) {
|
|
183
|
+
utils.setStatusError(node, `missing ${node.inputProperty}`);
|
|
184
|
+
if (done) done();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Track the value we're writing
|
|
189
|
+
lastWriteValue = writeValue;
|
|
190
|
+
|
|
191
|
+
// Generate unique request ID
|
|
192
|
+
const requestId = `${node.id}-${Date.now()}`;
|
|
193
|
+
pendingWrite = requestId;
|
|
194
|
+
|
|
195
|
+
// Show pending status
|
|
196
|
+
utils.setStatusWarn(node, `#${node.pointId} @${node.priority}: writing ${writeValue}...`);
|
|
197
|
+
|
|
198
|
+
// Emit write request to bridge
|
|
199
|
+
RED.events.emit("pointWrite:write", {
|
|
200
|
+
sourceNodeId: node.id,
|
|
201
|
+
bridgeNodeId: node.bridgeNodeId,
|
|
202
|
+
pointId: node.pointId,
|
|
203
|
+
priority: node.priority,
|
|
204
|
+
value: writeValue,
|
|
205
|
+
requestId: requestId
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (done) done();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ====================================================================
|
|
212
|
+
// Cleanup on close
|
|
213
|
+
// ====================================================================
|
|
214
|
+
node.on("close", function(done) {
|
|
215
|
+
RED.events.removeListener("pointWrite:response", responseHandler);
|
|
216
|
+
pendingWrite = null;
|
|
217
|
+
done();
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
RED.nodes.registerType("network-point-write", NetworkPointWriteNode);
|
|
222
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<script type="text/html" data-template-name="network-service-bridge">
|
|
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="Network Bridge">
|
|
5
|
+
</div>
|
|
6
|
+
<hr style="margin: 10px 0;">
|
|
7
|
+
<div class="form-row">
|
|
8
|
+
<label for="node-input-startupDelay" title="Delay before processing requests (seconds, allows network to come online)"><i class="fa fa-clock-o"></i> Startup Delay (sec)</label>
|
|
9
|
+
<input type="number" id="node-input-startupDelay" placeholder="30" min="0" step="1">
|
|
10
|
+
</div>
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<script type="text/javascript">
|
|
14
|
+
RED.nodes.registerType("network-service-bridge", {
|
|
15
|
+
category: "bldgblocks network",
|
|
16
|
+
color: "#3090C7",
|
|
17
|
+
defaults: {
|
|
18
|
+
name: { value: "Network Bridge" },
|
|
19
|
+
startupDelay: { value: 30, required: false, validate: function(v) { return !isNaN(parseInt(v)) && parseInt(v) >= 0; } }
|
|
20
|
+
},
|
|
21
|
+
inputs: 1,
|
|
22
|
+
outputs: 1,
|
|
23
|
+
inputLabels: ["read requests / responses"],
|
|
24
|
+
outputLabels: ["routed to/from remote"],
|
|
25
|
+
icon: "font-awesome/fa-exchange",
|
|
26
|
+
paletteLabel: "network point bridge",
|
|
27
|
+
label: function() {
|
|
28
|
+
return this.name || "network point bridge";
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<script type="text/markdown" data-help-name="network-service-bridge">
|
|
34
|
+
Routes point read/write requests through WebSocket to remote devices and correlates responses.
|
|
35
|
+
|
|
36
|
+
Sits in the network flow between point-reference nodes and WebSocket communication, handling request/response routing and request correlation.
|
|
37
|
+
|
|
38
|
+
### Inputs
|
|
39
|
+
: action (string) : Command - read, write
|
|
40
|
+
: pointId (number) : Target point ID for read/write operations.
|
|
41
|
+
: requestId (string) : Unique request identifier for correlation.
|
|
42
|
+
: priority (number) : Priority level for write operations.
|
|
43
|
+
: value (any) : Value to write (read operations ignore this).
|
|
44
|
+
: timestamp (number) : Request timestamp (milliseconds since epoch).
|
|
45
|
+
|
|
46
|
+
### Outputs
|
|
47
|
+
: payload (object) : Response data (value for reads, status for writes).
|
|
48
|
+
: pointId (number) : Echo of request point ID.
|
|
49
|
+
: requestId (string) : Matching request ID for correlation.
|
|
50
|
+
: action (string) : readResponse, writeResponse, or stats.
|
|
51
|
+
: value (any) : Retrieved value (for reads) or winner value (for writes).
|
|
52
|
+
: timestamp (number) : Response timestamp.
|
|
53
|
+
|
|
54
|
+
### Details
|
|
55
|
+
Request/Response Correlation:
|
|
56
|
+
1. Assigns unique requestId to each request
|
|
57
|
+
2. Tracks pending requests in memory
|
|
58
|
+
3. Matches incoming responses to original requests
|
|
59
|
+
4. Timeout: 10 seconds per request
|
|
60
|
+
|
|
61
|
+
Management Commands:
|
|
62
|
+
- getBridgeStats: Returns sent/received/pending counts
|
|
63
|
+
- clearPending: Clear all pending request timeouts
|
|
64
|
+
- resetStats: Reset statistics counters
|
|
65
|
+
|
|
66
|
+
Flow Architecture:
|
|
67
|
+
- Left input: Requests from point-read/point-write nodes
|
|
68
|
+
- Right input: Responses from remote via WebSocket
|
|
69
|
+
- Outputs routed appropriately (back to requester or to WebSocket)
|
|
70
|
+
|
|
71
|
+
### Status
|
|
72
|
+
- Green (dot): Idle, no pending requests
|
|
73
|
+
- Blue (dot): Requests pending
|
|
74
|
+
- Blue (ring): Active, zero pending
|
|
75
|
+
- Yellow (ring): Some requests failed
|
|
76
|
+
- Red (ring): Configuration error
|
|
77
|
+
|
|
78
|
+
### References
|
|
79
|
+
5. Cleans up stale requests automatically
|
|
80
|
+
|
|
81
|
+
Request Flow:
|
|
82
|
+
```
|
|
83
|
+
[point-reference] sends read request
|
|
84
|
+
↓
|
|
85
|
+
[point-network-bridge] assigns requestId, forwards to WebSocket
|
|
86
|
+
↓
|
|
87
|
+
[WebSocket client] sends to remote
|
|
88
|
+
↓
|
|
89
|
+
[Remote receives, gets value]
|
|
90
|
+
↓
|
|
91
|
+
[WebSocket client] receives response
|
|
92
|
+
↓
|
|
93
|
+
[point-network-bridge] matches by requestId, forwards back
|
|
94
|
+
↓
|
|
95
|
+
[point-reference] updates cache
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### Architecture
|
|
99
|
+
|
|
100
|
+
This node is the **bridge** between:
|
|
101
|
+
- **LOCAL**: point-reference nodes (send read requests)
|
|
102
|
+
- **REMOTE**: WebSocket endpoint (receives/sends over network)
|
|
103
|
+
|
|
104
|
+
Enables non-blocking polling pattern:
|
|
105
|
+
- Logic flows request cached values from point-reference
|
|
106
|
+
- point-reference triggers reads via this bridge
|
|
107
|
+
- Responses update cache in background
|
|
108
|
+
- Logic flows get instant responses without network latency
|
|
109
|
+
|
|
110
|
+
#### Statistics
|
|
111
|
+
|
|
112
|
+
- **sent**: Total read requests sent to remote
|
|
113
|
+
- **received**: Total responses received from remote
|
|
114
|
+
- **errors**: Requests that timed out
|
|
115
|
+
- **pending**: Currently outstanding requests
|
|
116
|
+
|
|
117
|
+
#### Example Flow
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
Network Flow:
|
|
121
|
+
[WebSocket client] ←→ [point-network-bridge]
|
|
122
|
+
↑
|
|
123
|
+
[point-reference #301]
|
|
124
|
+
[point-reference #302]
|
|
125
|
+
(send read requests)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### References
|
|
129
|
+
- [Polling Architecture](../planning/POLLING_ARCHITECTURE.md)
|
|
130
|
+
- [point-reference Node](./point-reference.html)
|
|
131
|
+
</script>
|