@bitpoolos/edge-bacnet 1.3.0 → 1.3.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.1] - 06-06-2024
4
+
5
+ ### Summary
6
+
7
+ Primarily bug fixes and performance improvements
8
+
9
+ ### Bug Fixes
10
+
11
+ - Adding individual points for MSTP devices would add incorrect device name to read list
12
+
13
+ - Adding individual points for IP devices would add all MSTP network folders to read list
14
+
15
+ - Intermittent incorrect device naming issues
16
+
17
+ - Fixed BACnet server incompatibility with YABE and other BACnet browsers.
18
+
19
+ - Added undefined check to bacstack client
20
+
21
+ ### Improvements
22
+
23
+ - First poll cycle only queries Object Name and Present Value properties for all applicable Object Types for smaller initial network load. Objects are then back populated with the remaining Object properties on the subsequent poll cycles.
24
+
25
+ - Devices are immediately added to the UI tree with a device placeholder on a whoIs/iAm response. The devices are then back populated with Names and BACnet Objects. This gives the user a fast understanding of the size and relationships of the BACnet network.
26
+
27
+ - Added docstrings to bacnet_server.js and code clean up.
28
+
29
+ - Set Server enable to default on True
30
+
31
+
3
32
  ## [1.3.0] - 16-05-2024
4
33
 
5
34
  ### Summary
package/README.md CHANGED
@@ -45,6 +45,7 @@ Upon updating to the latest version, we highly recommend:
45
45
  - Deploy all flows
46
46
  - Restart Node-RED
47
47
  - Insert and reconfigure new @bitpoolos/edge-bacnet nodes.
