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,652 @@
|
|
|
1
|
+
module.exports = function (RED) {
|
|
2
|
+
function LoRaBAC(config) {
|
|
3
|
+
RED.nodes.createNode(this, config);
|
|
4
|
+
var node = this;
|
|
5
|
+
const flow = node.context().flow;
|
|
6
|
+
|
|
7
|
+
node.warn(config.deviceList); // Display in debug
|
|
8
|
+
|
|
9
|
+
let status = verifyDeviceList(config.deviceList);
|
|
10
|
+
displayStatus(node, status);
|
|
11
|
+
if (status.ok) {
|
|
12
|
+
if (config.globalConfig.protocol === "restAPIBacnet") {
|
|
13
|
+
generateHttpAuthentication(config.deviceList);
|
|
14
|
+
}
|
|
15
|
+
setupGlobalVariables(this, config);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
node.on("input", function (msg) {
|
|
19
|
+
let device = createObject(this, msg);
|
|
20
|
+
const result = {
|
|
21
|
+
"device": device,
|
|
22
|
+
}
|
|
23
|
+
node.send(result);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ==================================================
|
|
29
|
+
// ============== UTIL FUNCTIONS ====================
|
|
30
|
+
// ==================================================
|
|
31
|
+
|
|
32
|
+
function generateHttpAuthentication(deviceList) {
|
|
33
|
+
for (let device in deviceList) {
|
|
34
|
+
const buffer = Buffer.from(deviceList[device].controller.login + ':' + deviceList[device].controller.password);
|
|
35
|
+
deviceList[device].controller.httpAuthentication = "Basic " + buffer.toString('base64');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function displayStatus(node, status){
|
|
40
|
+
if (status.ok) {
|
|
41
|
+
node.status({ fill: "green", shape: "dot", text: "Configuration OK" });
|
|
42
|
+
} else {
|
|
43
|
+
let message = status.message;
|
|
44
|
+
delete status.ok
|
|
45
|
+
delete status.message
|
|
46
|
+
node.status({
|
|
47
|
+
fill: "red",
|
|
48
|
+
shape: "dot",
|
|
49
|
+
text: message || "Invalid configuration"
|
|
50
|
+
});
|
|
51
|
+
node.error(`Error in device list : ${message}`, status);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
function setupGlobalVariables(node, config) {
|
|
57
|
+
const flow = node.context().flow;
|
|
58
|
+
const global = node.context().global;
|
|
59
|
+
|
|
60
|
+
global.set('g_deviceList', config.deviceList);
|
|
61
|
+
|
|
62
|
+
flow.set('g_httpRequestTimeOut', 5000);
|
|
63
|
+
flow.set('g_tts_topicDownlinkSuffix', "/down");
|
|
64
|
+
flow.set('g_tts_topicUplinkSuffix', "/up");
|
|
65
|
+
flow.set('g_chirp_topicDownlinkSuffix', "/command/down");
|
|
66
|
+
flow.set('g_chirp_topicUplinkSuffix', "/event/up");
|
|
67
|
+
flow.set('g_actility_topicDownlinkSuffix', "/downlink");
|
|
68
|
+
flow.set('g_actility_topicUplinkSuffix', "/uplink");
|
|
69
|
+
if (flow.get("g_previousValues") === undefined) {
|
|
70
|
+
flow.set("g_previousValues", {});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const debug = function (device, debugType, debugText) {
|
|
74
|
+
if (debugType == "forceOn") {
|
|
75
|
+
node.warn(debugText);
|
|
76
|
+
}
|
|
77
|
+
else if (device.controller.debug?.some(element => element === "all" || element === debugType)) {
|
|
78
|
+
node.warn(debugText);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
flow.set("g_debug", debug);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createObject(node, msg){
|
|
88
|
+
const flow = node.context().flow;
|
|
89
|
+
const global = node.context().global;
|
|
90
|
+
|
|
91
|
+
let deviceList = global.get('g_deviceList');
|
|
92
|
+
let networkServer;
|
|
93
|
+
let deviceName, deviceType, deviceNum, devEUI, topicDownlink;
|
|
94
|
+
let devicePayload = {};
|
|
95
|
+
let previousValues = flow.get("g_previousValues");
|
|
96
|
+
|
|
97
|
+
let topicUp = msg.topic;
|
|
98
|
+
|
|
99
|
+
// Guess the NetworkServer from the received frame
|
|
100
|
+
if (msg.payload.hasOwnProperty('deviceInfo')) networkServer = "chirpstack";
|
|
101
|
+
if (msg.payload.hasOwnProperty('end_device_ids')) networkServer = "tts";
|
|
102
|
+
if (msg.payload.hasOwnProperty('DevEUI_uplink')) networkServer = "actility";
|
|
103
|
+
|
|
104
|
+
// Reject messages from Actility :
|
|
105
|
+
if ('DevEUI_notification' in msg.payload || 'DevEUI_notification' in msg.payload) return null;
|
|
106
|
+
if ('DevEUI_downlink_Rejected' in msg.payload) {
|
|
107
|
+
node.error("Actility : Downlink Message Rejected");
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
//////////////////////////////////////////////////////////////////////////
|
|
112
|
+
// The Things Stack Network Server
|
|
113
|
+
/////////////////////////////////////////////////////////////////////////
|
|
114
|
+
|
|
115
|
+
if (networkServer == "tts") {
|
|
116
|
+
deviceName = msg.payload.end_device_ids.device_id;
|
|
117
|
+
topicDownlink = topicUp.replace(flow.get('g_tts_topicUplinkSuffix'), "") + flow.get('g_tts_topicDownlinkSuffix');
|
|
118
|
+
devEUI = msg.payload.end_device_ids.dev_eui;
|
|
119
|
+
if (!Object.keys(msg.payload.uplink_message).some(element => element == "decoded_payload")) {
|
|
120
|
+
node.error(deviceName + " : No payload decoder configured on the Network Server");
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
devicePayload = msg.payload.uplink_message.decoded_payload;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
//////////////////////////////////////////////////////////////////////////
|
|
128
|
+
// Chirpstack Network Server
|
|
129
|
+
/////////////////////////////////////////////////////////////////////////
|
|
130
|
+
|
|
131
|
+
if (networkServer == "chirpstack") {
|
|
132
|
+
if (msg.payload.fPort == 0) return 0;
|
|
133
|
+
deviceName = msg.payload.deviceInfo.deviceName;
|
|
134
|
+
topicDownlink = topicUp.replace(flow.get('g_chirp_topicUplinkSuffix'), "") + flow.get('g_chirp_topicDownlinkSuffix');
|
|
135
|
+
devEUI = msg.payload.deviceInfo.devEui;
|
|
136
|
+
if (!Object.keys(msg.payload).some(element => element == "object")) {
|
|
137
|
+
node.error(deviceName + " : No payload decoder configured on the Network Server");
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
devicePayload = msg.payload.object;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
//////////////////////////////////////////////////////////////////////////
|
|
144
|
+
// Actility Network Server
|
|
145
|
+
/////////////////////////////////////////////////////////////////////////
|
|
146
|
+
|
|
147
|
+
if (networkServer == "actility") {
|
|
148
|
+
deviceName = msg.payload.DevEUI_uplink.CustomerData.name;
|
|
149
|
+
topicDownlink = topicUp.replace(flow.get('g_actility_topicUplinkSuffix'), "") + flow.get('g_actility_topicDownlinkSuffix');
|
|
150
|
+
devEUI = msg.payload.DevEUI_uplink.DevEUI;
|
|
151
|
+
if (!Object.keys(msg.payload.DevEUI_uplink).some(element => element == "payload")) {
|
|
152
|
+
node.error(deviceName + " : No payload decoder configured on the Network Server");
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
devicePayload = msg.payload.DevEUI_uplink.payload;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
//////////////////////////////////////////////////////////////////////////
|
|
159
|
+
// Checks
|
|
160
|
+
/////////////////////////////////////////////////////////////////////////
|
|
161
|
+
const match = deviceName.match(/^(.*)-(\d+)$/);
|
|
162
|
+
if (match) {
|
|
163
|
+
deviceType = match[1]; // The part before the last dash
|
|
164
|
+
deviceNum = parseInt(match[2], 10); // The number at the end, converted to an integer
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
node.error("Error: Device Name (" + deviceName + ") does not respect *xxx - num* format",
|
|
168
|
+
{
|
|
169
|
+
errorType: "deviceName",
|
|
170
|
+
value: deviceName,
|
|
171
|
+
});
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if ((deviceNum == 0)) {
|
|
176
|
+
node.error("Error: Device Num is 0 is not allowed (" + deviceName + ")",
|
|
177
|
+
{
|
|
178
|
+
errorType: "deviceName",
|
|
179
|
+
value: deviceName,
|
|
180
|
+
});
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (deviceList[deviceType] == undefined) {
|
|
185
|
+
node.error("Error: Device Type does not belong to the Device List (" + deviceName + ")",
|
|
186
|
+
{
|
|
187
|
+
errorType: "deviceName",
|
|
188
|
+
value: deviceName,
|
|
189
|
+
});
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check deviceNum overflow
|
|
194
|
+
if (deviceNum > deviceList[deviceType].identity.maxDevNum) {
|
|
195
|
+
node.error("Error: Device number is too high (" + deviceName + ")",
|
|
196
|
+
{
|
|
197
|
+
errorType: "deviceName",
|
|
198
|
+
value: deviceName,
|
|
199
|
+
});
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
//////////////////////////////////////////////////////////////////////////
|
|
204
|
+
// Create a copy of the "deviceType" object of the "deviceList" structure
|
|
205
|
+
/////////////////////////////////////////////////////////////////////////
|
|
206
|
+
let device = JSON.parse(JSON.stringify(deviceList[deviceType]));
|
|
207
|
+
|
|
208
|
+
device.identity.deviceName = deviceName;
|
|
209
|
+
device.identity.deviceType = deviceType;
|
|
210
|
+
device.identity.deviceNum = deviceNum;
|
|
211
|
+
device.identity.devEUI = devEUI;
|
|
212
|
+
device.mqtt.topicDownlink = topicDownlink;
|
|
213
|
+
|
|
214
|
+
for (let object in device.bacnet.objects) {
|
|
215
|
+
// Update instanceNum
|
|
216
|
+
switch (device.bacnet.objects[object].assignementMode) {
|
|
217
|
+
case "manual":
|
|
218
|
+
|
|
219
|
+
break;
|
|
220
|
+
case "auto":
|
|
221
|
+
switch (device.bacnet.objects[object].objectType) {
|
|
222
|
+
case "analogValue":
|
|
223
|
+
device.bacnet.objects[object].instanceNum += device.bacnet.offsetAV + (device.bacnet.instanceRangeAV * deviceNum);
|
|
224
|
+
break;
|
|
225
|
+
case "binaryValue":
|
|
226
|
+
device.bacnet.objects[object].instanceNum += device.bacnet.offsetBV + (device.bacnet.instanceRangeBV * deviceNum);
|
|
227
|
+
break;
|
|
228
|
+
default:
|
|
229
|
+
node.error("Object type of " + object + " is unknown : " + device.bacnet.objects[object].objectType);
|
|
230
|
+
return null;
|
|
231
|
+
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
default:
|
|
235
|
+
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Update objectName
|
|
239
|
+
device.bacnet.objects[object].objectName = deviceName + '-' + object + '-' + device.bacnet.objects[object].instanceNum;
|
|
240
|
+
// Update value
|
|
241
|
+
if (device.bacnet.objects[object].dataDirection == "uplink") {
|
|
242
|
+
let lorawanPayloadName = device.bacnet.objects[object].lorawanPayloadName;
|
|
243
|
+
let keys = lorawanPayloadName.split(/[\.\[\]]/).filter(key => key !== "");
|
|
244
|
+
let value = keys.reduce((accumulator, currentValue) => accumulator[currentValue], devicePayload);
|
|
245
|
+
device.bacnet.objects[object].value = value;
|
|
246
|
+
}
|
|
247
|
+
// Check value
|
|
248
|
+
if (device.bacnet.objects[object].value == undefined || typeof device.bacnet.objects[object].value == "object") {
|
|
249
|
+
node.error(`Device : ${device.identity.deviceName} - Object : ${object} - Wrong Payload decoder or Wrong Device description`);
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (device.controller.protocol == "bacnet") {
|
|
254
|
+
// "restAPIBacnet" and "bacnet" compatibility
|
|
255
|
+
switch (device.bacnet.objects[object].objectType) {
|
|
256
|
+
case "analogValue": device.bacnet.objects[object].objectType = 2; break;
|
|
257
|
+
case "binaryValue": device.bacnet.objects[object].objectType = 5; break;
|
|
258
|
+
}
|
|
259
|
+
// Keep only uplink payload in a new object
|
|
260
|
+
device.bacnet.uplinkKeys = Object.entries(device.bacnet.objects)
|
|
261
|
+
.filter(([key, obj]) => obj.dataDirection === "uplink")
|
|
262
|
+
.map(([key, obj]) => key);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// For debug
|
|
267
|
+
device.transmitTime = Date.now();
|
|
268
|
+
|
|
269
|
+
// For InfluxDB support
|
|
270
|
+
device.influxdb = {
|
|
271
|
+
"source": "uplink"
|
|
272
|
+
};
|
|
273
|
+
// To save previous values
|
|
274
|
+
if (!previousValues.hasOwnProperty(device.identity.deviceName)) {
|
|
275
|
+
|
|
276
|
+
previousValues[device.identity.deviceName] = RED.util.cloneMessage(device);
|
|
277
|
+
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return device;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function verifyDeviceList(deviceList) {
|
|
284
|
+
const regexIP = /^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$/;
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
let objectInstanceArrayAVManual = [], objectInstanceArrayBVManual = [];
|
|
289
|
+
let deviceNameArray = [];
|
|
290
|
+
|
|
291
|
+
//#region ///// IP Check /////
|
|
292
|
+
for (let device in deviceList) {
|
|
293
|
+
const dev = deviceList[device];
|
|
294
|
+
|
|
295
|
+
if (!regexIP.test(dev?.controller?.ipAddress)){
|
|
296
|
+
return {
|
|
297
|
+
ok: false,
|
|
298
|
+
message: "Indvalid IP address",
|
|
299
|
+
value: dev.controller.ipAddress
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
//#endregion
|
|
304
|
+
|
|
305
|
+
//#region ///// Range Check /////
|
|
306
|
+
for (let device in deviceList) {
|
|
307
|
+
const dev = deviceList[device];
|
|
308
|
+
for (let object in dev.bacnet.objects) {
|
|
309
|
+
const obj = dev.bacnet.objects[object];
|
|
310
|
+
if (obj.dataDirection === "downlink") {
|
|
311
|
+
|
|
312
|
+
if (obj.downlinkStrategy === "onChangeOfThisValueWithinRange" || obj.downlinkStrategy === "compareToUplinkObjectWithinRange" ) {
|
|
313
|
+
|
|
314
|
+
if (obj.range.length !== 2 || obj.range[1] < obj.range[0]) {
|
|
315
|
+
return {
|
|
316
|
+
ok: false,
|
|
317
|
+
errorType: "deviceListBACnetObjectConfiguration",
|
|
318
|
+
message: `Invalid range configuration for object ${object} of device ${device}`,
|
|
319
|
+
device,
|
|
320
|
+
object,
|
|
321
|
+
property: "range",
|
|
322
|
+
value: obj.range
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
//#endregion
|
|
331
|
+
|
|
332
|
+
//#region ///// uplinkObjectToCompareWith Check /////
|
|
333
|
+
for (let device in deviceList) {
|
|
334
|
+
|
|
335
|
+
const dev = deviceList[device];
|
|
336
|
+
let uplinkToCompareObjectArray = [], uplinkObjectArray = [];
|
|
337
|
+
|
|
338
|
+
for (let object in dev.bacnet.objects) {
|
|
339
|
+
const obj = dev.bacnet.objects[object];
|
|
340
|
+
|
|
341
|
+
if (obj.dataDirection === "downlink") {
|
|
342
|
+
|
|
343
|
+
if (obj.downlinkStrategy === "compareToUplinkObject" || obj.downlinkStrategy === "compareToUplinkObjectWithinRange") {
|
|
344
|
+
uplinkToCompareObjectArray.push(obj.uplinkToCompareWith)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
}else{
|
|
348
|
+
|
|
349
|
+
uplinkObjectArray.push(object);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
uplinkToCompareObjectArray.forEach(element => {
|
|
354
|
+
if (uplinkObjectArray.some(uplink => uplink === element)){
|
|
355
|
+
return {
|
|
356
|
+
ok: false,
|
|
357
|
+
errorType: "deviceListBACnetConfiguration",
|
|
358
|
+
message: `${element} is not an uplink object for device ${device}`,
|
|
359
|
+
device,
|
|
360
|
+
property: "instanceNum",
|
|
361
|
+
value: dev.bacnet.instanceRangeBV
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
//#endregion
|
|
367
|
+
|
|
368
|
+
//#region ///// AssignementMode Check /////
|
|
369
|
+
for (let device in deviceList) {
|
|
370
|
+
const dev = deviceList[device];
|
|
371
|
+
|
|
372
|
+
let assignementMode = null;
|
|
373
|
+
|
|
374
|
+
for (let object in dev.bacnet.objects) {
|
|
375
|
+
const obj = dev.bacnet.objects[object];
|
|
376
|
+
|
|
377
|
+
if (assignementMode === null) {
|
|
378
|
+
assignementMode = obj.assignementMode;
|
|
379
|
+
} else if (assignementMode !== obj.assignementMode) {
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
errorType: "deviceListBACnetObjectConfiguration",
|
|
383
|
+
message: `Objects have different assignement modes for device ${device}`,
|
|
384
|
+
device,
|
|
385
|
+
object,
|
|
386
|
+
property: "assignementMode",
|
|
387
|
+
value: obj.assignementMode
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
//#endregion
|
|
393
|
+
|
|
394
|
+
//#region ///// AV InstanceNum Check /////
|
|
395
|
+
for (let device in deviceList) {
|
|
396
|
+
|
|
397
|
+
const dev = deviceList[device];
|
|
398
|
+
|
|
399
|
+
for (let object in dev.bacnet.objects) {
|
|
400
|
+
const obj = dev.bacnet.objects[object];
|
|
401
|
+
|
|
402
|
+
if (obj.objectType === "analogValue") {
|
|
403
|
+
if (obj.assignementMode !== "manual" && obj.instanceNum >= dev.bacnet.instanceRangeAV) {
|
|
404
|
+
return {
|
|
405
|
+
ok: false,
|
|
406
|
+
errorType: "deviceListBACnetObjectConfiguration",
|
|
407
|
+
message: `Analog instanceNum too high for object ${object} of device ${device}`,
|
|
408
|
+
device,
|
|
409
|
+
object,
|
|
410
|
+
property: "instanceNum",
|
|
411
|
+
value: obj.instanceNum
|
|
412
|
+
};
|
|
413
|
+
} else if (obj.assignementMode === "manual") {
|
|
414
|
+
objectInstanceArrayAVManual.push(obj.instanceNum);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
//#endregion
|
|
420
|
+
|
|
421
|
+
//#region ///// BV InstanceNum Check /////
|
|
422
|
+
for (let device in deviceList) {
|
|
423
|
+
|
|
424
|
+
const dev = deviceList[device];
|
|
425
|
+
|
|
426
|
+
for (let object in dev.bacnet.objects) {
|
|
427
|
+
const obj = dev.bacnet.objects[object];
|
|
428
|
+
|
|
429
|
+
if (obj.objectType === "binaryValue") {
|
|
430
|
+
if (obj.assignementMode !== "manual" && obj.instanceNum >= dev.bacnet.instanceRangeBV) {
|
|
431
|
+
return {
|
|
432
|
+
ok: false,
|
|
433
|
+
errorType: "deviceListBACnetObjectConfiguration",
|
|
434
|
+
message: `Binary instanceNum too high for object ${object} of device ${device}`,
|
|
435
|
+
device,
|
|
436
|
+
object,
|
|
437
|
+
property: "instanceNum",
|
|
438
|
+
value: obj.instanceNum
|
|
439
|
+
};
|
|
440
|
+
} else if (obj.assignementMode === "manual") {
|
|
441
|
+
objectInstanceArrayBVManual.push(obj.instanceNum);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
//#endregion
|
|
447
|
+
|
|
448
|
+
//#region ///// objects Name Check /////
|
|
449
|
+
for (let device in deviceList) {
|
|
450
|
+
|
|
451
|
+
const dev = deviceList[device];
|
|
452
|
+
let objectNameArray = []
|
|
453
|
+
|
|
454
|
+
for (let object in dev.bacnet.objects) {
|
|
455
|
+
|
|
456
|
+
if (objectNameArray.some( name => name === object)) {
|
|
457
|
+
return {
|
|
458
|
+
ok: false,
|
|
459
|
+
errorType: "deviceListBACnetConfiguration",
|
|
460
|
+
message: `Name already used: Object ${object} of device ${device}`,
|
|
461
|
+
device,
|
|
462
|
+
object,
|
|
463
|
+
value: object
|
|
464
|
+
};
|
|
465
|
+
}else{
|
|
466
|
+
objectNameArray.push(object);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
//#endregion
|
|
471
|
+
|
|
472
|
+
//#region ///// device objects InstanceNum Check /////
|
|
473
|
+
for (let device in deviceList) {
|
|
474
|
+
|
|
475
|
+
const dev = deviceList[device];
|
|
476
|
+
let objectInstanceArrayAV = [], objectInstanceArrayBV = [];
|
|
477
|
+
|
|
478
|
+
for (let object in dev.bacnet.objects) {
|
|
479
|
+
const obj = dev.bacnet.objects[object];
|
|
480
|
+
|
|
481
|
+
if ((obj.objectType === "analogValue" && objectInstanceArrayAV.some( instanceNum => instanceNum === obj.instanceNum))||(obj.objectType === "binaryValue" && objectInstanceArrayBV.some( instanceNum => instanceNum === obj.instanceNum))) {
|
|
482
|
+
return {
|
|
483
|
+
ok: false,
|
|
484
|
+
errorType: "deviceListBACnetConfiguration",
|
|
485
|
+
message: `instanceNum already used: Object ${object} of device ${device}`,
|
|
486
|
+
device,
|
|
487
|
+
object,
|
|
488
|
+
property: "instanceNum",
|
|
489
|
+
value: dev.bacnet.instanceRangeBV
|
|
490
|
+
};
|
|
491
|
+
}else if (obj.objectType === "binaryValue") {
|
|
492
|
+
objectInstanceArrayBV.push(obj.instanceNum);
|
|
493
|
+
}else if (obj.objectType === "analogValue") {
|
|
494
|
+
objectInstanceArrayAV.push(obj.instanceNum);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
//#endregion
|
|
500
|
+
|
|
501
|
+
//#region ///// device InstanceRangeAV Check /////
|
|
502
|
+
for (let device in deviceList) {
|
|
503
|
+
|
|
504
|
+
const dev = deviceList[device];
|
|
505
|
+
let instanceRangeAV = 0;
|
|
506
|
+
|
|
507
|
+
for (let object in dev.bacnet.objects) {
|
|
508
|
+
const obj = dev.bacnet.objects[object];
|
|
509
|
+
|
|
510
|
+
if (obj.objectType === "analogValue") {
|
|
511
|
+
instanceRangeAV++;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (dev.bacnet.instanceRangeAV < instanceRangeAV) {
|
|
516
|
+
return {
|
|
517
|
+
ok: false,
|
|
518
|
+
errorType: "deviceListBACnetConfiguration",
|
|
519
|
+
message: `InstanceRangeAV too small for device ${device}`,
|
|
520
|
+
device,
|
|
521
|
+
property: "instanceRangeAV",
|
|
522
|
+
value: dev.bacnet.instanceRangeAV
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
//#endregion
|
|
527
|
+
|
|
528
|
+
//#region ///// device InstanceRangeBV Check /////
|
|
529
|
+
for (let device in deviceList) {
|
|
530
|
+
|
|
531
|
+
const dev = deviceList[device];
|
|
532
|
+
let instanceRangeBV = 0;
|
|
533
|
+
|
|
534
|
+
for (let object in dev.bacnet.objects) {
|
|
535
|
+
const obj = dev.bacnet.objects[object];
|
|
536
|
+
|
|
537
|
+
if (obj.objectType === "binaryValue") {
|
|
538
|
+
instanceRangeBV++;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (dev.bacnet.instanceRangeBV < instanceRangeBV) {
|
|
542
|
+
return {
|
|
543
|
+
ok: false,
|
|
544
|
+
errorType: "deviceListBACnetConfiguration",
|
|
545
|
+
message: `InstanceRangeBV too small for device ${device}`,
|
|
546
|
+
device,
|
|
547
|
+
property: "instanceRangeBV",
|
|
548
|
+
value: dev.bacnet.instanceRangeBV
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
//#endregion
|
|
553
|
+
|
|
554
|
+
//#region ///// device Name Check /////
|
|
555
|
+
for (let device in deviceList) {
|
|
556
|
+
|
|
557
|
+
const dev = deviceList[device];
|
|
558
|
+
|
|
559
|
+
if (deviceNameArray.some( name => name === device)) {
|
|
560
|
+
return {
|
|
561
|
+
ok: false,
|
|
562
|
+
message: `name already used: Device ${device}`,
|
|
563
|
+
device,
|
|
564
|
+
value: device
|
|
565
|
+
};
|
|
566
|
+
}else{
|
|
567
|
+
deviceNameArray.push(device);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
//#endregion
|
|
571
|
+
|
|
572
|
+
//#region ///// AV overlap Check /////
|
|
573
|
+
let objectInstanceArrayAV = []
|
|
574
|
+
for (let device in deviceList) {
|
|
575
|
+
|
|
576
|
+
const dev = deviceList[device];
|
|
577
|
+
|
|
578
|
+
objectInstanceArrayAV.push({ device, offset: dev.bacnet.offsetAV, instanceRange: dev.bacnet.instanceRangeAV, maxdevNum: dev.identity.maxDevNum });
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
objectInstanceArrayAV.sort((a, b) => a.offset - b.offset);
|
|
582
|
+
|
|
583
|
+
for (let i = 0; i < objectInstanceArrayAV.length - 1; i++) {
|
|
584
|
+
const current = objectInstanceArrayAV[i];
|
|
585
|
+
const next = objectInstanceArrayAV[i + 1];
|
|
586
|
+
if (current.offset + current.instanceRange * current.maxdevNum > next.offset) {
|
|
587
|
+
return {
|
|
588
|
+
ok: false,
|
|
589
|
+
errorType: "deviceListOverlap",
|
|
590
|
+
message: `Analog BACnet objects overlap for device ${current.device} and device ${next.device}`,
|
|
591
|
+
device1: current.device,
|
|
592
|
+
device2: next.device
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
for (let inst of objectInstanceArrayAVManual) {
|
|
597
|
+
if (inst >= current.offset && inst < current.offset + current.instanceRange * current.maxdevNum) {
|
|
598
|
+
return {
|
|
599
|
+
ok: false,
|
|
600
|
+
errorType: "deviceListOverlapManual",
|
|
601
|
+
message: `Manual analog object instance (${inst}) overlaps another device's range`,
|
|
602
|
+
device: current.device,
|
|
603
|
+
instanceNum: inst
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
//#endregion
|
|
609
|
+
|
|
610
|
+
//#region ///// BV overlap Check /////
|
|
611
|
+
let objectInstanceArrayBV = [];
|
|
612
|
+
for (let device in deviceList) {
|
|
613
|
+
|
|
614
|
+
const dev = deviceList[device];
|
|
615
|
+
|
|
616
|
+
objectInstanceArrayBV.push({ device, offset: dev.bacnet.offsetBV, instanceRange: dev.bacnet.instanceRangeBV, maxdevNum: dev.identity.maxDevNum });
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
objectInstanceArrayBV.sort((a, b) => a.offset - b.offset);
|
|
620
|
+
|
|
621
|
+
for (let i = 0; i < objectInstanceArrayBV.length - 1; i++) {
|
|
622
|
+
const current = objectInstanceArrayBV[i];
|
|
623
|
+
const next = objectInstanceArrayBV[i + 1];
|
|
624
|
+
if (current.offset + current.instanceRange * current.maxdevNum > next.offset) {
|
|
625
|
+
return {
|
|
626
|
+
ok: false,
|
|
627
|
+
errorType: "deviceListOverlap",
|
|
628
|
+
message: `Binary BACnet objects overlap for device ${current.device} and device ${next.device}`,
|
|
629
|
+
device1: current.device,
|
|
630
|
+
device2: next.device
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
for (let inst of objectInstanceArrayBVManual) {
|
|
635
|
+
if (inst >= current.offset && inst < current.offset + current.instanceRange * current.maxdevNum) {
|
|
636
|
+
return {
|
|
637
|
+
ok: false,
|
|
638
|
+
errorType: "deviceListOverlapManual",
|
|
639
|
+
message: "Manual binary object instance overlaps another device's range",
|
|
640
|
+
device: current.device,
|
|
641
|
+
instanceNum: inst
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
//#endregion
|
|
647
|
+
return { ok: true };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
RED.nodes.registerType("LoRaBAC", LoRaBAC);
|
|
652
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-contrib-lorawan-bacnet-server",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Custom Node-RED nodes to interface LoRaWAN devices with BACnet protocol. ",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"LoRaWAN",
|
|
7
|
+
"LoRaBAC",
|
|
8
|
+
"LoRa",
|
|
9
|
+
"node-red",
|
|
10
|
+
"BACnet",
|
|
11
|
+
"BACnet Server",
|
|
12
|
+
"IoT"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"contributors": [
|
|
16
|
+
{
|
|
17
|
+
"name": "Elias CUZEAU",
|
|
18
|
+
"email": "elias.cuzeau@ikmail.com"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=16.0.0"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"node-bacnet": "^0.2.4"
|
|
26
|
+
},
|
|
27
|
+
"node-red": {
|
|
28
|
+
"version": ">=3.0.0",
|
|
29
|
+
"nodes": {
|
|
30
|
+
"lorabac": "nodes/lorabac/lorabac.js",
|
|
31
|
+
"bacnet-server": "nodes/bacnet-server/bacnet-server.js",
|
|
32
|
+
"bacnet-point": "nodes/bacnet-point/bacnet-point.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/SylvainMontagny/node-red-contrib-lorawan-bacnet.git"
|
|
38
|
+
}
|
|
39
|
+
}
|