@bitpoolos/edge-bacnet 1.6.5 → 1.6.7
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 +18 -0
- package/bacnet_client.js +160 -92
- package/bacnet_device.js +27 -0
- package/bacnet_read.html +45 -10
- package/package.json +4 -2
- package/resources/style.css +7 -2
- package/treeBuilder.js +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.7] - 15-01-2026
|
|
4
|
+
|
|
5
|
+
Bug fix:
|
|
6
|
+
|
|
7
|
+
- Fix UI tree rendering issue with MSTP folders
|
|
8
|
+
|
|
9
|
+
Minor update:
|
|
10
|
+
|
|
11
|
+
- Styling
|
|
12
|
+
- Added properties to device points and minor change to device point polling
|
|
13
|
+
|
|
14
|
+
## [1.6.6] - 20-11-2025
|
|
15
|
+
|
|
16
|
+
Minor update:
|
|
17
|
+
|
|
18
|
+
- Added all stored point properties as columns to "Export point list" CSV in read node Properties tab.
|
|
19
|
+
- Adjustment to point polling, setting stricter maxApdu sizes
|
|
20
|
+
|
|
3
21
|
## [1.6.5] - 09-10-2025
|
|
4
22
|
|
|
5
23
|
Bug fix:
|
package/bacnet_client.js
CHANGED
|
@@ -222,9 +222,31 @@ class BacnetClient extends EventEmitter {
|
|
|
222
222
|
port: port,
|
|
223
223
|
};
|
|
224
224
|
|
|
225
|
+
// Try to find the device to use device-specific options
|
|
226
|
+
let device = null;
|
|
227
|
+
if (type === 8) {
|
|
228
|
+
// Device object - instance is the device ID
|
|
229
|
+
device = that.deviceList.find(ele => ele.getDeviceId() === instance);
|
|
230
|
+
} else {
|
|
231
|
+
// For non-device objects, we can't determine the device from just address/instance
|
|
232
|
+
// This is a limitation of testFunction's current signature
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Use device-specific options if we found the device, otherwise use safer defaults
|
|
236
|
+
let readOptions;
|
|
237
|
+
if (device) {
|
|
238
|
+
readOptions = that.getDeviceSpecificOptions(device);
|
|
239
|
+
} else {
|
|
240
|
+
// Conservative defaults for unknown devices (assume small MSTP)
|
|
241
|
+
readOptions = {
|
|
242
|
+
maxSegments: 0, // No segmentation
|
|
243
|
+
maxApdu: 2 // 206 octets - safe for most MSTP devices
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
225
247
|
const propertiesArray = [{ objectId: { type: type, instance: instance }, properties: [{ id: property }] }];
|
|
226
248
|
|
|
227
|
-
that.client.readPropertyMultiple(addressObject, propertiesArray,
|
|
249
|
+
that.client.readPropertyMultiple(addressObject, propertiesArray, readOptions, (err, value) => {
|
|
228
250
|
console.log("1 - readPropertyMultiple: ");
|
|
229
251
|
|
|
230
252
|
console.log(value);
|
|
@@ -248,7 +270,7 @@ class BacnetClient extends EventEmitter {
|
|
|
248
270
|
addressObject,
|
|
249
271
|
{ type: type, instance: instance },
|
|
250
272
|
property,
|
|
251
|
-
|
|
273
|
+
readOptions,
|
|
252
274
|
(err, value) => {
|
|
253
275
|
console.log("2 - readProperty: ");
|
|
254
276
|
|
|
@@ -316,12 +338,13 @@ class BacnetClient extends EventEmitter {
|
|
|
316
338
|
address: device.getAddress(),
|
|
317
339
|
port: device.getPort(),
|
|
318
340
|
};
|
|
341
|
+
const readOptions = that.getDeviceSpecificOptions(device);
|
|
319
342
|
return new Promise((resolve, reject) => {
|
|
320
343
|
that.client.readProperty(
|
|
321
344
|
addressObject,
|
|
322
345
|
{ type: baEnum.ObjectType.DEVICE, instance: device.getDeviceId() },
|
|
323
346
|
baEnum.PropertyIdentifier.PROTOCOL_SERVICES_SUPPORTED,
|
|
324
|
-
|
|
347
|
+
readOptions,
|
|
325
348
|
(err, value) => {
|
|
326
349
|
if (err) {
|
|
327
350
|
reject(err);
|
|
@@ -339,7 +362,7 @@ class BacnetClient extends EventEmitter {
|
|
|
339
362
|
let that = this;
|
|
340
363
|
let address = device.getAddress().address;
|
|
341
364
|
let deviceId = device.getDeviceId();
|
|
342
|
-
let foundParentIndex = that.deviceList.findIndex((ele) =>
|
|
365
|
+
let foundParentIndex = that.deviceList.findIndex((ele) => that.getDeviceAddress(ele) == address && !ele.getIsMstpDevice());
|
|
343
366
|
if (foundParentIndex !== -1) {
|
|
344
367
|
that.deviceList[foundParentIndex].addChildDevice(deviceId);
|
|
345
368
|
device.setParentDeviceId(that.deviceList[foundParentIndex].getDeviceId());
|
|
@@ -939,13 +962,46 @@ class BacnetClient extends EventEmitter {
|
|
|
939
962
|
|
|
940
963
|
async updateManyPoints(device, points) {
|
|
941
964
|
try {
|
|
942
|
-
|
|
965
|
+
// Use device-specific options instead of global options
|
|
966
|
+
const deviceOptions = this.getDeviceSpecificOptions(device);
|
|
967
|
+
const results = await this._readObjectWithRequestArray(device, points, deviceOptions);
|
|
943
968
|
return results;
|
|
944
969
|
} catch (error) {
|
|
945
970
|
throw error;
|
|
946
971
|
}
|
|
947
972
|
}
|
|
948
973
|
|
|
974
|
+
getDeviceSpecificOptions(device) {
|
|
975
|
+
let maxSegments = this.readPropertyMultipleOptions.maxSegments;
|
|
976
|
+
let maxApdu = this.readPropertyMultipleOptions.maxApdu;
|
|
977
|
+
|
|
978
|
+
// Adjust for devices with no segmentation support
|
|
979
|
+
if (device.getSegmentation() == 3) {
|
|
980
|
+
maxSegments = 0;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Adjust maxApdu based on device capability
|
|
984
|
+
const deviceMaxApdu = device.getMaxApdu();
|
|
985
|
+
if (deviceMaxApdu <= 50) {
|
|
986
|
+
maxApdu = 0; // 50 octets
|
|
987
|
+
} else if (deviceMaxApdu <= 128) {
|
|
988
|
+
maxApdu = 1; // 128 octets
|
|
989
|
+
} else if (deviceMaxApdu <= 206) {
|
|
990
|
+
maxApdu = 2; // 206 octets
|
|
991
|
+
} else if (deviceMaxApdu <= 480) {
|
|
992
|
+
maxApdu = 3; // 480 octets
|
|
993
|
+
} else if (deviceMaxApdu <= 1024) {
|
|
994
|
+
maxApdu = 4; // 1024 octets
|
|
995
|
+
} else {
|
|
996
|
+
maxApdu = 5; // 1476 octets
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
return {
|
|
1000
|
+
maxSegments: maxSegments,
|
|
1001
|
+
maxApdu: maxApdu
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
949
1005
|
updatePointWithRetry(device, point, retryCount = 1) {
|
|
950
1006
|
let that = this;
|
|
951
1007
|
const tryUpdate = (retriesLeft) => {
|
|
@@ -997,21 +1053,8 @@ class BacnetClient extends EventEmitter {
|
|
|
997
1053
|
port: device.getPort(),
|
|
998
1054
|
};
|
|
999
1055
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
if (device.getSegmentation() == 3) {
|
|
1004
|
-
maxSegments = 0;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
if (device.getMaxApdu() == 480) {
|
|
1008
|
-
maxApdu = 3;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
let settings = {
|
|
1012
|
-
maxSegments: maxSegments,
|
|
1013
|
-
maxApdu: maxApdu,
|
|
1014
|
-
};
|
|
1056
|
+
// Use device-specific options
|
|
1057
|
+
const settings = that.getDeviceSpecificOptions(device);
|
|
1015
1058
|
|
|
1016
1059
|
return new Promise((resolve, reject) => {
|
|
1017
1060
|
that.client.readProperty(
|
|
@@ -1032,8 +1075,15 @@ class BacnetClient extends EventEmitter {
|
|
|
1032
1075
|
}
|
|
1033
1076
|
|
|
1034
1077
|
estimateMaxObjectSize(apduSize) {
|
|
1035
|
-
|
|
1036
|
-
|
|
1078
|
+
// Be more conservative for very small MSTP devices
|
|
1079
|
+
if (apduSize <= 50) {
|
|
1080
|
+
return 1; // Only 1 object at a time for 50-byte devices
|
|
1081
|
+
} else if (apduSize <= 128) {
|
|
1082
|
+
return 3; // 3 objects for 128-byte devices
|
|
1083
|
+
} else if (apduSize <= 206) {
|
|
1084
|
+
return 5; // 5 objects for 206-byte devices
|
|
1085
|
+
} else if (apduSize < 500) {
|
|
1086
|
+
return 10; // Reduced from 20 for safety
|
|
1037
1087
|
} else if (apduSize > 500 && apduSize < 1000) {
|
|
1038
1088
|
//return Math.round(((apduSize - 30) / 7));
|
|
1039
1089
|
return 50;
|
|
@@ -1254,12 +1304,13 @@ class BacnetClient extends EventEmitter {
|
|
|
1254
1304
|
port: device.getPort(),
|
|
1255
1305
|
};
|
|
1256
1306
|
let deviceId = device.getDeviceId();
|
|
1307
|
+
const readOptions = that.getDeviceSpecificOptions(device);
|
|
1257
1308
|
|
|
1258
1309
|
that.client.readProperty(
|
|
1259
1310
|
addressObject,
|
|
1260
1311
|
{ type: baEnum.ObjectType.DEVICE, instance: deviceId },
|
|
1261
1312
|
baEnum.PropertyIdentifier.OBJECT_NAME,
|
|
1262
|
-
|
|
1313
|
+
readOptions,
|
|
1263
1314
|
callback
|
|
1264
1315
|
);
|
|
1265
1316
|
}
|
|
@@ -1304,10 +1355,8 @@ class BacnetClient extends EventEmitter {
|
|
|
1304
1355
|
|
|
1305
1356
|
_readObjectFull(device, type, instance) {
|
|
1306
1357
|
const that = this;
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
maxApdu: that.readPropertyMultipleOptions.maxApdu,
|
|
1310
|
-
};
|
|
1358
|
+
// Use device-specific options for reading all properties
|
|
1359
|
+
const readOptions = that.getDeviceSpecificOptions(device);
|
|
1311
1360
|
|
|
1312
1361
|
const readIndividualPropsOptions = {
|
|
1313
1362
|
maxSegments: 0,
|
|
@@ -1319,8 +1368,8 @@ class BacnetClient extends EventEmitter {
|
|
|
1319
1368
|
port: device.getPort(),
|
|
1320
1369
|
};
|
|
1321
1370
|
|
|
1322
|
-
// Define
|
|
1323
|
-
const
|
|
1371
|
+
// Define default properties for non-device objects
|
|
1372
|
+
const defaultProperties = [
|
|
1324
1373
|
{ id: baEnum.PropertyIdentifier.PRESENT_VALUE },
|
|
1325
1374
|
{ id: baEnum.PropertyIdentifier.DESCRIPTION },
|
|
1326
1375
|
{ id: baEnum.PropertyIdentifier.UNITS },
|
|
@@ -1335,8 +1384,66 @@ class BacnetClient extends EventEmitter {
|
|
|
1335
1384
|
{ id: baEnum.PropertyIdentifier.VENDOR_NAME },
|
|
1336
1385
|
];
|
|
1337
1386
|
|
|
1387
|
+
// Use device-specific properties for type 8, otherwise use default
|
|
1388
|
+
const propertiesToRead = type === 8 ? BacnetDevice.getDeviceObjectProperties() : defaultProperties;
|
|
1389
|
+
|
|
1390
|
+
// Function to read properties individually
|
|
1391
|
+
const readPropertiesIndividually = (resolve, reject) => {
|
|
1392
|
+
const promises = propertiesToRead.map(
|
|
1393
|
+
(property) =>
|
|
1394
|
+
new Promise((propertyResolve) => {
|
|
1395
|
+
that.client.readProperty(
|
|
1396
|
+
addressObject,
|
|
1397
|
+
{ type: type, instance: instance },
|
|
1398
|
+
property.id,
|
|
1399
|
+
readIndividualPropsOptions,
|
|
1400
|
+
(err, value) => {
|
|
1401
|
+
if (err) {
|
|
1402
|
+
propertyResolve(null);
|
|
1403
|
+
} else {
|
|
1404
|
+
propertyResolve({
|
|
1405
|
+
id: property.id,
|
|
1406
|
+
index: value.property.index,
|
|
1407
|
+
value: value.values,
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
);
|
|
1412
|
+
})
|
|
1413
|
+
);
|
|
1414
|
+
|
|
1415
|
+
Promise.all(promises)
|
|
1416
|
+
.then((resultArray) => {
|
|
1417
|
+
// Filter out null results
|
|
1418
|
+
const validResults = resultArray.filter((result) => result !== null);
|
|
1419
|
+
|
|
1420
|
+
resolve({
|
|
1421
|
+
error: null,
|
|
1422
|
+
value: {
|
|
1423
|
+
values: [
|
|
1424
|
+
{
|
|
1425
|
+
objectId: {
|
|
1426
|
+
type: type,
|
|
1427
|
+
instance: instance,
|
|
1428
|
+
},
|
|
1429
|
+
values: validResults,
|
|
1430
|
+
},
|
|
1431
|
+
],
|
|
1432
|
+
},
|
|
1433
|
+
});
|
|
1434
|
+
})
|
|
1435
|
+
.catch(reject);
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1338
1438
|
return new Promise((resolve, reject) => {
|
|
1339
|
-
//
|
|
1439
|
+
// For Device objects (type 8), skip ALL attempt - many MSTP devices don't support it
|
|
1440
|
+
// Go straight to reading individual properties for better reliability
|
|
1441
|
+
if (type === 8) {
|
|
1442
|
+
readPropertiesIndividually(resolve, reject);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// For other object types, try to read all properties at once first
|
|
1340
1447
|
that
|
|
1341
1448
|
._readObject(addressObject, type, instance, [{ id: baEnum.PropertyIdentifier.ALL }], readOptions)
|
|
1342
1449
|
.then((result) => {
|
|
@@ -1345,70 +1452,20 @@ class BacnetClient extends EventEmitter {
|
|
|
1345
1452
|
resolve(result);
|
|
1346
1453
|
} else {
|
|
1347
1454
|
// If not, proceed to read individual properties
|
|
1348
|
-
readPropertiesIndividually();
|
|
1455
|
+
readPropertiesIndividually(resolve, reject);
|
|
1349
1456
|
}
|
|
1350
1457
|
})
|
|
1351
1458
|
.catch(() => {
|
|
1352
1459
|
// On error, proceed to read individual properties
|
|
1353
|
-
readPropertiesIndividually();
|
|
1460
|
+
readPropertiesIndividually(resolve, reject);
|
|
1354
1461
|
});
|
|
1355
|
-
|
|
1356
|
-
// Function to read properties individually
|
|
1357
|
-
const readPropertiesIndividually = () => {
|
|
1358
|
-
const promises = allProperties.map(
|
|
1359
|
-
(property, index) =>
|
|
1360
|
-
new Promise((propertyResolve) => {
|
|
1361
|
-
that.client.readProperty(
|
|
1362
|
-
addressObject,
|
|
1363
|
-
{ type: type, instance: instance },
|
|
1364
|
-
property.id,
|
|
1365
|
-
readIndividualPropsOptions,
|
|
1366
|
-
(err, value) => {
|
|
1367
|
-
if (err) {
|
|
1368
|
-
propertyResolve(null);
|
|
1369
|
-
} else {
|
|
1370
|
-
propertyResolve({
|
|
1371
|
-
id: property.id,
|
|
1372
|
-
index: value.property.index,
|
|
1373
|
-
value: value.values,
|
|
1374
|
-
});
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
);
|
|
1378
|
-
})
|
|
1379
|
-
);
|
|
1380
|
-
|
|
1381
|
-
Promise.all(promises)
|
|
1382
|
-
.then((resultArray) => {
|
|
1383
|
-
// Filter out null results
|
|
1384
|
-
const validResults = resultArray.filter((result) => result !== null);
|
|
1385
|
-
|
|
1386
|
-
resolve({
|
|
1387
|
-
error: null,
|
|
1388
|
-
value: {
|
|
1389
|
-
values: [
|
|
1390
|
-
{
|
|
1391
|
-
objectId: {
|
|
1392
|
-
type: type,
|
|
1393
|
-
instance: instance,
|
|
1394
|
-
},
|
|
1395
|
-
values: validResults,
|
|
1396
|
-
},
|
|
1397
|
-
],
|
|
1398
|
-
},
|
|
1399
|
-
});
|
|
1400
|
-
})
|
|
1401
|
-
.catch(reject);
|
|
1402
|
-
};
|
|
1403
1462
|
});
|
|
1404
1463
|
}
|
|
1405
1464
|
|
|
1406
1465
|
_readObjectLite(device, type, instance) {
|
|
1407
1466
|
const that = this;
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
maxApdu: that.readPropertyMultipleOptions.maxApdu,
|
|
1411
|
-
};
|
|
1467
|
+
// Use device-specific options
|
|
1468
|
+
const readOptions = that.getDeviceSpecificOptions(device);
|
|
1412
1469
|
|
|
1413
1470
|
const readIndividualPropsOptions = {
|
|
1414
1471
|
maxSegments: 0,
|
|
@@ -1573,10 +1630,8 @@ class BacnetClient extends EventEmitter {
|
|
|
1573
1630
|
scanDevice(device) {
|
|
1574
1631
|
let that = this;
|
|
1575
1632
|
return new Promise((resolve, reject) => {
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
maxApdu: that.readPropertyMultipleOptions.maxApdu,
|
|
1579
|
-
};
|
|
1633
|
+
// Use device-specific options
|
|
1634
|
+
const readOptions = that.getDeviceSpecificOptions(device);
|
|
1580
1635
|
this._readObjectList(device, readOptions, (err, result) => {
|
|
1581
1636
|
if (!err) {
|
|
1582
1637
|
try {
|
|
@@ -2057,10 +2112,23 @@ class BacnetClient extends EventEmitter {
|
|
|
2057
2112
|
}
|
|
2058
2113
|
break;
|
|
2059
2114
|
case baEnum.PropertyIdentifier.VENDOR_NAME:
|
|
2060
|
-
if (object.value) {
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2115
|
+
if (object.value && object.value[0] && object.value[0].value && typeof object.value[0].value == "string") {
|
|
2116
|
+
values[objectId].vendorName = object.value[0].value;
|
|
2117
|
+
}
|
|
2118
|
+
break;
|
|
2119
|
+
case baEnum.PropertyIdentifier.MODEL_NAME:
|
|
2120
|
+
if (object.value && object.value[0] && object.value[0].value && typeof object.value[0].value == "string") {
|
|
2121
|
+
values[objectId].modelName = object.value[0].value;
|
|
2122
|
+
}
|
|
2123
|
+
break;
|
|
2124
|
+
case baEnum.PropertyIdentifier.FIRMWARE_REVISION:
|
|
2125
|
+
if (object.value && object.value[0] && object.value[0].value && typeof object.value[0].value == "string") {
|
|
2126
|
+
values[objectId].firmwareRevision = object.value[0].value;
|
|
2127
|
+
}
|
|
2128
|
+
break;
|
|
2129
|
+
case baEnum.PropertyIdentifier.APPLICATION_SOFTWARE_VERSION:
|
|
2130
|
+
if (object.value && object.value[0] && object.value[0].value && typeof object.value[0].value == "string") {
|
|
2131
|
+
values[objectId].applicationSoftwareVersion = object.value[0].value;
|
|
2064
2132
|
}
|
|
2065
2133
|
break;
|
|
2066
2134
|
}
|
package/bacnet_device.js
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
|
+
const bacnet = require("./resources/node-bacstack-ts/dist/index.js");
|
|
2
|
+
const baEnum = bacnet.enum;
|
|
3
|
+
|
|
4
|
+
// Device Object (type 8) properties - critical properties guaranteed per ASHRAE 135
|
|
5
|
+
// These are the minimum required properties that all BACnet devices must support
|
|
6
|
+
const DEVICE_OBJECT_PROPERTIES = [
|
|
7
|
+
{ id: baEnum.PropertyIdentifier.OBJECT_IDENTIFIER },
|
|
8
|
+
{ id: baEnum.PropertyIdentifier.OBJECT_NAME },
|
|
9
|
+
{ id: baEnum.PropertyIdentifier.OBJECT_TYPE },
|
|
10
|
+
{ id: baEnum.PropertyIdentifier.SYSTEM_STATUS },
|
|
11
|
+
{ id: baEnum.PropertyIdentifier.VENDOR_NAME },
|
|
12
|
+
{ id: baEnum.PropertyIdentifier.VENDOR_IDENTIFIER },
|
|
13
|
+
{ id: baEnum.PropertyIdentifier.MODEL_NAME },
|
|
14
|
+
{ id: baEnum.PropertyIdentifier.FIRMWARE_REVISION },
|
|
15
|
+
{ id: baEnum.PropertyIdentifier.APPLICATION_SOFTWARE_VERSION },
|
|
16
|
+
{ id: baEnum.PropertyIdentifier.DESCRIPTION },
|
|
17
|
+
];
|
|
18
|
+
|
|
1
19
|
class BacnetDevice {
|
|
20
|
+
/**
|
|
21
|
+
* Returns the standardized list of properties to read for Device objects (type 8).
|
|
22
|
+
* These are critical properties guaranteed to exist on all BACnet devices per ASHRAE 135.
|
|
23
|
+
* @returns {Array} Array of property identifier objects
|
|
24
|
+
*/
|
|
25
|
+
static getDeviceObjectProperties() {
|
|
26
|
+
return DEVICE_OBJECT_PROPERTIES;
|
|
27
|
+
}
|
|
28
|
+
|
|
2
29
|
constructor(fromImport, config) {
|
|
3
30
|
let that = this;
|
|
4
31
|
|
package/bacnet_read.html
CHANGED
|
@@ -433,8 +433,20 @@
|
|
|
433
433
|
},
|
|
434
434
|
exportPointListCsv() {
|
|
435
435
|
let app = this;
|
|
436
|
-
let csvContent = "ipAddress,deviceId,deviceName,pointName,objectType" + "\r\n";
|
|
436
|
+
let csvContent = "ipAddress,deviceId,deviceName,pointName,objectType,presentValue,description,units,objectName,displayName,systemStatus,modificationDate,programState,recordCount,hasPriorityArray,vendorName" + "\r\n";
|
|
437
437
|
const keys = Object.keys(app.pointList);
|
|
438
|
+
|
|
439
|
+
// Helper function to safely get property value and escape for CSV
|
|
440
|
+
const getCsvValue = (value) => {
|
|
441
|
+
if (value === null || value === undefined) return "";
|
|
442
|
+
const stringValue = String(value);
|
|
443
|
+
// Escape double quotes and wrap in quotes if contains comma, newline, or double quote
|
|
444
|
+
if (stringValue.includes(',') || stringValue.includes('\n') || stringValue.includes('"')) {
|
|
445
|
+
return '"' + stringValue.replace(/"/g, '""') + '"';
|
|
446
|
+
}
|
|
447
|
+
return stringValue;
|
|
448
|
+
};
|
|
449
|
+
|
|
438
450
|
for (key in keys) {
|
|
439
451
|
const guid = keys[key];
|
|
440
452
|
const ipAddress = guid.split("-")[0];
|
|
@@ -452,16 +464,39 @@
|
|
|
452
464
|
const points = Object.keys(deviceObject);
|
|
453
465
|
if (points.length > 0) {
|
|
454
466
|
for (point in points) {
|
|
467
|
+
const pointData = deviceObject[points[point]] || {};
|
|
455
468
|
csvContent +=
|
|
456
|
-
ipAddress +
|
|
469
|
+
getCsvValue(ipAddress) +
|
|
470
|
+
"," +
|
|
471
|
+
getCsvValue(deviceId) +
|
|
472
|
+
"," +
|
|
473
|
+
getCsvValue(deviceName) +
|
|
474
|
+
"," +
|
|
475
|
+
getCsvValue(points[point]) +
|
|
476
|
+
"," +
|
|
477
|
+
getCsvValue(pointData.meta?.objectId?.type) +
|
|
457
478
|
"," +
|
|
458
|
-
|
|
479
|
+
getCsvValue(pointData.presentValue) +
|
|
459
480
|
"," +
|
|
460
|
-
|
|
481
|
+
getCsvValue(pointData.description) +
|
|
461
482
|
"," +
|
|
462
|
-
|
|
483
|
+
getCsvValue(pointData.units) +
|
|
463
484
|
"," +
|
|
464
|
-
|
|
485
|
+
getCsvValue(pointData.objectName) +
|
|
486
|
+
"," +
|
|
487
|
+
getCsvValue(pointData.displayName) +
|
|
488
|
+
"," +
|
|
489
|
+
getCsvValue(pointData.systemStatus) +
|
|
490
|
+
"," +
|
|
491
|
+
getCsvValue(pointData.modificationDate) +
|
|
492
|
+
"," +
|
|
493
|
+
getCsvValue(pointData.programState) +
|
|
494
|
+
"," +
|
|
495
|
+
getCsvValue(pointData.recordCount) +
|
|
496
|
+
"," +
|
|
497
|
+
getCsvValue(pointData.hasPriorityArray) +
|
|
498
|
+
"," +
|
|
499
|
+
getCsvValue(pointData.vendorName) +
|
|
465
500
|
"\r\n";
|
|
466
501
|
|
|
467
502
|
if (parseInt(point) == points.length - 1 && parseInt(key) == keys.length - 1) {
|
|
@@ -687,14 +722,14 @@
|
|
|
687
722
|
let app = this;
|
|
688
723
|
let device = app.getDeviceFromDeviceList(slotProps.node.ipAddr, slotProps.node.deviceId);
|
|
689
724
|
if (device) {
|
|
690
|
-
app.nodeService.purgeDevice(device).then(function (result) {});
|
|
725
|
+
app.nodeService.purgeDevice(device).then(function (result) { });
|
|
691
726
|
}
|
|
692
727
|
},
|
|
693
728
|
updatePointsForDevice(slotProps) {
|
|
694
729
|
let app = this;
|
|
695
730
|
let device = app.getDeviceFromDeviceList(slotProps.node.ipAddr, slotProps.node.deviceId);
|
|
696
731
|
if (device) {
|
|
697
|
-
app.nodeService.updatePointsForDevice(device).then(function (result) {});
|
|
732
|
+
app.nodeService.updatePointsForDevice(device).then(function (result) { });
|
|
698
733
|
}
|
|
699
734
|
},
|
|
700
735
|
updatePoint(slotProps) {
|
|
@@ -705,7 +740,7 @@
|
|
|
705
740
|
});
|
|
706
741
|
const deviceKey = `${app.getDeviceAddress(device.address)}-${device.deviceId}`;
|
|
707
742
|
if (device) {
|
|
708
|
-
app.nodeService.updatePoint(deviceKey, pointKey).then(function (result) {});
|
|
743
|
+
app.nodeService.updatePoint(deviceKey, pointKey).then(function (result) { });
|
|
709
744
|
}
|
|
710
745
|
},
|
|
711
746
|
setDeviceName() {
|
|
@@ -1708,4 +1743,4 @@
|
|
|
1708
1743
|
<li><a href="https://wiki.bitpool.com/">wiki.bitpool.com</a> - find more documentation.</li>
|
|
1709
1744
|
<li><a href="https://bacnet.org/">BACnet</a> - find more about the protocol.</li>
|
|
1710
1745
|
</ul>
|
|
1711
|
-
</script>
|
|
1746
|
+
</script>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitpoolos/edge-bacnet",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.7",
|
|
4
4
|
"description": "A bacnet gateway for node-red",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@plus4nodered/ts-node-bacnet": "^1.0.0-beta.2",
|
|
@@ -26,13 +26,15 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"keywords": [
|
|
29
|
+
"BACnet",
|
|
29
30
|
"node-red",
|
|
30
31
|
"bitpool",
|
|
31
32
|
"bitpoolos",
|
|
32
33
|
"edge",
|
|
33
34
|
"big data",
|
|
34
35
|
"iot",
|
|
35
|
-
"
|
|
36
|
+
"building automation",
|
|
37
|
+
"bms"
|
|
36
38
|
],
|
|
37
39
|
"engines": {
|
|
38
40
|
"node": ">=12.0.0"
|
package/resources/style.css
CHANGED
|
@@ -319,8 +319,13 @@
|
|
|
319
319
|
margin-left: 10px;
|
|
320
320
|
background-color: #00aeef;
|
|
321
321
|
color: #ffffff;
|
|
322
|
-
border-radius:
|
|
323
|
-
font-size:
|
|
322
|
+
border-radius: 4px;
|
|
323
|
+
font-size: 11px;
|
|
324
|
+
font-weight: 500;
|
|
325
|
+
padding: 2px 6px;
|
|
326
|
+
display: inline-block;
|
|
327
|
+
vertical-align: middle;
|
|
328
|
+
line-height: 1.2;
|
|
324
329
|
}
|
|
325
330
|
|
|
326
331
|
.pointAddedToRead {
|
package/treeBuilder.js
CHANGED
|
@@ -355,6 +355,12 @@ class treeBuilder {
|
|
|
355
355
|
this.addPointProperty(pointProperties, "Modification Date", point.modificationDate);
|
|
356
356
|
this.addPointProperty(pointProperties, "Program State", point.programState);
|
|
357
357
|
this.addPointProperty(pointProperties, "Record Count", point.recordCount);
|
|
358
|
+
|
|
359
|
+
// Add device-specific properties (type 8)
|
|
360
|
+
this.addPointProperty(pointProperties, "Vendor Name", point.vendorName);
|
|
361
|
+
this.addPointProperty(pointProperties, "Model Name", point.modelName);
|
|
362
|
+
this.addPointProperty(pointProperties, "Firmware Revision", point.firmwareRevision);
|
|
363
|
+
this.addPointProperty(pointProperties, "Application Software Version", point.applicationSoftwareVersion);
|
|
358
364
|
|
|
359
365
|
// Return the array of point properties
|
|
360
366
|
return pointProperties;
|