48
+ - Restart Node-RED again if no devices are discovered.
48
49
  ```
49
50
  Main reason being, the behaviour of the bacnet client binding to network interfaces can remain stagnent if the Node-RED service is not restarted. This also ensures that all of the nodes are correctly configured as there are often properties added and removed from nodes.
50
51
 
@@ -61,6 +62,8 @@ Main reason being, the behaviour of the bacnet client binding to network interfa
61
62
 
62
63
  - If you are using this node in a linux environment, using the 'All interfaces : 0.0.0.0' can be more reliable with a greater range of BACnet devices.
63
64
 
65
+ - Note your broadcast address, compatibility can vary from 255.255.255.255 (all subnets) and 192.x.x.255 (locked down to your current subnet).
66
+
64
67
  ## Resources
65
68
  - [bitpool.com](https://www.bitpool.com/) - who are we.
66
69
  - [wiki.bitpool.com](https://wiki.bitpool.com/) - helpful docs.
package/bacnet_client.js CHANGED
@@ -124,14 +124,15 @@ class BacnetClient extends EventEmitter {
124
124
  that.addToParentMstpNetwork(newBacnetDevice);
125
125
  }
126
126
  that.deviceList.push(newBacnetDevice);
127
+ that.addToNetworkTree(newBacnetDevice);
127
128
  } else if (foundIndex !== -1) {
128
129
  that.deviceList[foundIndex].updateDeviceConfig(device);
129
130
  that.deviceList[foundIndex].setLastSeen(Date.now());
130
131
  if (that.deviceList[foundIndex].getIsMstpDevice()) {
131
132
  that.addToParentMstpNetwork(that.deviceList[foundIndex]);
132
133
  }
134
+ that.addToNetworkTree(that.deviceList[foundIndex]);
133
135
  }
134
-
135
136
  //emit event for node-red to log
136
137
  that.emit("deviceFound", device);
137
138
  }
@@ -145,12 +146,14 @@ class BacnetClient extends EventEmitter {
145
146
  that.addToParentMstpNetwork(newBacnetDevice);
146
147
  }
147
148
  that.deviceList.push(newBacnetDevice);
149
+ that.addToNetworkTree(newBacnetDevice);
148
150
  } else if (foundIndex !== -1) {
149
151
  that.deviceList[foundIndex].updateDeviceConfig(device);
150
152
  that.deviceList[foundIndex].setLastSeen(Date.now());
151
153
  if (that.deviceList[foundIndex].getIsMstpDevice()) {
152
154
  that.addToParentMstpNetwork(that.deviceList[foundIndex]);
153
155
  }
156
+ that.addToNetworkTree(that.deviceList[foundIndex]);
154
157
  }
155
158
 
156
159
  //emit event for node-red to log
@@ -177,27 +180,68 @@ class BacnetClient extends EventEmitter {
177
180
 
178
181
  testFunction(address, type, instance, property) {
179
182
  let that = this;
180
-
181
183
  console.log("test function ");
182
-
183
184
  that.client.readProperty(
184
185
  address,
185
186
  { type: type, instance: instance },
186
187
  property,
187
188
  that.readPropertyMultipleOptions,
188
189
  (err, value) => {
189
- if (err) {
190
- console.log("err: ", err);
191
- }
192
-
190
+ console.log(value);
193
191
  if (value) {
194
- console.log("value : ", value);
195
- console.log("value1 : ", value.values[0].value);
192
+ // If the result has value, resolve the promise
193
+ console.log(value.values[0]);
194
+ value.values[0].values.forEach(function (value) {
195
+ console.log("value: ", value.value);
196
+ });
197
+ } else {
198
+ console.log(err);
196
199
  }
197
200
  }
198
201
  );
199
202
  }
200
203
 
204
+ addToNetworkTree(device) {
205
+ let that = this;
206
+ try {
207
+ const deviceKey = that.createDeviceKey(device);
208
+ let deviceName = device.getDeviceName();
209
+ if (deviceName !== null) {
210
+ const deviceId = device.getDeviceId();
211
+ if (deviceId !== null) {
212
+ let lastIndex = deviceName.lastIndexOf(deviceId);
213
+ if (lastIndex) {
214
+ let formattedName = deviceName.substring(0, lastIndex);
215
+ formattedName = `${formattedName.trim()}_Device_${deviceId}`;
216
+ if (that.networkTree[deviceKey][formattedName] &&
217
+ Object.keys(that.networkTree[deviceKey][formattedName]).length > 0) {
218
+ delete that.networkTree[deviceKey]["device"];
219
+ }
220
+ }
221
+ }
222
+ } else {
223
+ const json = {
224
+ "objectId": {
225
+ "type": 8,
226
+ "instance": device.getDeviceId()
227
+ }
228
+ };
229
+
230
+ if (that.networkTree[deviceKey] && that.networkTree[deviceKey]["device"]) {
231
+ that.networkTree[deviceKey]["device"]["meta"] = json;
232
+ } else {
233
+ that.networkTree[deviceKey] = {
234
+ "device": {
235
+ "meta": json
236
+ }
237
+ }
238
+ }
239
+ }
240
+ } catch (e) {
241
+ that.logOut("addToNetworkTree error: ", e);
242
+ }
243
+ }
244
+
201
245
  getProtocolSupported(device) {
202
246
  //return protocols support for device
203
247
  let that = this;
@@ -283,8 +327,26 @@ class BacnetClient extends EventEmitter {
283
327
  let device = that.deviceList.find((ele) => ele.getDeviceId() == deviceObject.deviceId);
284
328
  that.updateDeviceName(device);
285
329
 
330
+
331
+ //test
332
+
333
+ that.getProtocolSupported(device).then(function (result) {
334
+ console.log("updatePointsForDevice getProtocolSupported ", result.values[0].originalBitString);
335
+ console.log(result.values[0]);
336
+ console.log(result);
337
+ console.log(result.values[0]);
338
+ let decodedValues = decodeBitArray(8, result.values[0].originalBitString.value);
339
+ device.setProtocolServicesSupported(decodedValues);
340
+ }).catch(function (error) {
341
+ that.logOut("getProtocolSupported error: ", error);
342
+ });
343
+
344
+ //test
345
+
346
+
286
347
  if (device.getIsProtocolServicesSet() == false) {
287
348
  that.getProtocolSupported(device).then(function (result) {
349
+ console.log("updatePointsForDevice getProtocolSupported ", result.values[0].originalBitString);
288
350
  let decodedValues = decodeBitArray(8, result.values[0].originalBitString.value);
289
351
  device.setProtocolServicesSupported(decodedValues);
290
352
  }).catch(function (error) {
@@ -302,11 +364,11 @@ class BacnetClient extends EventEmitter {
302
364
  resolve(true);
303
365
  })
304
366
  .catch(function (e) {
305
- that.logOut(`Update points list error 1: ${device.getAddress()}`, e);
367
+ that.logOut(`Update points list error 1: ${that.getDeviceAddress(device)}`, e);
306
368
  });
307
369
  })
308
370
  .catch(function (e) {
309
- that.logOut(`Update points list error 2: ${device.getAddress()}`, e);
371
+ that.logOut(`Update points list error 2: ${that.getDeviceAddress(device)}`, e);
310
372
  device.setManualDiscoveryMode(true);
311
373
  that
312
374
  .getDevicePointListWithoutObjectList(device)
@@ -318,11 +380,11 @@ class BacnetClient extends EventEmitter {
318
380
  resolve(true);
319
381
  })
320
382
  .catch(function (e) {
321
- that.logOut(`Update points list error 3: ${device.getAddress()}`, e);
383
+ that.logOut(`Update points list error 3: ${that.getDeviceAddress(device)}`, e);
322
384
  });
323
385
  })
324
386
  .catch(function (e) {
325
- that.logOut(`Update points list error 4: ${device.getAddress()}`, e);
387
+ that.logOut(`Update points list error 4: ${that.getDeviceAddress(device)}`, e);
326
388
  });
327
389
  });
328
390
  } catch (e) {
@@ -416,9 +478,9 @@ class BacnetClient extends EventEmitter {
416
478
  that.logOut("getProtocolSupported error: ", error);
417
479
  });
418
480
  }
481
+ try {
419
482
 
420
- if (!device.getManualDiscoveryMode()) {
421
- try {
483
+ if (device.getSegmentation() !== 3) {
422
484
  that.updateDeviceName(device);
423
485
  that
424
486
  .getDevicePointList(device)
@@ -435,7 +497,6 @@ class BacnetClient extends EventEmitter {
435
497
  })
436
498
  .catch(function (e) {
437
499
  that.logOut(`getDevicePointList error: ${device.getAddress()}`, e);
438
- device.setManualDiscoveryMode(true);
439
500
  that
440
501
  .getDevicePointListWithoutObjectList(device)
441
502
  .then(function () {
@@ -453,11 +514,29 @@ class BacnetClient extends EventEmitter {
453
514
  query(index);
454
515
  });
455
516
  });
456
- } catch (e) {
457
- that.logOut("Error while querying devices: ", e);
458
- query(index);
517
+
518
+ } else if (device.getSegmentation() == 3) {
519
+
520
+ that.updateDeviceName(device);
521
+ that
522
+ .getDevicePointListWithoutObjectList(device)
523
+ .then(function () {
524
+ that
525
+ .buildJsonObject(device)
526
+ .then(function () {
527
+ query(index);
528
+ })
529
+ .catch(function (e) {
530
+ that.logOut(`getDevicePointList error: ${device.getAddress()}`, e);
531
+ query(index);
532
+ });
533
+ })
534
+ .catch(function (e) {
535
+ query(index);
536
+ });
459
537
  }
460
- } else {
538
+ } catch (e) {
539
+ that.logOut("Error while querying devices: ", e);
461
540
  query(index);
462
541
  }
463
542
  } else {
@@ -1061,6 +1140,83 @@ class BacnetClient extends EventEmitter {
1061
1140
  }
1062
1141
 
1063
1142
 
1143
+ _readObjectLite(device, deviceAddress, type, instance) {
1144
+ const that = this;
1145
+ const readOptions = {
1146
+ maxSegments: that.readPropertyMultipleOptions.maxSegments,
1147
+ maxApdu: that.readPropertyMultipleOptions.maxApdu,
1148
+ };
1149
+
1150
+ // Define all properties to be read
1151
+ const allProperties = [
1152
+ { id: baEnum.PropertyIdentifier.PRESENT_VALUE },
1153
+ { id: baEnum.PropertyIdentifier.OBJECT_NAME },
1154
+ ];
1155
+
1156
+ return new Promise((resolve, reject) => {
1157
+ // Try to read all properties at once
1158
+ that._readObject(deviceAddress, type, instance, allProperties, readOptions)
1159
+ .then(result => {
1160
+ if (result.value) {
1161
+ // If the result has value, resolve the promise
1162
+ resolve(result);
1163
+ } else {
1164
+ // If not, proceed to read individual properties
1165
+ readPropertiesIndividually();
1166
+ }
1167
+ })
1168
+ .catch(() => {
1169
+ // On error, proceed to read individual properties
1170
+ readPropertiesIndividually();
1171
+ });
1172
+
1173
+ // Function to read properties individually
1174
+ const readPropertiesIndividually = () => {
1175
+ const promises = allProperties.map((property, index) => new Promise((propertyResolve) => {
1176
+ that.client.readProperty(
1177
+ deviceAddress,
1178
+ { type: type, instance: instance },
1179
+ property.id,
1180
+ readOptions,
1181
+ (err, value) => {
1182
+ if (err) {
1183
+ propertyResolve(null);
1184
+ } else {
1185
+ propertyResolve({
1186
+ id: property.id,
1187
+ index: value.property.index,
1188
+ value: value.values,
1189
+ });
1190
+ }
1191
+ }
1192
+ );
1193
+ }));
1194
+
1195
+ Promise.all(promises)
1196
+ .then(resultArray => {
1197
+ // Filter out null results
1198
+ const validResults = resultArray.filter(result => result !== null);
1199
+
1200
+ resolve({
1201
+ error: null,
1202
+ value: {
1203
+ values: [
1204
+ {
1205
+ objectId: {
1206
+ type: type,
1207
+ instance: instance,
1208
+ },
1209
+ values: validResults,
1210
+ },
1211
+ ],
1212
+ },
1213
+ });
1214
+ })
1215
+ .catch(reject);
1216
+ };
1217
+ });
1218
+ }
1219
+
1064
1220
  _readObjectPropList(deviceAddress, type, instance) {
1065
1221
  return this._readObject(deviceAddress, type, instance, [{ id: baEnum.PropertyIdentifier.PROPERTY_LIST }]);
1066
1222
  }
@@ -1394,49 +1550,100 @@ class BacnetClient extends EventEmitter {
1394
1550
  if (typeof pointList !== "undefined" && pointList.length > 0) {
1395
1551
  pointList.forEach(function (point, pointListIndex) {
1396
1552
  requestMutex.acquire().then(function (release) {
1397
- that
1398
- ._readObjectFull(device, address, point.value.type, point.value.instance)
1399
- .then(function (result) {
1400
- if (!result.error) {
1401
- if (result.length > 0 && Array.isArray(result)) {
1402
- promiseArray = result;
1403
- } else {
1404
- promiseArray.push(result);
1553
+ if (device.getIsInitialQuery()) {
1554
+ that
1555
+ ._readObjectLite(device, address, point.value.type, point.value.instance)
1556
+ .then(function (result) {
1557
+ if (!result.error) {
1558
+ if (result.length > 0 && Array.isArray(result)) {
1559
+ promiseArray = result;
1560
+ } else {
1561
+ promiseArray.push(result);
1562
+ }
1405
1563
  }
1406
- }
1407
1564
 
1408
- release();
1565
+ release();
1566
+
1567
+ if (pointListIndex == pointList.length - 1) {
1568
+ device.setIsInitialQuery(false);
1569
+ that
1570
+ .buildResponse(promiseArray, device)
1571
+ .then(function () {
1572
+ that.lastNetworkPoll = Date.now();
1573
+ resolve({ deviceList: that.deviceList, pointList: that.networkTree });
1574
+ })
1575
+ .catch(function (e) {
1576
+ that.logOut("Error while building json object: ", e);
1577
+ reject(e);
1578
+ });
1579
+ }
1580
+ })
1581
+ .catch(function (e) {
1582
+ release();
1583
+ that.logOut("_readObjectLite error: ", e);
1584
+
1585
+ if (pointListIndex == pointList.length - 1) {
1586
+ device.setIsInitialQuery(false);
1587
+ that
1588
+ .buildResponse(promiseArray, device)
1589
+ .then(function () {
1590
+ that.lastNetworkPoll = Date.now();
1591
+ resolve({ deviceList: that.deviceList, pointList: that.networkTree });
1592
+ })
1593
+ .catch(function (e) {
1594
+ that.logOut("Error while building json object: ", e);
1595
+ reject(e);
1596
+ });
1597
+ }
1598
+ });
1409
1599
 
1410
- if (pointListIndex == pointList.length - 1) {
1411
- that
1412
- .buildResponse(promiseArray, device)
1413
- .then(function () {
1414
- that.lastNetworkPoll = Date.now();
1415
- resolve({ deviceList: that.deviceList, pointList: that.networkTree });
1416
- })
1417
- .catch(function (e) {
1418
- that.logOut("Error while building json object: ", e);
1419
- reject(e);
1420
- });
1421
- }
1422
- })
1423
- .catch(function (e) {
1424
- release();
1425
- that.logOut("_readObjectFull error: ", e);
1426
1600
 
1427
- if (pointListIndex == pointList.length - 1) {
1428
- that
1429
- .buildResponse(promiseArray, device)
1430
- .then(function () {
1431
- that.lastNetworkPoll = Date.now();
1432
- resolve({ deviceList: that.deviceList, pointList: that.networkTree });
1433
- })
1434
- .catch(function (e) {
1435
- that.logOut("Error while building json object: ", e);
1436
- reject(e);
1437
- });
1438
- }
1439
- });
1601
+
1602
+ } else {
1603
+ that
1604
+ ._readObjectFull(device, address, point.value.type, point.value.instance)
1605
+ .then(function (result) {
1606
+ if (!result.error) {
1607
+ if (result.length > 0 && Array.isArray(result)) {
1608
+ promiseArray = result;
1609
+ } else {
1610
+ promiseArray.push(result);
1611
+ }
1612
+ }
1613
+
1614
+ release();
1615
+
1616
+ if (pointListIndex == pointList.length - 1) {
1617
+ that
1618
+ .buildResponse(promiseArray, device)
1619
+ .then(function () {
1620
+ that.lastNetworkPoll = Date.now();
1621
+ resolve({ deviceList: that.deviceList, pointList: that.networkTree });
1622
+ })
1623
+ .catch(function (e) {
1624
+ that.logOut("Error while building json object: ", e);
1625
+ reject(e);
1626
+ });
1627
+ }
1628
+ })
1629
+ .catch(function (e) {
1630
+ release();
1631
+ that.logOut("_readObjectFull error: ", e);
1632
+
1633
+ if (pointListIndex == pointList.length - 1) {
1634
+ that
1635
+ .buildResponse(promiseArray, device)
1636
+ .then(function () {
1637
+ that.lastNetworkPoll = Date.now();
1638
+ resolve({ deviceList: that.deviceList, pointList: that.networkTree });
1639
+ })
1640
+ .catch(function (e) {
1641
+ that.logOut("Error while building json object: ", e);
1642
+ reject(e);
1643
+ });
1644
+ }
1645
+ });
1646
+ }
1440
1647
  });
1441
1648
  });
1442
1649
  } else {
@@ -1467,7 +1674,8 @@ class BacnetClient extends EventEmitter {
1467
1674
  let currobjectId = pointProperty.objectId.type;
1468
1675
  let bac_obj = that.getObjectType(currobjectId);
1469
1676
  let objectName = that._findValueById(pointProperty.values, baEnum.PropertyIdentifier.OBJECT_NAME);
1470
- let objectType = that._findValueById(pointProperty.values, baEnum.PropertyIdentifier.OBJECT_TYPE);
1677
+ let objectType = pointProperty.objectId.type;
1678
+
1471
1679
  let objectId;
1472
1680
  if (objectName !== null && typeof objectName == "string") {
1473
1681
  objectName = objectName.replace(reg, '');
@@ -1504,7 +1712,7 @@ class BacnetClient extends EventEmitter {
1504
1712
  values[objectId].presentValue = values[objectId].stateTextArray[object.value[0].value - 1].value;
1505
1713
  }
1506
1714
  }
1507
- } else {
1715
+ } else if (objectType !== 8) {
1508
1716
  values[objectId].presentValue = roundDecimalPlaces(object.value[0].value, 2);
1509
1717
  }
1510
1718
  }
package/bacnet_device.js CHANGED
@@ -34,6 +34,7 @@ class BacnetDevice {
34
34
  that.displayName = config.displayName;
35
35
  that.protocolServicesSupported = config.protocolServicesSupported;
36
36
  that.isProtocolServicesSet = config.isProtocolServicesSet;
37
+ that.isInitialQuery = config.isInitialQuery;
37
38
 
38
39
  } else if (fromImport == false) {
39
40
  if (config.net && config.adr) {
@@ -62,6 +63,7 @@ class BacnetDevice {
62
63
  that.protocolServicesSupported = [];
63
64
  that.protocolServicesSupported = [];
64
65
  that.isProtocolServicesSet = false;
66
+ that.isInitialQuery = true;
65
67
  }
66
68
  }
67
69
 
@@ -354,6 +356,14 @@ class BacnetDevice {
354
356
  }
355
357
  }
356
358
 
359
+ getIsInitialQuery() {
360
+ return this.isInitialQuery;
361
+ }
362
+
363
+ setIsInitialQuery(bool) {
364
+ this.isInitialQuery = bool;
365
+ }
366
+
357
367
  }
358
368
 
359
369
  module.exports = { BacnetDevice };
@@ -156,7 +156,7 @@
156
156
  discover_polling_schedule_options: { value: "Minutes", required: true },
157
157
  deviceId: { value: 817001, required: true },
158
158
  logErrorToConsole: { value: false },
159
- serverEnabled: { value: false },
159
+ serverEnabled: { value: true },
160
160
  device_read_schedule: { value: "900" },
161
161
  device_read_schedule_value: { value: "15", required: true },
162
162
  device_read_schedule_options: { value: "Minutes", required: true },
package/bacnet_read.html CHANGED
@@ -218,8 +218,9 @@
218
218
  addPointClicked(slotProps) {
219
219
  let app = this;
220
220
  //update UI
221
- let parentDeviceName = slotProps.node.parentDevice;
222
- let device = this.deviceList.find((ele) => ele.deviceName == parentDeviceName);
221
+ const parentDeviceName = slotProps.node.parentDevice;
222
+ const slotDeviceId = slotProps.node.parentDeviceId;
223
+ let device = this.deviceList.find((ele) => ele.deviceId == slotDeviceId);
223
224
  let foundDeviceIndex = this.readDevices
224
225
  ? this.readDevices.findIndex((ele) => ele.deviceId == device.deviceId)
225
226
  : -1;
@@ -228,11 +229,24 @@
228
229
  let parentDevice = this.devices.find((ele) => ele.ipAddr == deviceAddress);
229
230
  let childDevice;
230
231
  if (device.isMstp) {
231
- let foundChildIndex = parentDevice.children[1].children.findIndex(
232
- (ele) => ele.initialName == parentDeviceName
233
- );
234
- if (foundChildIndex !== -1) {
235
- childDevice = parentDevice.children[1].children[foundChildIndex];
232
+ let indexMap = {
233
+ deviceIndex: -1,
234
+ mstpNetorkIndex: -1
235
+ };
236
+ parentDevice.children.forEach(function (child, index) {
237
+ if (child.label.includes("MSTP")) {
238
+ const tempIndex = child.children.findIndex(
239
+ (ele) => ele.deviceId == slotDeviceId
240
+ );
241
+ if (tempIndex !== -1) {
242
+ indexMap.deviceIndex = tempIndex;
243
+ indexMap.mstpNetorkIndex = index;
244
+ }
245
+ }
246
+ });
247
+
248
+ if (indexMap.deviceIndex !== -1 && indexMap.mstpNetorkIndex !== -1) {
249
+ childDevice = parentDevice.children[indexMap.mstpNetorkIndex].children[indexMap.deviceIndex];
236
250
  }
237
251
  }
238
252
 
@@ -246,6 +260,13 @@
246
260
  }
247
261
  newReadParent.children[0].children = [];
248
262
  newReadParent.children[0].children.push(slotProps.node);
263
+ while (newReadParent.children.length > 1) {
264
+ newReadParent.children.forEach(function (child, index) {
265
+ if (child.label.includes("MSTP")) {
266
+ newReadParent.children.splice(index, 1);
267
+ }
268
+ });
269
+ }
249
270
  if (this.readDevices) {
250
271
  this.readDevices.push(newReadParent);
251
272
  } else {
package/bacnet_server.js CHANGED
@@ -3,6 +3,19 @@ const pjson = require('./package.json');
3
3
  const baEnum = bacnet.enum;
4
4
  const { Store_Config_Server, Read_Config_Sync_Server } = require('./common');
5
5
 
6
+ /**
7
+ * Class representing a BACnet Server.
8
+ *
9
+ * This class initializes a BACnet server with specified client, device ID, and Node-Red version.
10
+ * It provides methods to set device name, add objects, retrieve objects, clear server points, clear server point, and get server points.
11
+ *
12
+ * Simulates a BACnet IP device on a regular IP network
13
+ *
14
+ * @constructor
15
+ * @param {Object} client - The BACnet client object.
16
+ * @param {number} deviceId - The ID of the device.
17
+ * @param {string} nodeRedVersion - The version of Node-Red.
18
+ */
6
19
  class BacnetServer {
7
20
 
8
21
  constructor(client, deviceId, nodeRedVersion) {
@@ -14,13 +27,13 @@ class BacnetServer {
14
27
  that.objectIdNumber[baEnum.ObjectType.ANALOG_VALUE] = 0;
15
28
  that.objectIdNumber[baEnum.ObjectType.CHARACTERSTRING_VALUE] = 0;
16
29
  that.objectIdNumber[baEnum.ObjectType.BINARY_VALUE] = 0;
17
-
18
30
  that.nodeRedVersion = nodeRedVersion;
19
31
  that.deviceId = deviceId;
20
32
  that.vendorId = 1401;
21
33
  that.objectList = [
22
34
  { value: { type: baEnum.ObjectType.DEVICE, instance: that.deviceId }, type: 12 }
23
35
  ];
36
+
24
37
  that.objectStore = {
25
38
  [baEnum.ObjectType.DEVICE]: {
26
39
  [baEnum.PropertyIdentifier.OBJECT_IDENTIFIER]: [{ value: { type: baEnum.ObjectType.DEVICE, instance: that.deviceId }, type: 12 }],
@@ -36,8 +49,7 @@ class BacnetServer {
36
49
  [baEnum.PropertyIdentifier.PROTOCOL_REVISION]: [{ value: 19, type: 2 }],
37
50
  [baEnum.PropertyIdentifier.PROTOCOL_VERSION]: [{ value: 0, type: 2 }],
38
51
  [baEnum.PropertyIdentifier.APPLICATION_SOFTWARE_VERSION]: [{ value: pjson.version, type: 7 }],
39
- [baEnum.PropertyIdentifier.PROTOCOL_SERVICES_SUPPORTED]: [{ value: { value: [0, 80, 0, 4, 4], bitsUsed: 40 }, type: 8 }],
40
- [baEnum.PropertyIdentifier.PROTOCOL_OBJECT_TYPES_SUPPORTED]: [{ value: { value: [0, 80, 0, 4, 4], bitsUsed: 40 }, type: 8 }],
52
+ [baEnum.PropertyIdentifier.PROTOCOL_SERVICES_SUPPORTED]: [{ value: { value: [0, 10, 0, 32, 32], bitsUsed: 40 }, type: 8 }],
41
53
  [baEnum.PropertyIdentifier.MAX_APDU_LENGTH_ACCEPTED]: [{ value: 1476, type: 2 }],
42
54
  [baEnum.PropertyIdentifier.SEGMENTATION_SUPPORTED]: [{ value: 0, type: 9 }],
43
55
  [baEnum.PropertyIdentifier.APDU_TIMEOUT]: [{ value: that.bacnetClient.config.apduTimeout, type: 2 }],
@@ -76,8 +88,10 @@ class BacnetServer {
76
88
  try {
77
89
  let cachedData = JSON.parse(Read_Config_Sync_Server());
78
90
  if (typeof cachedData == "object") {
79
-
80
- if (cachedData.objectList) that.objectList = cachedData.objectList;
91
+ if (cachedData.objectList) {
92
+ that.objectList = cachedData.objectList;
93
+ that.objectStore[baEnum.ObjectType.DEVICE][baEnum.PropertyIdentifier.OBJECT_LIST] = that.objectList;
94
+ }
81
95
  if (cachedData.objectStore) {
82
96
  that.objectStore[baEnum.ObjectType.ANALOG_VALUE] = cachedData.objectStore[baEnum.ObjectType.ANALOG_VALUE];
83
97
  that.objectStore[baEnum.ObjectType.CHARACTERSTRING_VALUE] = cachedData.objectStore[baEnum.ObjectType.CHARACTERSTRING_VALUE];
@@ -94,24 +108,20 @@ class BacnetServer {
94
108
  });
95
109
 
96
110
  that.bacnetClient.client.on('readPropertyMultiple', (data) => {
97
-
98
111
  let senderAddress = data.address;
99
112
  let requestProps = data.request.properties;
100
113
  let responseObject = [];
101
114
 
102
115
  try {
103
116
  if (requestProps) {
104
-
105
117
  for (let i = 0; i < requestProps.length; i++) {
106
118
  let prop = requestProps[i].properties[0].id;
107
119
  let type = requestProps[i].objectId.type;
108
120
  let instance = requestProps[i].objectId.instance;
109
121
  let foundObject = that.getObjectMultiple(type, prop, instance, requestProps[i].properties);
110
-
111
122
  if (foundObject !== null && foundObject !== undefined && foundObject !== "undefined") {
112
123
  responseObject.push({ objectId: { type: type, instance: instance }, values: foundObject });
113
124
  }
114
-
115
125
  if (i == requestProps.length - 1) {
116
126
  if (responseObject.length > 0) {
117
127
  that.bacnetClient.client.readPropertyMultipleResponse(senderAddress, data.invokeId, responseObject);
@@ -127,7 +137,6 @@ class BacnetServer {
127
137
  }
128
138
  }
129
139
  }
130
-
131
140
  } catch (e) {
132
141
  that.bacnetClient.client.errorResponse(
133
142
  data.address,
@@ -137,24 +146,21 @@ class BacnetServer {
137
146
  baEnum.ErrorCode.UNKNOWN_PROPERTY
138
147
  );
139
148
  }
140
-
141
149
  });
142
150
 
143
151
  that.bacnetClient.client.on('readProperty', (data) => {
144
-
145
152
  try {
146
-
147
153
  let objectId = data.request.objectId.type;
148
154
  let objectInstance = data.request.objectId.instance;
149
155
  let propId = data.request.property.id.toString();
150
-
151
156
  let responseObj = that.getObject(objectId, propId, objectInstance);
152
-
153
- if (propId == baEnum.PropertyIdentifier.OBJECT_LIST && ((Date.now() - that.lastWhoIsRecived) / 1000) < 0.7) {
154
- responseObj = [{ value: that.objectList.length, type: 2 }];
157
+ if (propId == baEnum.PropertyIdentifier.OBJECT_LIST) {
158
+ if (data.request.property.index !== 0xFFFFFFFF) {
159
+ responseObj = responseObj[data.request.property.index];
160
+ }
155
161
  }
156
162
  if (responseObj !== null && responseObj !== undefined && typeof responseObj !== "undefined") {
157
- that.bacnetClient.client.readPropertyResponse(data.address, data.invokeId, objectId, data.request.property, responseObj);
163
+ that.bacnetClient.client.readPropertyResponse(data.address, data.invokeId, data.request.objectId, data.request.property, responseObj);
158
164
  } else {
159
165
  that.bacnetClient.client.errorResponse(
160
166
  data.address,
@@ -167,13 +173,17 @@ class BacnetServer {
167
173
  } catch (e) {
168
174
  console.log("Local BACnet device readProperty error: ", e);
169
175
  }
170
-
171
176
  });
172
177
 
173
178
  //do initial iAm broadcast when BACnet server starts
174
179
  that.bacnetClient.client.iAmResponse(that.deviceId, baEnum.Segmentation.SEGMENTED_BOTH, that.vendorId);
175
180
  }
176
181
 
182
+ /**
183
+ * Set the name of the device.
184
+ *
185
+ * @param {string} nodeName - The new name for the device.
186
+ */
177
187
  setDeviceName(nodeName) {
178
188
  let that = this;
179
189
  if (typeof nodeName == "string" && nodeName !== "") {
@@ -181,6 +191,13 @@ class BacnetServer {
181
191
  }
182
192
  }
183
193
 
194
+ /**
195
+ * Adds a new object to the BacnetServer's object store based on the provided name and value.
196
+ *
197
+ * @param {string} name - The name of the object to be added.
198
+ * @param {number|boolean|string} value - The value of the object to be added.
199
+ * @returns {void}
200
+ */
184
201
  addObject(name, value) {
185
202
  let that = this;
186
203
  let objectType = that.getBacnetObjectType(value);
@@ -290,11 +307,18 @@ class BacnetServer {
290
307
  Store_Config_Server(JSON.stringify({ objectList: that.objectList, objectStore: that.objectStore }));
291
308
  }
292
309
 
310
+ /**
311
+ * Retrieves a specific property of an object based on the object ID, property ID, and instance number.
312
+ *
313
+ * @param {number} objectId - The ID of the object type.
314
+ * @param {number} propId - The ID of the property to retrieve.
315
+ * @param {number} instance - The instance number of the object.
316
+ * @returns {any} The requested property value if found, otherwise null.
317
+ */
293
318
  getObject(objectId, propId, instance) {
294
319
  let that = this;
295
320
  let objectGroup = that.objectStore[objectId];
296
321
 
297
-
298
322
  if (Array.isArray(objectGroup)) {
299
323
  for (let i = 0; i < objectGroup.length; i++) {
300
324
  let object = objectGroup[i];
@@ -306,19 +330,26 @@ class BacnetServer {
306
330
  }
307
331
  }
308
332
  } else {
309
-
310
333
  return objectGroup[propId];
311
334
  }
312
335
 
313
336
  return null;
314
337
  }
315
338
 
339
+ /**
340
+ * Retrieves the properties of a specific object instance from the objectStore based on the provided parameters.
341
+ *
342
+ * @param {number} objectId - The type of the object to retrieve.
343
+ * @param {number} propId - The property identifier to retrieve.
344
+ * @param {number} instance - The instance number of the object to retrieve.
345
+ * @param {Array} properties - An array of additional properties to retrieve along with the main property.
346
+ * @returns {Array|null} - An array of properties with values for the specified object instance, or null if not found.
347
+ */
316
348
  getObjectMultiple(objectId, propId, instance, properties) {
317
349
  let that = this;
318
350
  let objectGroup = that.objectStore[objectId];
319
351
 
320
352
  try {
321
-
322
353
  if (Array.isArray(objectGroup)) {
323
354
  for (let i = 0; i < objectGroup.length; i++) {
324
355
  let object = objectGroup[i];
@@ -379,6 +410,12 @@ class BacnetServer {
379
410
  return null;
380
411
  }
381
412
 
413
+ /**
414
+ * Clear all server points by resetting the object lists and object store.
415
+ * This method resets the object list for the device, clears the character string values,
416
+ * and clears the analog values. It also resets the object id numbers for analog, character string, and binary values.
417
+ * Finally, it stores the updated object list and object store in the server configuration.
418
+ */
382
419
  clearServerPoints() {
383
420
  let that = this;
384
421
 
@@ -397,6 +434,14 @@ class BacnetServer {
397
434
  Store_Config_Server(JSON.stringify({ objectList: that.objectList, objectStore: that.objectStore }));
398
435
  }
399
436
 
437
+ /**
438
+ * Removes a server point from the objectStore and objectList based on the provided JSON data.
439
+ *
440
+ * @param {Object} json - The JSON data containing information about the server point to be removed.
441
+ * @param {string} json.body.type - The type of the server point ('SV' for CharacterString, 'BV' for BinaryValue, default is AnalogValue).
442
+ * @param {number} json.body.instance - The instance number of the server point to be removed.
443
+ * @returns {Promise<boolean>} A Promise that resolves to true if the server point is successfully removed, otherwise rejects with an error.
444
+ */
400
445
  clearServerPoint(json) {
401
446
  let that = this;
402
447
  return new Promise(async function (resolve, reject) {
@@ -447,6 +492,17 @@ class BacnetServer {
447
492
  });
448
493
  }
449
494
 
495
+ /**
496
+ * Retrieves all server points from the BacnetServer objectStore.
497
+ * Points include Analog Value, Character String Value, and Binary Value objects.
498
+ * Each point is represented as an object with properties:
499
+ * - name: The name of the object.
500
+ * - type: The type of the object (AV for Analog Value, SV for Character String Value, BV for Binary Value).
501
+ * - instance: The instance number of the object.
502
+ *
503
+ * @returns {Promise<Array>} A promise that resolves with an array of points sorted by instance number.
504
+ * @throws {Error} If an error occurs during the retrieval process.
505
+ */
450
506
  getServerPoints() {
451
507
  let that = this;
452
508
  let points = [];
@@ -502,10 +558,12 @@ class BacnetServer {
502
558
  });
503
559
  }
504
560
 
505
- getRandomArbitrary(min, max) {
506
- return Math.random() * (max - min) + min;
507
- }
508
-
561
+ /**
562
+ * Determines the BACnet object type based on the provided value.
563
+ *
564
+ * @param {any} value - The value to determine the BACnet object type for.
565
+ * @returns {string|null} The BACnet object type as a string ('string', 'number', 'boolean') or null if the type is not recognized.
566
+ */
509
567
  getBacnetObjectType(value) {
510
568
  let type = typeof value;
511
569
 
@@ -521,6 +579,13 @@ class BacnetServer {
521
579
  }
522
580
  }
523
581
 
582
+ /**
583
+ * Returns the object identifier for the given type and instance number.
584
+ *
585
+ * @param {string} type - The type of the object.
586
+ * @param {number} instanceNumber - The instance number of the object.
587
+ * @returns {number} The object identifier.
588
+ */
524
589
  getObjectIdentifier(type, instanceNumber) {
525
590
  let that = this;
526
591
  // manual instance numbering
@@ -533,7 +598,6 @@ class BacnetServer {
533
598
  that.objectIdNumber[type]++;
534
599
  return objectId;
535
600
  }
536
-
537
601
  }
538
602
 
539
603
  module.exports = { BacnetServer };
package/bacnet_write.html CHANGED
@@ -42,6 +42,10 @@
42
42
  pointsToWrite: ref([]),
43
43
  nodeService: ref(new NodeService()),
44
44
  deviceCount: ref(),
45
+ showDeviceNameDialog: ref(false),
46
+ showPointNameDialog: ref(false),
47
+ deviceDisplayNameValue: ref(),
48
+ pointDisplayNameValue: ref(),
45
49
  };
46
50
  },
47
51
  setup() {
@@ -301,6 +305,12 @@
301
305
 
302
306
  return count;
303
307
  },
308
+ hasMstpDevices(slotProps) {
309
+ if (slotProps.node.children[1] && slotProps.node.children[1].children.length > 0) {
310
+ return true;
311
+ }
312
+ return false;
313
+ },
304
314
  settingDeviceName(slotProps) {
305
315
  let app = this;
306
316
  if (slotProps.node.settingDisplayName) {
@@ -461,6 +471,8 @@
461
471
  "p-tree": primevue.tree,
462
472
  "p-button": primevue.button,
463
473
  "p-timeline": primevue.timeline,
474
+ "p-dialog": primevue.dialog,
475
+ "p-input-text": primevue.inputtext,
464
476
  },
465
477
  };
466
478
 
@@ -794,6 +806,49 @@
794
806
 
795
807
  <div id="read-networkTree-tab" style="display:none">
796
808
  <div id="read-networkTree-tab-content" class="networkTreeContent" style="display:none">
809
+ <p-dialog
810
+ v-model:visible="showDeviceNameDialog"
811
+ modal
812
+ header="Set Display Name"
813
+ :style="{ width: '25rem' }"
814
+ class="deviceNameDialog">
815
+ <div class="flex align-items-center gap-3 mb-3">
816
+ <label for="displayName" class="font-semibold">Name</label>
817
+ <p-input-text id="displayName" class="flex-auto" autocomplete="off" v-model="deviceDisplayNameValue" />
818
+ </div>
819
+ <div class="flex justify-content-end gap-2">
820
+ <p-button
821
+ class="diplayNameDialogCancel"
822
+ type="button"
823
+ label="Cancel"
824
+ severity="secondary"
825
+ @click="cancelDisplayNameDialog"></p-button>
826
+ <p-button class="diplayNameDialogSave" type="button" label="Save" @click="setDeviceName"></p-button>
827
+ </div>
828
+ </p-dialog>
829
+
830
+ <p-dialog
831
+ v-model:visible="showPointNameDialog"
832
+ modal
833
+ header="Set Point Name"
834
+ :style="{ width: '25rem' }"
835
+ class="deviceNameDialog">
836
+ <div class="flex align-items-center gap-3 mb-3">
837
+ <label for="displayName" class="font-semibold">Name</label>
838
+ <p-input-text id="displayName" class="flex-auto" autocomplete="off" v-model="pointDisplayNameValue" />
839
+ </div>
840
+ <div class="flex justify-content-end gap-2">
841
+ <p-button
842
+ class="diplayNameDialogCancel"
843
+ type="button"
844
+ label="Cancel"
845
+ severity="secondary"
846
+ @click="cancelPointNameDialog"></p-button>
847
+ <p-button class="diplayNameDialogSave" type="button" label="Save" @click="setPointName"></p-button>
848
+ </div>
849
+ </p-dialog>
850
+
851
+
797
852
  <div>
798
853
  <a class="countStatus" style="margin-left: 15px;">Count: {{deviceCount}} device(s)</a>
799
854
  <button @click="getData()" class="reloadButton">
@@ -809,13 +864,19 @@
809
864
  <div v-if="isDeviceActive(slotProps)" class="deviceLabelParent">
810
865
  <b class="deviceLabel">
811
866
  <span class="statusOnline deviceStatus dotOnline dot"></span>
812
- {{slotProps.node.label}}
867
+ {{slotProps.node.label}}
868
+ <span v-if="hasMstpDevices(slotProps)" class="mstpDeviceCount"
869
+ >&nbsp; {{calculateMstpCount(slotProps)}} &nbsp;</span
870
+ >
813
871
  </b>
814
872
  </div>
815
873
  <div v-else>
816
874
  <b class="deviceLabel">
817
875
  <span class="statusOffline deviceStatus dotOffline dot"></span>
818
876
  {{slotProps.node.label}}
877
+ <span v-if="hasMstpDevices(slotProps)" class="mstpDeviceCount"
878
+ >&nbsp; {{calculateMstpCount(slotProps)}} &nbsp;</span
879
+ >
819
880
  </b>
820
881
  </div>
821
882
 
package/common.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- MIT License Copyright 2021, 2022 - Bitpool Pty Ltd
2
+ MIT License Copyright 2021, 2024 - Bitpool Pty Ltd
3
3
  */
4
4
 
5
5
  const { createLogger, format, transports } = require("winston");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitpoolos/edge-bacnet",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "A bacnet gateway for node-red",
5
5
  "dependencies": {
6
6
  "@plus4nodered/ts-node-bacnet": "^1.0.0-beta.2",
@@ -430,7 +430,7 @@ class Client extends events_1.EventEmitter {
430
430
  const result = baNpdu.decode(buffer, offset);
431
431
  var addressObject, destAddress;
432
432
  // @todo a MSTP Dest & Source could occur. this case is not handled.
433
- if (typeof (result.source) != 'undefined') {
433
+ if (typeof (result.source) != 'undefined' && result && result.source) {
434
434
  addressObject = result.source;
435
435
  addressObject.ip = remoteAddress;
436
436
  } else {
package/treeBuilder.js CHANGED
@@ -60,8 +60,18 @@ class treeBuilder {
60
60
  }
61
61
 
62
62
  // Check if the device object exists and the device name is valid
63
- if (deviceObject && typeof deviceName !== 'object') {
63
+ //if (deviceObject && typeof deviceName !== 'object') {
64
+ if (deviceObject) {
65
+
66
+
67
+ //console.log("processDevice found deviceObject ");
68
+
69
+ //await this.processDevicePoints(device, deviceObject, "testingDeviceName", ipAddress, deviceId, index);
70
+
64
71
  await this.processDevicePoints(device, deviceObject, deviceName, ipAddress, deviceId, index);
72
+
73
+ } else {
74
+ //console.log("Unable to find device object");
65
75
  }
66
76
  }
67
77
 
@@ -165,6 +175,7 @@ class treeBuilder {
165
175
  children: pointProperties,
166
176
  type: 'point',
167
177
  parentDevice: deviceName,
178
+ parentDeviceId: device.getDeviceId(),
168
179
  showAdded: false,
169
180
  bacnetType: point.meta.objectId.type,
170
181
  };
@@ -190,7 +201,6 @@ class treeBuilder {
190
201
  this.addPointProperty(pointProperties, 'Modification Date', point.modificationDate);
191
202
  this.addPointProperty(pointProperties, 'Program State', point.programState);
192
203
  this.addPointProperty(pointProperties, 'Record Count', point.recordCount);
193
- this.addPointProperty(pointProperties, 'Vendor Name', point.vendorName);
194
204
 
195
205
  // Return the array of point properties
196
206
  return pointProperties;
@@ -394,7 +404,9 @@ class treeBuilder {
394
404
  * @returns {string} - The computed device name.
395
405
  */
396
406
  computeDeviceName(device) {
397
- if (device.getDisplayName() !== null && device.getDisplayName() !== "" && device.getDisplayName() !== undefined) {
407
+ if (device.getDeviceName() == null && device.getDisplayName() == null) {
408
+ return `${this.getDeviceIpAddress(device)}-${device.getDeviceId()}`;
409
+ } else if (device.getDisplayName() !== null && device.getDisplayName() !== "" && device.getDisplayName() !== undefined) {
398
410
  return device.getDisplayName();
399
411
  }
400
412
  return device.getDeviceName();