node-red-contrib-lorawan-bacnet-server 1.2.0
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.
Potentially problematic release.
This version of node-red-contrib-lorawan-bacnet-server might be problematic. Click here for more details.
- package/.gitattributes +2 -0
- package/CONTRIBUTING.md +64 -0
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/examples/BACnet-Server.json +401 -0
- package/examples/LoRaBAC.json +1152 -0
- package/images/BACnetObjectListExample.png +0 -0
- package/images/controllerSetpointExample3.png +0 -0
- package/images/deviceListExample.png +0 -0
- package/images/deviceListExample3.png +0 -0
- package/images/lorabac.png +0 -0
- package/images/objectConfigurationExample2.png +0 -0
- package/images/objectListExample2.png +0 -0
- package/images/objectListExample3.png +0 -0
- package/images/valveSetpointExample3.png +0 -0
- package/images/valveTemperatureExample3.png +0 -0
- package/nodes/bacnet-point/bacnet-point.html +403 -0
- package/nodes/bacnet-point/bacnet-point.js +293 -0
- package/nodes/bacnet-server/bacnet-server.html +138 -0
- package/nodes/bacnet-server/bacnet-server.js +817 -0
- package/nodes/lorabac/lorabac.html +1588 -0
- package/nodes/lorabac/lorabac.js +652 -0
- package/package.json +39 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BACnet Point Node
|
|
3
|
+
*
|
|
4
|
+
* This node represents a BACnet object (Analog Value, Binary Value, etc.)
|
|
5
|
+
* that is exposed through the BACnet Server. It:
|
|
6
|
+
* - Registers itself with the parent bacnet-server config node
|
|
7
|
+
* - Accepts input messages to update the present value
|
|
8
|
+
* - Emits output messages when external clients read/write the point
|
|
9
|
+
* - Supports Priority Array for commandable objects
|
|
10
|
+
* - Supports COV (Change of Value) threshold configuration
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
module.exports = function (RED) {
|
|
14
|
+
|
|
15
|
+
// BACnet object type constants
|
|
16
|
+
const ObjectTypes = {
|
|
17
|
+
'analogInput': 0,
|
|
18
|
+
'analogOutput': 1,
|
|
19
|
+
'analogValue': 2,
|
|
20
|
+
'binaryInput': 3,
|
|
21
|
+
'binaryOutput': 4,
|
|
22
|
+
'binaryValue': 5,
|
|
23
|
+
'multiStateValue': 19
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Object types that support Priority Array (commandable objects)
|
|
27
|
+
const CommandableTypes = ['analogOutput', 'analogValue', 'binaryOutput', 'binaryValue'];
|
|
28
|
+
|
|
29
|
+
function BacnetPointNode(config) {
|
|
30
|
+
RED.nodes.createNode(this, config);
|
|
31
|
+
const node = this;
|
|
32
|
+
|
|
33
|
+
// Get the server config node
|
|
34
|
+
node.server = RED.nodes.getNode(config.server);
|
|
35
|
+
|
|
36
|
+
if (!node.server) {
|
|
37
|
+
node.status({ fill: 'red', shape: 'ring', text: 'No server configured' });
|
|
38
|
+
node.error('No BACnet server configured');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Configuration
|
|
43
|
+
node.objectType = config.objectType || 'analogValue';
|
|
44
|
+
node.objectTypeId = ObjectTypes[node.objectType];
|
|
45
|
+
node.instanceNumber = parseInt(config.instanceNumber) || 0;
|
|
46
|
+
node.objectName = config.objectName || `${node.objectType}_${node.instanceNumber}`;
|
|
47
|
+
node.initialValue = parseFloat(config.initialValue) || 0;
|
|
48
|
+
node.units = config.units !== '' ? parseInt(config.units) : undefined;
|
|
49
|
+
node.description = config.description || '';
|
|
50
|
+
node.writable = config.writable !== false;
|
|
51
|
+
node.usePriorityArray = config.usePriorityArray === true && CommandableTypes.includes(node.objectType);
|
|
52
|
+
node.relinquishDefault = parseFloat(config.relinquishDefault) || 0;
|
|
53
|
+
node.covIncrement = parseFloat(config.covIncrement) || 0;
|
|
54
|
+
node.numberOfStates = parseInt(config.numberOfStates) || 3;
|
|
55
|
+
node.stateText = config.stateText ? config.stateText.split(',').map(s => s.trim()) : undefined;
|
|
56
|
+
|
|
57
|
+
// Output options
|
|
58
|
+
node.outputOnRead = config.outputOnRead === true;
|
|
59
|
+
node.outputOnWrite = config.outputOnWrite !== false;
|
|
60
|
+
node.outputOnCov = config.outputOnCov === true;
|
|
61
|
+
|
|
62
|
+
// Current value
|
|
63
|
+
node.currentValue = node.initialValue;
|
|
64
|
+
|
|
65
|
+
// Register with server
|
|
66
|
+
node.registerWithServer = function () {
|
|
67
|
+
if (!node.server) return;
|
|
68
|
+
|
|
69
|
+
const objectData = {
|
|
70
|
+
objectType: node.objectTypeId,
|
|
71
|
+
instanceNumber: node.instanceNumber,
|
|
72
|
+
objectName: node.objectName,
|
|
73
|
+
initialValue: node.initialValue,
|
|
74
|
+
units: node.units,
|
|
75
|
+
description: node.description,
|
|
76
|
+
writable: node.writable,
|
|
77
|
+
priorityArray: node.usePriorityArray,
|
|
78
|
+
relinquishDefault: node.relinquishDefault,
|
|
79
|
+
covIncrement: node.covIncrement,
|
|
80
|
+
numberOfStates: node.objectType === 'multiStateValue' ? node.numberOfStates : undefined,
|
|
81
|
+
stateText: node.objectType === 'multiStateValue' ? node.stateText : undefined
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
node.server.registerObject(objectData, node);
|
|
85
|
+
node.updateStatus();
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Update node status
|
|
89
|
+
node.updateStatus = function () {
|
|
90
|
+
const typeLabel = node.objectType.replace(/([A-Z])/g, ' $1').trim();
|
|
91
|
+
node.status({
|
|
92
|
+
fill: 'green',
|
|
93
|
+
shape: 'dot',
|
|
94
|
+
text: `${typeLabel} ${node.instanceNumber}: ${formatValue(node.currentValue)}`
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Format value for status display
|
|
99
|
+
function formatValue(val) {
|
|
100
|
+
if (typeof val === 'number') {
|
|
101
|
+
if (Number.isInteger(val)) {
|
|
102
|
+
return val.toString();
|
|
103
|
+
}
|
|
104
|
+
return val.toFixed(2);
|
|
105
|
+
}
|
|
106
|
+
return String(val);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Emit message when external client reads the point
|
|
110
|
+
node.emitReadEvent = function (value) {
|
|
111
|
+
if (!node.outputOnRead) return;
|
|
112
|
+
|
|
113
|
+
const msg = {
|
|
114
|
+
payload: value,
|
|
115
|
+
topic: node.objectName,
|
|
116
|
+
bacnet: {
|
|
117
|
+
event: 'read',
|
|
118
|
+
objectType: node.objectType,
|
|
119
|
+
objectTypeId: node.objectTypeId,
|
|
120
|
+
instanceNumber: node.instanceNumber,
|
|
121
|
+
objectName: node.objectName
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
node.send(msg);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Emit message when external client writes to the point
|
|
129
|
+
node.emitWriteEvent = function (value, priority, address) {
|
|
130
|
+
node.currentValue = value;
|
|
131
|
+
node.updateStatus();
|
|
132
|
+
|
|
133
|
+
if (!node.outputOnWrite) return;
|
|
134
|
+
|
|
135
|
+
const msg = {
|
|
136
|
+
payload: value,
|
|
137
|
+
topic: node.objectName,
|
|
138
|
+
bacnet: {
|
|
139
|
+
event: 'write',
|
|
140
|
+
objectType: node.objectType,
|
|
141
|
+
objectTypeId: node.objectTypeId,
|
|
142
|
+
instanceNumber: node.instanceNumber,
|
|
143
|
+
objectName: node.objectName,
|
|
144
|
+
priority: priority,
|
|
145
|
+
source: address
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
node.send(msg);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Emit message on COV notification
|
|
153
|
+
node.emitCovEvent = function (value) {
|
|
154
|
+
if (!node.outputOnCov) return;
|
|
155
|
+
|
|
156
|
+
const msg = {
|
|
157
|
+
payload: value,
|
|
158
|
+
topic: node.objectName,
|
|
159
|
+
bacnet: {
|
|
160
|
+
event: 'cov',
|
|
161
|
+
objectType: node.objectType,
|
|
162
|
+
objectTypeId: node.objectTypeId,
|
|
163
|
+
instanceNumber: node.instanceNumber,
|
|
164
|
+
objectName: node.objectName
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
node.send(msg);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Handle incoming messages to update value
|
|
172
|
+
node.on('input', function (msg, send, done) {
|
|
173
|
+
let newValue;
|
|
174
|
+
let priority = 16;
|
|
175
|
+
|
|
176
|
+
// Extract value from message
|
|
177
|
+
if (typeof msg.payload === 'object' && msg.payload !== null) {
|
|
178
|
+
// Structured payload
|
|
179
|
+
if (msg.payload.value !== undefined) {
|
|
180
|
+
newValue = msg.payload.value;
|
|
181
|
+
}
|
|
182
|
+
if (msg.payload.priority !== undefined) {
|
|
183
|
+
priority = parseInt(msg.payload.priority);
|
|
184
|
+
if (priority < 1 || priority > 16) priority = 16;
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
// Simple payload
|
|
188
|
+
newValue = msg.payload;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check for priority in msg.bacnet
|
|
192
|
+
if (msg.bacnet && msg.bacnet.priority !== undefined) {
|
|
193
|
+
priority = parseInt(msg.bacnet.priority);
|
|
194
|
+
if (priority < 1 || priority > 16) priority = 16;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Validate and convert value based on object type
|
|
198
|
+
if (newValue === undefined || newValue === null) {
|
|
199
|
+
// Null value means relinquish at this priority (for priority array)
|
|
200
|
+
if (node.usePriorityArray) {
|
|
201
|
+
newValue = null;
|
|
202
|
+
} else {
|
|
203
|
+
if (done) done();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
switch (node.objectType) {
|
|
208
|
+
case 'binaryInput':
|
|
209
|
+
case 'binaryOutput':
|
|
210
|
+
case 'binaryValue':
|
|
211
|
+
// Convert to binary (0 or 1)
|
|
212
|
+
if (typeof newValue === 'boolean') {
|
|
213
|
+
newValue = newValue ? 1 : 0;
|
|
214
|
+
} else if (typeof newValue === 'string') {
|
|
215
|
+
const lower = newValue.toLowerCase();
|
|
216
|
+
if (lower === 'true' || lower === 'on' || lower === 'active' || lower === '1') {
|
|
217
|
+
newValue = 1;
|
|
218
|
+
} else {
|
|
219
|
+
newValue = 0;
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
newValue = newValue ? 1 : 0;
|
|
223
|
+
}
|
|
224
|
+
break;
|
|
225
|
+
|
|
226
|
+
case 'multiStateValue':
|
|
227
|
+
// Convert to integer, ensure within valid range
|
|
228
|
+
newValue = parseInt(newValue);
|
|
229
|
+
if (isNaN(newValue) || newValue < 1) newValue = 1;
|
|
230
|
+
if (newValue > node.numberOfStates) newValue = node.numberOfStates;
|
|
231
|
+
break;
|
|
232
|
+
|
|
233
|
+
case 'analogInput':
|
|
234
|
+
case 'analogOutput':
|
|
235
|
+
case 'analogValue':
|
|
236
|
+
default:
|
|
237
|
+
// Convert to float
|
|
238
|
+
newValue = parseFloat(newValue);
|
|
239
|
+
if (isNaN(newValue)) newValue = 0;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Update the server object
|
|
245
|
+
if (node.server) {
|
|
246
|
+
// For priority array objects, we need to handle priority
|
|
247
|
+
if (node.usePriorityArray) {
|
|
248
|
+
// Update through the server which handles priority array
|
|
249
|
+
const objectId = { type: node.objectTypeId, instance: node.instanceNumber };
|
|
250
|
+
const result = node.server.setPropertyValue(
|
|
251
|
+
objectId,
|
|
252
|
+
85, // PRESENT_VALUE property ID
|
|
253
|
+
[{ type: newValue === null ? 0 : 4, value: newValue }],
|
|
254
|
+
priority
|
|
255
|
+
);
|
|
256
|
+
if (result.success) {
|
|
257
|
+
node.currentValue = result.value;
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
node.server.updateObjectValue(node.objectTypeId, node.instanceNumber, newValue);
|
|
261
|
+
node.currentValue = newValue;
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
node.currentValue = newValue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
node.updateStatus();
|
|
268
|
+
|
|
269
|
+
if (done) done();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Wait for server to be ready before registering
|
|
273
|
+
if (node.server.client) {
|
|
274
|
+
// Server already initialized
|
|
275
|
+
node.registerWithServer();
|
|
276
|
+
} else {
|
|
277
|
+
// Wait for server ready event
|
|
278
|
+
node.server.on('ready', function () {
|
|
279
|
+
node.registerWithServer();
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Handle node close
|
|
284
|
+
node.on('close', function (done) {
|
|
285
|
+
if (node.server) {
|
|
286
|
+
node.server.unregisterObject(node.objectTypeId, node.instanceNumber);
|
|
287
|
+
}
|
|
288
|
+
done();
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
RED.nodes.registerType('bacnet-point', BacnetPointNode);
|
|
293
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('bacnet-server', {
|
|
3
|
+
category: 'config',
|
|
4
|
+
defaults: {
|
|
5
|
+
name: { value: '' },
|
|
6
|
+
deviceId: { value: 123456, required: true, validate: RED.validators.number() },
|
|
7
|
+
deviceName: { value: 'Node-RED-BACnet', required: true },
|
|
8
|
+
vendorId: { value: 999, required: true, validate: RED.validators.number() },
|
|
9
|
+
port: { value: 47808, required: true, validate: RED.validators.number() },
|
|
10
|
+
interface: { value: '' },
|
|
11
|
+
broadcastAddress: { value: '255.255.255.255' }
|
|
12
|
+
},
|
|
13
|
+
label: function () {
|
|
14
|
+
return this.name || this.deviceName || 'BACnet Server';
|
|
15
|
+
},
|
|
16
|
+
oneditprepare: function () {
|
|
17
|
+
var node = this;
|
|
18
|
+
|
|
19
|
+
// Validate device ID range (0 to 4194302)
|
|
20
|
+
$('#node-config-input-deviceId').on('change', function () {
|
|
21
|
+
var val = parseInt($(this).val());
|
|
22
|
+
if (isNaN(val) || val < 0 || val > 4194302) {
|
|
23
|
+
RED.notify('Device ID must be between 0 and 4194302', 'error');
|
|
24
|
+
$(this).val(123456);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Validate port range
|
|
29
|
+
$('#node-config-input-port').on('change', function () {
|
|
30
|
+
var val = parseInt($(this).val());
|
|
31
|
+
if (isNaN(val) || val < 1 || val > 65535) {
|
|
32
|
+
RED.notify('Port must be between 1 and 65535', 'error');
|
|
33
|
+
$(this).val(47808);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Validate vendor ID range (0 to 65535)
|
|
38
|
+
$('#node-config-input-vendorId').on('change', function () {
|
|
39
|
+
var val = parseInt($(this).val());
|
|
40
|
+
if (isNaN(val) || val < 0 || val > 65535) {
|
|
41
|
+
RED.notify('Vendor ID must be between 0 and 65535', 'error');
|
|
42
|
+
$(this).val(999);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<script type="text/html" data-template-name="bacnet-server">
|
|
50
|
+
<div class="form-row">
|
|
51
|
+
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
52
|
+
<input type="text" id="node-config-input-name" placeholder="Optional name">
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="form-row">
|
|
56
|
+
<h4><i class="fa fa-server"></i> Device Configuration</h4>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="form-row">
|
|
60
|
+
<label for="node-config-input-deviceId"><i class="fa fa-hashtag"></i> Device ID</label>
|
|
61
|
+
<input type="number" id="node-config-input-deviceId" min="0" max="4194302" style="width: 120px;">
|
|
62
|
+
<span class="form-tips" style="margin-left: 10px;">Instance number (0-4194302)</span>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="form-row">
|
|
66
|
+
<label for="node-config-input-deviceName"><i class="fa fa-bookmark"></i> Device Name</label>
|
|
67
|
+
<input type="text" id="node-config-input-deviceName" placeholder="Node-RED-BACnet">
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="form-row">
|
|
71
|
+
<label for="node-config-input-vendorId"><i class="fa fa-building"></i> Vendor ID</label>
|
|
72
|
+
<input type="number" id="node-config-input-vendorId" min="0" max="65535" style="width: 100px;">
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="form-row">
|
|
76
|
+
<h4><i class="fa fa-plug"></i> Network Configuration</h4>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div class="form-row">
|
|
80
|
+
<label for="node-config-input-port"><i class="fa fa-globe"></i> UDP Port</label>
|
|
81
|
+
<input type="number" id="node-config-input-port" min="1" max="65535" style="width: 100px;">
|
|
82
|
+
<span class="form-tips" style="margin-left: 10px;">Default: 47808 (0xBAC0)</span>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="form-row">
|
|
86
|
+
<label for="node-config-input-interface"><i class="fa fa-ethernet"></i> Interface</label>
|
|
87
|
+
<input type="text" id="node-config-input-interface" placeholder="Leave empty for all interfaces">
|
|
88
|
+
<span class="form-tips" style="margin-left: 10px;">e.g., 192.168.1.100</span>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="form-row">
|
|
92
|
+
<label for="node-config-input-broadcastAddress"><i class="fa fa-broadcast-tower"></i> Broadcast</label>
|
|
93
|
+
<input type="text" id="node-config-input-broadcastAddress" placeholder="255.255.255.255">
|
|
94
|
+
</div>
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<script type="text/html" data-help-name="bacnet-server">
|
|
98
|
+
<p>BACnet Server configuration node that exposes Node-RED as a BACnet device on the network.</p>
|
|
99
|
+
|
|
100
|
+
<h3>Configuration</h3>
|
|
101
|
+
<dl class="message-properties">
|
|
102
|
+
<dt>Device ID <span class="property-type">number</span></dt>
|
|
103
|
+
<dd>The BACnet device instance number (0-4194302). Must be unique on the network.</dd>
|
|
104
|
+
|
|
105
|
+
<dt>Device Name <span class="property-type">string</span></dt>
|
|
106
|
+
<dd>The name of this BACnet device as it will appear to other devices.</dd>
|
|
107
|
+
|
|
108
|
+
<dt>Vendor ID <span class="property-type">number</span></dt>
|
|
109
|
+
<dd>The BACnet vendor identifier (0-65535). Use 999 for experimental/private use.</dd>
|
|
110
|
+
|
|
111
|
+
<dt>UDP Port <span class="property-type">number</span></dt>
|
|
112
|
+
<dd>The UDP port for BACnet communication. Default is 47808 (0xBAC0).</dd>
|
|
113
|
+
|
|
114
|
+
<dt>Interface <span class="property-type">string</span></dt>
|
|
115
|
+
<dd>Network interface IP to bind to. Leave empty to listen on all interfaces.</dd>
|
|
116
|
+
|
|
117
|
+
<dt>Broadcast Address <span class="property-type">string</span></dt>
|
|
118
|
+
<dd>The broadcast address for BACnet discovery. Usually 255.255.255.255.</dd>
|
|
119
|
+
</dl>
|
|
120
|
+
|
|
121
|
+
<h3>Details</h3>
|
|
122
|
+
<p>This configuration node manages the BACnet server infrastructure:</p>
|
|
123
|
+
<ul>
|
|
124
|
+
<li><b>Who-Is / I-Am</b>: Responds to device discovery requests</li>
|
|
125
|
+
<li><b>Read Property</b>: Allows external clients to read object values</li>
|
|
126
|
+
<li><b>Write Property</b>: Allows external clients to write object values</li>
|
|
127
|
+
<li><b>COV Subscriptions</b>: Supports Change of Value notifications</li>
|
|
128
|
+
</ul>
|
|
129
|
+
|
|
130
|
+
<p>Use <code>bacnet-point</code> nodes to create BACnet objects (Analog Value, Binary Value, etc.)
|
|
131
|
+
that are exposed through this server.</p>
|
|
132
|
+
|
|
133
|
+
<h3>References</h3>
|
|
134
|
+
<ul>
|
|
135
|
+
<li><a href="http://www.bacnet.org/">BACnet International</a></li>
|
|
136
|
+
<li><a href="https://github.com/BACnet-IT/node-bacnet">node-bacnet library</a></li>
|
|
137
|
+
</ul>
|
|
138
|
+
</script>
|