@bitpoolos/edge-bacnet 1.5.2 → 1.6.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.
- package/CHANGELOG.md +151 -1
- package/bacnet_client.js +202 -108
- package/bacnet_device.js +3 -1
- package/bacnet_gateway.html +100 -2
- package/bacnet_gateway.js +382 -250
- package/bacnet_inspector.html +43 -0
- package/bacnet_inspector.js +1564 -0
- package/bacnet_inspector_worker.js +535 -0
- package/bacnet_read.html +47 -50
- package/bacnet_read.js +0 -3
- package/common.js +201 -38
- package/inspector.html +460 -0
- package/package.json +6 -2
- package/resources/Logo_Simplified_Positive.svg +32 -0
- package/resources/downloadAsHtml.js +654 -0
- package/resources/icons/device-id-change-icon.svg +4 -0
- package/resources/icons/device-id-conflict-icon.svg +4 -0
- package/resources/icons/favicon.ico +0 -0
- package/resources/icons/points-error-icon.svg +4 -0
- package/resources/icons/points-missing-icon.svg +4 -0
- package/resources/icons/points-ok-icon.svg +4 -0
- package/resources/icons/points-unmapped-icon.svg +5 -0
- package/resources/icons/points-warning-icon.svg +4 -0
- package/resources/inspector.css +25312 -0
- package/resources/inspectorStyle.css +254 -0
- package/resources/inspectorStyles.css +478 -0
- package/resources/node-bacstack-ts/dist/lib/client.js +7 -3
- package/resources/primevue.min.js +1 -0
- package/resources/style.css +17 -1
- package/resources/vue3513.global.prod.js +9 -0
- package/ssrHtmlExporter.js +535 -0
- package/treeBuilder.js +3 -3
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
const { parentPort } = require("worker_threads");
|
|
2
|
+
const { Worker } = require("worker_threads");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
function getRelativeTime(timestamp) {
|
|
6
|
+
if (!timestamp) return "N/A";
|
|
7
|
+
let now = Date.now();
|
|
8
|
+
let timeDiff = now - timestamp;
|
|
9
|
+
if (timeDiff < 0) return "In the future";
|
|
10
|
+
|
|
11
|
+
let seconds = Math.floor(timeDiff / 1000);
|
|
12
|
+
let minutes = Math.floor(seconds / 60);
|
|
13
|
+
let hours = Math.floor(minutes / 60);
|
|
14
|
+
let days = Math.floor(hours / 24);
|
|
15
|
+
let weeks = Math.floor(days / 7);
|
|
16
|
+
let months = Math.floor(days / 30);
|
|
17
|
+
let years = Math.floor(days / 365);
|
|
18
|
+
|
|
19
|
+
if (seconds < 60) return `${seconds} seconds ago`;
|
|
20
|
+
if (minutes < 60) return `${minutes} minutes ago`;
|
|
21
|
+
if (hours < 24) return `${hours} hours ago`;
|
|
22
|
+
if (days < 7) return `${days} days ago`;
|
|
23
|
+
if (weeks < 4) return `${weeks} weeks ago`;
|
|
24
|
+
if (months < 12) return `${months} months ago`;
|
|
25
|
+
return `${years} years ago`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Main processing function
|
|
29
|
+
function processModelStats(data) {
|
|
30
|
+
const { ReadList, DiscoveryList, PublishList, statBlock, filterKey, filterValue } = data;
|
|
31
|
+
const ResultList = {};
|
|
32
|
+
let readCount = 0;
|
|
33
|
+
let pointOkCount = 0;
|
|
34
|
+
let pointNotInDiscoveryCount = 0;
|
|
35
|
+
let pointMatchDiscoveryNotPublishingCount = 0;
|
|
36
|
+
let discoveryCount = 0;
|
|
37
|
+
let unmappedCount = 0;
|
|
38
|
+
|
|
39
|
+
// Reset all statBlock counters to ensure fresh counts
|
|
40
|
+
statBlock.ok = 0;
|
|
41
|
+
statBlock.error = 0;
|
|
42
|
+
statBlock.missing = 0;
|
|
43
|
+
statBlock.warnings = 0;
|
|
44
|
+
statBlock.unmapped = 0;
|
|
45
|
+
statBlock.moved = 0;
|
|
46
|
+
statBlock.deviceIdChange = 0;
|
|
47
|
+
statBlock.deviceIdConflict = 0;
|
|
48
|
+
|
|
49
|
+
// Add this section to track device ID conflicts
|
|
50
|
+
const deviceIdCount = {}; // Track count of each device ID
|
|
51
|
+
const deviceIdIPs = {}; // Track IPs for each device ID for reporting
|
|
52
|
+
const deviceIpDetails = {}; // Store additional details for debugging
|
|
53
|
+
|
|
54
|
+
// Count device IDs while preprocessing DiscoveryList
|
|
55
|
+
for (const [DiscoveryDevice, DiscoveryDeviceObj] of Object.entries(DiscoveryList)) {
|
|
56
|
+
// Improved parsing with validation
|
|
57
|
+
const parts = DiscoveryDevice.split("-");
|
|
58
|
+
if (parts.length < 2) continue; // Skip invalid format
|
|
59
|
+
|
|
60
|
+
const IP = parts[0];
|
|
61
|
+
// Handle case where deviceID might be after the first dash
|
|
62
|
+
// If there are multiple dashes, combine all parts after the first dash
|
|
63
|
+
const DeviceID = parts.slice(1).join("-").trim();
|
|
64
|
+
|
|
65
|
+
// Skip entries with empty deviceIDs
|
|
66
|
+
if (!DeviceID) continue;
|
|
67
|
+
|
|
68
|
+
// Normalize deviceID to avoid false conflicts
|
|
69
|
+
// Convert to string and trim to avoid type and whitespace issues
|
|
70
|
+
const normalizedDeviceID = String(DeviceID).trim();
|
|
71
|
+
|
|
72
|
+
// Initialize or increment device ID count
|
|
73
|
+
deviceIdCount[normalizedDeviceID] = (deviceIdCount[normalizedDeviceID] || 0) + 1;
|
|
74
|
+
|
|
75
|
+
// Track IPs for this device ID
|
|
76
|
+
if (!deviceIdIPs[normalizedDeviceID]) {
|
|
77
|
+
deviceIdIPs[normalizedDeviceID] = new Set();
|
|
78
|
+
deviceIpDetails[normalizedDeviceID] = [];
|
|
79
|
+
}
|
|
80
|
+
deviceIdIPs[normalizedDeviceID].add(IP);
|
|
81
|
+
|
|
82
|
+
// Store enough details to help debug the issue
|
|
83
|
+
deviceIpDetails[normalizedDeviceID].push({
|
|
84
|
+
fullKey: DiscoveryDevice,
|
|
85
|
+
ip: IP,
|
|
86
|
+
deviceId: DeviceID,
|
|
87
|
+
normalizedDeviceId: normalizedDeviceID,
|
|
88
|
+
pointCount: Object.keys(DiscoveryDeviceObj).length
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Count conflicts - a conflict exists when a device ID appears with multiple IPs
|
|
93
|
+
for (const [deviceId, ipSet] of Object.entries(deviceIdIPs)) {
|
|
94
|
+
// Only consider it a conflict if there are truly multiple distinct IPs
|
|
95
|
+
// This additional validation helps filter out false positives
|
|
96
|
+
if (ipSet.size > 1) {
|
|
97
|
+
// Filter out any possible empty or invalid IPs
|
|
98
|
+
const validIPs = Array.from(ipSet).filter(ip => ip && ip.trim().length > 0);
|
|
99
|
+
|
|
100
|
+
if (validIPs.length > 1) {
|
|
101
|
+
statBlock.deviceIdConflict++;
|
|
102
|
+
|
|
103
|
+
// Optionally, add detailed information to a result list
|
|
104
|
+
// This can be useful if you want to display the conflicts
|
|
105
|
+
const ipsList = validIPs.join(', ');
|
|
106
|
+
const conflictKey = `conflict:${deviceId}`;
|
|
107
|
+
|
|
108
|
+
// Get more detailed information about the conflict for display
|
|
109
|
+
const detailsText = deviceIpDetails[deviceId]
|
|
110
|
+
.map(d => `[${d.ip} has ${d.pointCount} points]`)
|
|
111
|
+
.join(', ');
|
|
112
|
+
|
|
113
|
+
ResultList[conflictKey] = {
|
|
114
|
+
deviceID: deviceId,
|
|
115
|
+
ipAddress: ipsList,
|
|
116
|
+
dataModelStatus: `Device ID Conflict - Same ID (${deviceId}) found on multiple IP addresses: ${ipsList}. ${detailsText}`,
|
|
117
|
+
pointInReadList: false,
|
|
118
|
+
pointMatchedDiscoveryList: true,
|
|
119
|
+
pointBeingPublished: false,
|
|
120
|
+
// Add other required fields with empty/default values
|
|
121
|
+
objectType: 'N/A',
|
|
122
|
+
objectInstance: 'N/A',
|
|
123
|
+
pointName: 'N/A',
|
|
124
|
+
displayName: 'Device ID Conflict',
|
|
125
|
+
deviceName: 'Multiple Devices',
|
|
126
|
+
presentValue: null,
|
|
127
|
+
topic: 'N/A',
|
|
128
|
+
discoveredBACnetPointName: 'N/A',
|
|
129
|
+
lastSeen: 'N/A',
|
|
130
|
+
error: 'Device ID Conflict'
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Create a more flexible structure for matching - store multiple variations of the normalized topic
|
|
137
|
+
const PublishTopicsNormalized = new Map();
|
|
138
|
+
for (const [publishTopic, data] of Object.entries(PublishList)) {
|
|
139
|
+
const parts = publishTopic.split("/");
|
|
140
|
+
|
|
141
|
+
// Store multiple variations to increase match chances
|
|
142
|
+
// 1. Full lowercased path
|
|
143
|
+
const fullPath = publishTopic.toLowerCase();
|
|
144
|
+
// 2. Remove the first 2 levels (traditional approach)
|
|
145
|
+
const withoutPrefix = parts.length > 2 ? parts.slice(2).join("/").toLowerCase() : "";
|
|
146
|
+
// 3. Just the last segment (for very short read paths)
|
|
147
|
+
const lastSegment = parts.length > 0 ? parts[parts.length - 1].toLowerCase() : "";
|
|
148
|
+
|
|
149
|
+
// Store the data under all variations
|
|
150
|
+
const publishData = {
|
|
151
|
+
error: data.error || "N/A",
|
|
152
|
+
presentValue: data.presentValue,
|
|
153
|
+
bacnetLastSeen: data.bacnetLastSeen
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
PublishTopicsNormalized.set(fullPath, publishData);
|
|
157
|
+
if (withoutPrefix) PublishTopicsNormalized.set(withoutPrefix, publishData);
|
|
158
|
+
if (lastSegment) PublishTopicsNormalized.set(lastSegment, publishData);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Preprocess DiscoveryList
|
|
162
|
+
const DiscoveryKeys = new Set();
|
|
163
|
+
const DiscoveryPointMap = {};
|
|
164
|
+
|
|
165
|
+
for (const [DiscoveryDevice, DiscoveryDeviceObj] of Object.entries(DiscoveryList)) {
|
|
166
|
+
const [IP, DeviceID] = DiscoveryDevice.split("-");
|
|
167
|
+
|
|
168
|
+
for (const [DiscoveryPoint, DiscoveryPointObj] of Object.entries(DiscoveryDeviceObj)) {
|
|
169
|
+
const DiscPointKey = `${IP}:${DeviceID}:${DiscoveryPointObj.meta.objectId.type}:${DiscoveryPointObj.meta.objectId.instance}`;
|
|
170
|
+
DiscoveryKeys.add(DiscPointKey);
|
|
171
|
+
DiscoveryPointMap[DiscPointKey] = DiscoveryPointObj;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Preprocess DiscoveryKeys into structured maps
|
|
176
|
+
const discoveryMap = {
|
|
177
|
+
withoutIP: new Set(),
|
|
178
|
+
withoutTypeAndInstance: new Set(),
|
|
179
|
+
deviceIDOnly: new Set(),
|
|
180
|
+
ipTypeInstance: new Set(),
|
|
181
|
+
deviceIPOnly: new Set(),
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
DiscoveryKeys.forEach((key) => {
|
|
185
|
+
const parts = key.split(":");
|
|
186
|
+
discoveryMap.withoutIP.add(parts.slice(-3).join(":"));
|
|
187
|
+
discoveryMap.withoutTypeAndInstance.add(parts.slice(0, 2).join(":"));
|
|
188
|
+
discoveryMap.deviceIDOnly.add(parts[1]);
|
|
189
|
+
discoveryMap.ipTypeInstance.add([parts[0], parts[2], parts[3]].join(":"));
|
|
190
|
+
discoveryMap.deviceIPOnly.add(parts[0]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Process read points
|
|
194
|
+
for (const [ReadPoint, ReadPointObj] of Object.entries(ReadList)) {
|
|
195
|
+
const ReadPointKey = `${ReadPointObj.ipAddress}:${ReadPointObj.key}`;
|
|
196
|
+
const DiscoveryKeyMatch = DiscoveryKeys.has(ReadPointKey);
|
|
197
|
+
|
|
198
|
+
// Create multiple variations of the read point path to increase match chances
|
|
199
|
+
const parts = ReadPoint.split("/");
|
|
200
|
+
const normalizedFull = ReadPoint.toLowerCase();
|
|
201
|
+
const normalizedNoPrefix = parts.length > 2 ? parts.slice(2).join("/").toLowerCase() : "";
|
|
202
|
+
const normalizedLastSegment = parts.length > 0 ? parts[parts.length - 1].toLowerCase() : "";
|
|
203
|
+
|
|
204
|
+
// Try each variation to find a match
|
|
205
|
+
let matchVariation = null;
|
|
206
|
+
let PublishPointTopicMatch = false;
|
|
207
|
+
|
|
208
|
+
if (PublishTopicsNormalized.has(normalizedFull)) {
|
|
209
|
+
matchVariation = normalizedFull;
|
|
210
|
+
PublishPointTopicMatch = true;
|
|
211
|
+
} else if (normalizedNoPrefix && PublishTopicsNormalized.has(normalizedNoPrefix)) {
|
|
212
|
+
matchVariation = normalizedNoPrefix;
|
|
213
|
+
PublishPointTopicMatch = true;
|
|
214
|
+
} else if (normalizedLastSegment && PublishTopicsNormalized.has(normalizedLastSegment)) {
|
|
215
|
+
matchVariation = normalizedLastSegment;
|
|
216
|
+
PublishPointTopicMatch = true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const PointResult = { ...ReadPointObj };
|
|
220
|
+
PointResult.topic = ReadPoint;
|
|
221
|
+
PointResult.pointInReadList = true;
|
|
222
|
+
PointResult.pointMatchedDiscoveryList = DiscoveryKeyMatch;
|
|
223
|
+
PointResult.pointBeingPublished = PublishPointTopicMatch;
|
|
224
|
+
|
|
225
|
+
if (PublishPointTopicMatch && matchVariation) {
|
|
226
|
+
const publishData = PublishTopicsNormalized.get(matchVariation);
|
|
227
|
+
PointResult.presentValue = publishData.presentValue ?? null;
|
|
228
|
+
PointResult.lastSeenTimestamp = publishData.bacnetLastSeen;
|
|
229
|
+
} else {
|
|
230
|
+
PointResult.presentValue = null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (DiscoveryKeyMatch) {
|
|
234
|
+
if (PublishPointTopicMatch) {
|
|
235
|
+
const error = PublishTopicsNormalized.get(matchVariation).error;
|
|
236
|
+
if (error !== "none" && error !== "N/A") {
|
|
237
|
+
PointResult.dataModelStatus = `Point Error - Matched in Discovery / Data Model and publishing, however BACNet Error is present: ${error}`;
|
|
238
|
+
statBlock.error++;
|
|
239
|
+
} else {
|
|
240
|
+
PointResult.dataModelStatus = "Point Ok - Matched in Discovery / Data Model and publishing";
|
|
241
|
+
statBlock.ok++;
|
|
242
|
+
}
|
|
243
|
+
PointResult.error = error;
|
|
244
|
+
pointOkCount++;
|
|
245
|
+
} else {
|
|
246
|
+
if (DiscoveryPointMap[ReadPointKey].objectName !== ReadPointObj.pointName) {
|
|
247
|
+
PointResult.dataModelStatus = "Point Missing - Point name in BACnet controller has been changed. Point not publishing.";
|
|
248
|
+
} else {
|
|
249
|
+
PointResult.dataModelStatus = "Point Warning - Matched in Discovery / Data Model ok but not publishing";
|
|
250
|
+
}
|
|
251
|
+
PointResult.error = "N/A";
|
|
252
|
+
pointMatchDiscoveryNotPublishingCount++;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
PointResult.discoveredBACnetPointName = DiscoveryPointMap[ReadPointKey].objectName;
|
|
256
|
+
const timestamp = DiscoveryPointMap[ReadPointKey].timestamp ?? "N/A";
|
|
257
|
+
PointResult.lastSeen = getRelativeTime(timestamp);
|
|
258
|
+
} else {
|
|
259
|
+
PointResult.dataModelStatus = "Point Missing - Point in Read List but not Discovery / Data Model and not publishing";
|
|
260
|
+
|
|
261
|
+
const readParts = ReadPointKey.split(":");
|
|
262
|
+
const readWithoutIP = readParts.slice(-3).join(":");
|
|
263
|
+
const readWithoutTypeAndInstance = readParts.slice(0, 2).join(":");
|
|
264
|
+
const readDeviceIDOnly = readParts[1];
|
|
265
|
+
const readIPTypeInstance = [readParts[0], readParts[2], readParts[3]].join(":");
|
|
266
|
+
const readDeviceIPOnly = readParts[0];
|
|
267
|
+
|
|
268
|
+
if (discoveryMap.withoutIP.has(readWithoutIP)) {
|
|
269
|
+
PointResult.dataModelStatus += ". It appears like the IP associated with this device ID has changed.";
|
|
270
|
+
} else if (discoveryMap.withoutTypeAndInstance.has(readWithoutTypeAndInstance)) {
|
|
271
|
+
PointResult.dataModelStatus +=
|
|
272
|
+
". It appears like the device ID exists in the discovery with the correct IP but does not contain the required point (object type and instance).";
|
|
273
|
+
} else if (discoveryMap.deviceIDOnly.has(readDeviceIDOnly)) {
|
|
274
|
+
PointResult.dataModelStatus +=
|
|
275
|
+
". It appears like the device ID exists in the discovery but does not have the correct IP and does not have the required point (object type and instance).";
|
|
276
|
+
} else if (discoveryMap.ipTypeInstance.has(readIPTypeInstance)) {
|
|
277
|
+
PointResult.dataModelStatus +=
|
|
278
|
+
". It appears like the IP exists in the discovery but does not have the correct device ID. It does however have the required point (object type and instance).";
|
|
279
|
+
} else if (discoveryMap.deviceIPOnly.has(readDeviceIPOnly)) {
|
|
280
|
+
PointResult.dataModelStatus +=
|
|
281
|
+
". It appears like the IP exists in the discovery but does not have the correct device ID and does not have the required point (object type and instance).";
|
|
282
|
+
} else {
|
|
283
|
+
PointResult.dataModelStatus +=
|
|
284
|
+
". No matching combination of IP or ID and point (type and instance) can be found. Device is likely not on the network.";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
PointResult.discoveredBACnetPointName = "N/A";
|
|
288
|
+
PointResult.lastSeen = "N/A";
|
|
289
|
+
PointResult.error = "N/A";
|
|
290
|
+
pointNotInDiscoveryCount++;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
ResultList[ReadPointKey] = PointResult;
|
|
294
|
+
readCount++;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Process unmapped points
|
|
298
|
+
const ReadKeys = new Set(Object.values(ReadList).map((ReadPointObj) => `${ReadPointObj.ipAddress}:${ReadPointObj.key}`));
|
|
299
|
+
const UnmappedKeys = {
|
|
300
|
+
IDName: new Map(),
|
|
301
|
+
IPTypeInstanceName: new Map(),
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
for (const DiscoveryKey of DiscoveryKeys) {
|
|
305
|
+
if (!ReadKeys.has(DiscoveryKey)) {
|
|
306
|
+
const parts = DiscoveryKey.split(":");
|
|
307
|
+
const unmappedPoint = {
|
|
308
|
+
deviceID: parts[1],
|
|
309
|
+
objectType: parts[2],
|
|
310
|
+
objectInstance: parts[3],
|
|
311
|
+
pointName: DiscoveryPointMap[DiscoveryKey].objectName,
|
|
312
|
+
displayName: DiscoveryPointMap[DiscoveryKey].displayName,
|
|
313
|
+
deviceName: DiscoveryPointMap[DiscoveryKey]?.meta?.device?.deviceName || "N/A",
|
|
314
|
+
ipAddress: parts[0] || "N/A",
|
|
315
|
+
presentValue: DiscoveryPointMap[DiscoveryKey].presentValue || null,
|
|
316
|
+
area: "N/A",
|
|
317
|
+
key: DiscoveryKey,
|
|
318
|
+
topic: "N/A",
|
|
319
|
+
pointInReadList: false,
|
|
320
|
+
pointMatchedDiscoveryList: false,
|
|
321
|
+
pointBeingPublished: false,
|
|
322
|
+
dataModelStatus: "Point Unmapped - In discovery list but not read list",
|
|
323
|
+
discoveredBACnetPointName: "N/A",
|
|
324
|
+
lastSeen: "N/A",
|
|
325
|
+
error: "N/A",
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
ResultList[DiscoveryKey] = unmappedPoint;
|
|
329
|
+
|
|
330
|
+
UnmappedKeys.IDName.set(unmappedPoint.deviceID + ":" + unmappedPoint.pointName, unmappedPoint.objectInstance);
|
|
331
|
+
UnmappedKeys.IPTypeInstanceName.set(
|
|
332
|
+
`${unmappedPoint.ipAddress}:${unmappedPoint.objectType}:${unmappedPoint.objectInstance}:${unmappedPoint.pointName}`,
|
|
333
|
+
unmappedPoint.deviceID
|
|
334
|
+
);
|
|
335
|
+
unmappedCount++;
|
|
336
|
+
}
|
|
337
|
+
discoveryCount++;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Find moved points
|
|
341
|
+
for (const [ResultPoint, ResultPointObj] of Object.entries(ResultList)) {
|
|
342
|
+
if (
|
|
343
|
+
ResultPointObj.dataModelStatus.includes(
|
|
344
|
+
". It appears like the device ID exists in the discovery with the correct IP but does not contain the required point (object type and instance)."
|
|
345
|
+
)
|
|
346
|
+
) {
|
|
347
|
+
const key = ResultPointObj.deviceID + ":" + ResultPointObj.pointName;
|
|
348
|
+
if (UnmappedKeys.IDName.has(key)) {
|
|
349
|
+
ResultList[
|
|
350
|
+
ResultPoint
|
|
351
|
+
].dataModelStatus = `Point Moved - Point not publishing - Point potentially moved from object instance ${ResultList[ResultPoint].objectInstance
|
|
352
|
+
} to ${UnmappedKeys.IDName.get(key)}.`;
|
|
353
|
+
statBlock.moved++;
|
|
354
|
+
}
|
|
355
|
+
} else if (
|
|
356
|
+
ResultPointObj.dataModelStatus.includes(
|
|
357
|
+
". It appears like the IP exists in the discovery but does not have the correct device ID. It does however have the required point (object type and instance)."
|
|
358
|
+
)
|
|
359
|
+
) {
|
|
360
|
+
const key = `${ResultPointObj.ipAddress}:${ResultPointObj.objectType}:${ResultPointObj.objectInstance}:${ResultPointObj.pointName}`;
|
|
361
|
+
if (UnmappedKeys.IPTypeInstanceName.has(key)) {
|
|
362
|
+
ResultList[ResultPoint].dataModelStatus = `Device ID Changed - Point not publishing - Device ID potentially change from ${ResultList[ResultPoint].deviceID
|
|
363
|
+
} to ${UnmappedKeys.IPTypeInstanceName.get(key)}.`;
|
|
364
|
+
statBlock.deviceIdChange++;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Update statBlock
|
|
370
|
+
statBlock.missing = pointMatchDiscoveryNotPublishingCount;
|
|
371
|
+
statBlock.warnings = pointMatchDiscoveryNotPublishingCount;
|
|
372
|
+
statBlock.unmapped = unmappedCount;
|
|
373
|
+
|
|
374
|
+
// If we're asked to filter the results directly in the worker
|
|
375
|
+
if (filterKey && filterValue) {
|
|
376
|
+
const filteredList = {};
|
|
377
|
+
|
|
378
|
+
// Apply filtering to the ResultList
|
|
379
|
+
for (const [key, item] of Object.entries(ResultList)) {
|
|
380
|
+
try {
|
|
381
|
+
// Handle nested properties using dot notation
|
|
382
|
+
const value = filterKey.split(".").reduce((obj, key) => {
|
|
383
|
+
if (obj === null || obj === undefined) return undefined;
|
|
384
|
+
return obj[key];
|
|
385
|
+
}, item);
|
|
386
|
+
|
|
387
|
+
// Handle undefined/null values
|
|
388
|
+
if (value === undefined || value === null) {
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Split filter values by comma and trim whitespace
|
|
393
|
+
const filterValues = filterValue.split(",").map((v) => v.trim());
|
|
394
|
+
|
|
395
|
+
// Case-insensitive string comparison for string values
|
|
396
|
+
if (typeof value === "string") {
|
|
397
|
+
const valueLower = value.toLowerCase();
|
|
398
|
+
if (filterValues.some((filterVal) => valueLower.includes(filterVal.toLowerCase()))) {
|
|
399
|
+
filteredList[key] = item;
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Direct comparison for non-string values
|
|
405
|
+
if (
|
|
406
|
+
filterValues.some((filterVal) => {
|
|
407
|
+
// Try to convert filterVal to the same type as value for comparison
|
|
408
|
+
const convertedFilterVal = typeof value === "number" ? Number(filterVal) : filterVal;
|
|
409
|
+
return value === convertedFilterVal;
|
|
410
|
+
})
|
|
411
|
+
) {
|
|
412
|
+
filteredList[key] = item;
|
|
413
|
+
}
|
|
414
|
+
} catch (err) {
|
|
415
|
+
// Skip items that cause filter errors
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Return the filtered list instead
|
|
421
|
+
return {
|
|
422
|
+
ResultList: filteredList,
|
|
423
|
+
statBlock,
|
|
424
|
+
stat_counts: {
|
|
425
|
+
readCount,
|
|
426
|
+
discoveryCount,
|
|
427
|
+
pointOkCount,
|
|
428
|
+
pointNotInDiscoveryCount,
|
|
429
|
+
pointMatchDiscoveryNotPublishingCount,
|
|
430
|
+
unmappedCount,
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
ResultList,
|
|
437
|
+
statBlock,
|
|
438
|
+
stat_counts: {
|
|
439
|
+
readCount,
|
|
440
|
+
discoveryCount,
|
|
441
|
+
pointOkCount,
|
|
442
|
+
pointNotInDiscoveryCount,
|
|
443
|
+
pointMatchDiscoveryNotPublishingCount,
|
|
444
|
+
unmappedCount,
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Listen for messages from the main thread
|
|
450
|
+
parentPort.on("message", (data) => {
|
|
451
|
+
try {
|
|
452
|
+
const result = processModelStats(data);
|
|
453
|
+
parentPort.postMessage(result);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
parentPort.postMessage({ error: error.message });
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Add these variables at the top with your other worker variables
|
|
460
|
+
let activeWorker = null;
|
|
461
|
+
let isWorkerBusy = false;
|
|
462
|
+
let workerQueue = [];
|
|
463
|
+
let workerTaskCount = 0; // Add this to track number of tasks processed
|
|
464
|
+
const MAX_TASKS_PER_WORKER = 5; // Terminate worker after 5 tasks
|
|
465
|
+
|
|
466
|
+
// Then modify the runWithWorker function
|
|
467
|
+
async function runWithWorker(task) {
|
|
468
|
+
if (isWorkerBusy) {
|
|
469
|
+
return new Promise((resolve, reject) => {
|
|
470
|
+
workerQueue.push({ task, resolve, reject });
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
isWorkerBusy = true;
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
// Create worker if needed
|
|
478
|
+
if (!activeWorker) {
|
|
479
|
+
activeWorker = new Worker(path.join(__dirname, "bacnet_inspector_worker.js"));
|
|
480
|
+
workerTaskCount = 0; // Reset task count for new worker
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Increment task count
|
|
484
|
+
workerTaskCount++;
|
|
485
|
+
// Set up promise to get worker response
|
|
486
|
+
const result = await new Promise((resolve, reject) => {
|
|
487
|
+
const messageHandler = (data) => {
|
|
488
|
+
activeWorker.removeListener("error", errorHandler);
|
|
489
|
+
resolve(data);
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const errorHandler = (error) => {
|
|
493
|
+
activeWorker.removeListener("message", messageHandler);
|
|
494
|
+
reject(error);
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
activeWorker.once("message", messageHandler);
|
|
498
|
+
activeWorker.once("error", errorHandler);
|
|
499
|
+
|
|
500
|
+
// Send the task data to the worker
|
|
501
|
+
activeWorker.postMessage(task);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
return result;
|
|
505
|
+
} catch (error) {
|
|
506
|
+
throw error;
|
|
507
|
+
} finally {
|
|
508
|
+
isWorkerBusy = false;
|
|
509
|
+
|
|
510
|
+
// Process next task in queue or terminate worker
|
|
511
|
+
if (workerQueue.length > 0) {
|
|
512
|
+
const nextTask = workerQueue.shift();
|
|
513
|
+
runWithWorker(nextTask.task).then(nextTask.resolve).catch(nextTask.reject);
|
|
514
|
+
} else {
|
|
515
|
+
// Check if worker has processed too many tasks
|
|
516
|
+
if (workerTaskCount >= MAX_TASKS_PER_WORKER && activeWorker) {
|
|
517
|
+
// Terminate worker immediately
|
|
518
|
+
activeWorker.terminate().catch((err) => {
|
|
519
|
+
console.error("Error terminating worker:", err);
|
|
520
|
+
});
|
|
521
|
+
activeWorker = null;
|
|
522
|
+
} else if (activeWorker) {
|
|
523
|
+
// Keep original timeout logic as a backup
|
|
524
|
+
setTimeout(() => {
|
|
525
|
+
if (!isWorkerBusy && activeWorker) {
|
|
526
|
+
activeWorker.terminate().catch((err) => {
|
|
527
|
+
console.error("Error terminating worker:", err);
|
|
528
|
+
});
|
|
529
|
+
activeWorker = null;
|
|
530
|
+
}
|
|
531
|
+
}, 2000); // Reduced from 5000ms to 2000ms
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|