@uns-kit/core 2.0.40 → 2.0.42

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.
Files changed (102) hide show
  1. package/README.md +38 -15
  2. package/dist/base-path.js.map +1 -1
  3. package/dist/config/project.config.extension.js.map +1 -1
  4. package/dist/config-file.js.map +1 -1
  5. package/dist/graphql/schema.js.map +1 -1
  6. package/dist/index.d.ts +9 -8
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +8 -7
  9. package/dist/index.js.map +1 -1
  10. package/dist/logger.js.map +1 -1
  11. package/dist/tools/auth/auth-client.js.map +1 -1
  12. package/dist/tools/auth/index.js.map +1 -1
  13. package/dist/tools/auth/secure-store.js.map +1 -1
  14. package/dist/tools/base-path.js.map +1 -1
  15. package/dist/tools/datahub/datahub-client.js.map +1 -1
  16. package/dist/tools/file-utils.d.ts +2 -2
  17. package/dist/tools/file-utils.js +24 -24
  18. package/dist/tools/file-utils.js.map +1 -1
  19. package/dist/tools/generate-config-schema.js.map +1 -1
  20. package/dist/tools/generate-uns-dictionary.d.ts +23 -23
  21. package/dist/tools/generate-uns-dictionary.js +147 -147
  22. package/dist/tools/generate-uns-dictionary.js.map +1 -1
  23. package/dist/tools/generate-uns-measurements.d.ts +21 -21
  24. package/dist/tools/generate-uns-measurements.js +119 -119
  25. package/dist/tools/generate-uns-measurements.js.map +1 -1
  26. package/dist/tools/generate-uns-reference.d.ts +2 -2
  27. package/dist/tools/generate-uns-reference.js +76 -76
  28. package/dist/tools/generate-uns-reference.js.map +1 -1
  29. package/dist/tools/pull-request.js.map +1 -1
  30. package/dist/tools/refresh-uns.js.map +1 -1
  31. package/dist/tools/schema.js.map +1 -1
  32. package/dist/tools/sync-uns-schema.d.ts +2 -2
  33. package/dist/tools/sync-uns-schema.js +472 -472
  34. package/dist/tools/sync-uns-schema.js.map +1 -1
  35. package/dist/uns/handover-manager-event-emitter.js.map +1 -1
  36. package/dist/uns/handover-manager.js.map +1 -1
  37. package/dist/uns/process-config.js.map +1 -1
  38. package/dist/uns/service-metadata.d.ts +53 -0
  39. package/dist/uns/service-metadata.d.ts.map +1 -0
  40. package/dist/uns/service-metadata.js +56 -0
  41. package/dist/uns/service-metadata.js.map +1 -0
  42. package/dist/uns/status-monitor.js.map +1 -1
  43. package/dist/uns/uns-asset.d.ts +1 -1
  44. package/dist/uns/uns-asset.js +1 -1
  45. package/dist/uns/uns-asset.js.map +1 -1
  46. package/dist/uns/uns-attributes.d.ts +6 -6
  47. package/dist/uns/uns-attributes.js +9 -9
  48. package/dist/uns/uns-attributes.js.map +1 -1
  49. package/dist/uns/uns-dictionary-registry.d.ts +4 -4
  50. package/dist/uns/uns-dictionary-registry.js +20 -20
  51. package/dist/uns/uns-dictionary-registry.js.map +1 -1
  52. package/dist/uns/uns-dictionary.generated.d.ts +663 -663
  53. package/dist/uns/uns-dictionary.generated.js +745 -745
  54. package/dist/uns/uns-dictionary.generated.js.map +1 -1
  55. package/dist/uns/uns-event-emitter.js.map +1 -1
  56. package/dist/uns/uns-interfaces.d.ts +365 -365
  57. package/dist/uns/uns-interfaces.js.map +1 -1
  58. package/dist/uns/uns-measurements.generated.d.ts +362 -362
  59. package/dist/uns/uns-measurements.generated.js +359 -359
  60. package/dist/uns/uns-measurements.generated.js.map +1 -1
  61. package/dist/uns/uns-measurements.js.map +1 -1
  62. package/dist/uns/uns-object.d.ts +20 -20
  63. package/dist/uns/uns-object.js +9 -9
  64. package/dist/uns/uns-object.js.map +1 -1
  65. package/dist/uns/uns-packet.js +263 -263
  66. package/dist/uns/uns-packet.js.map +1 -1
  67. package/dist/uns/uns-path.d.ts +2 -2
  68. package/dist/uns/uns-path.js +10 -10
  69. package/dist/uns/uns-path.js.map +1 -1
  70. package/dist/uns/uns-proxy-process.d.ts +11 -0
  71. package/dist/uns/uns-proxy-process.d.ts.map +1 -1
  72. package/dist/uns/uns-proxy-process.js +224 -203
  73. package/dist/uns/uns-proxy-process.js.map +1 -1
  74. package/dist/uns/uns-proxy.d.ts +51 -51
  75. package/dist/uns/uns-proxy.js +190 -190
  76. package/dist/uns/uns-proxy.js.map +1 -1
  77. package/dist/uns/uns-tags.js.map +1 -1
  78. package/dist/uns/uns-topic-matcher.js.map +1 -1
  79. package/dist/uns/uns-topics.js.map +1 -1
  80. package/dist/uns-config/config-schema.js.map +1 -1
  81. package/dist/uns-config/host-placeholders.js.map +1 -1
  82. package/dist/uns-config/schema-tolls.js.map +1 -1
  83. package/dist/uns-config/schema-tools.js.map +1 -1
  84. package/dist/uns-config/secret-placeholders.js.map +1 -1
  85. package/dist/uns-config/secret-resolver.js.map +1 -1
  86. package/dist/uns-config/uns-core-schema.d.ts +3276 -3276
  87. package/dist/uns-config/uns-core-schema.js.map +1 -1
  88. package/dist/uns-grpc/uns-gateway-cli.js.map +1 -1
  89. package/dist/uns-grpc/uns-gateway-server.js.map +1 -1
  90. package/dist/uns-mqtt/mqtt-interfaces.js.map +1 -1
  91. package/dist/uns-mqtt/mqtt-proxy.js.map +1 -1
  92. package/dist/uns-mqtt/mqtt-topic-builder.d.ts +6 -0
  93. package/dist/uns-mqtt/mqtt-topic-builder.d.ts.map +1 -1
  94. package/dist/uns-mqtt/mqtt-topic-builder.js +8 -0
  95. package/dist/uns-mqtt/mqtt-topic-builder.js.map +1 -1
  96. package/dist/uns-mqtt/mqtt-worker-init.js.map +1 -1
  97. package/dist/uns-mqtt/mqtt-worker.js.map +1 -1
  98. package/dist/uns-mqtt/throttled-queue.js.map +1 -1
  99. package/dist/uns-mqtt/uns-mqtt-proxy.js +477 -477
  100. package/dist/uns-mqtt/uns-mqtt-proxy.js.map +1 -1
  101. package/dist/uns-mqtt/ws-proxy.js.map +1 -1
  102. package/package.json +1 -1
@@ -1,478 +1,478 @@
1
- import { readFileSync } from "fs";
2
- import * as path from "path";
3
- import { Worker } from "worker_threads";
4
- import { fileURLToPath } from "url";
5
- import { basePath } from "../base-path.js";
6
- import logger from "../logger.js";
7
- import { getObjectTypeDescription } from "../uns/uns-object.js";
8
- import { UnsPacket } from "../uns/uns-packet.js";
9
- import { MqttTopicBuilder } from "./mqtt-topic-builder.js";
10
- import UnsProxy from "../uns/uns-proxy.js";
11
- import { UnsAttributeType } from "../graphql/schema.js";
12
- import { getAttributeDescription } from "../uns/uns-attributes.js";
13
- const packageJsonPath = path.join(basePath, "package.json");
14
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
15
- const moduleDirectory = path.dirname(fileURLToPath(import.meta.url));
16
- const packageRoot = path.resolve(moduleDirectory, "..", "..");
17
- const workerScriptPath = path.join(packageRoot, "dist/uns-mqtt/mqtt-worker-init.js");
18
- export var MessageMode;
19
- (function (MessageMode) {
20
- MessageMode["Raw"] = "raw";
21
- MessageMode["Delta"] = "delta";
22
- MessageMode["Both"] = "both"; // Send both the original and delta messages
23
- })(MessageMode || (MessageMode = {}));
24
- export default class UnsMqttProxy extends UnsProxy {
25
- lastValues = new Map();
26
- worker;
27
- pendingEnqueues = new Map();
28
- unsParameters;
29
- processStatusTopic;
30
- instanceName;
31
- currentSequenceId = new Map();
32
- topicBuilder;
33
- constructor(mqttHost, processName, instanceName, unsParameters, publisherActive = false, subscriberActive = false) {
34
- super();
35
- this.instanceName = instanceName;
36
- // Create the topic builder using packageJson values and the processName.
37
- this.topicBuilder = new MqttTopicBuilder(`uns-infra/${MqttTopicBuilder.sanitizeTopicPart(packageJson.name)}/${MqttTopicBuilder.sanitizeTopicPart(packageJson.version)}/${MqttTopicBuilder.sanitizeTopicPart(processName)}/`);
38
- // Generate the processStatusTopic using the builder.
39
- this.processStatusTopic = this.topicBuilder.getProcessStatusTopic();
40
- // Derive the instanceStatusTopic by appending the instance name.
41
- this.instanceStatusTopic = this.processStatusTopic + instanceName + "/";
42
- // Concatenate processName with instanceName for the worker identification.
43
- this.instanceNameWithSuffix = `${processName}-${instanceName}`;
44
- const mqttParameters = {
45
- mqttSubToTopics: unsParameters?.mqttSubToTopics ?? [],
46
- username: unsParameters?.username ?? "",
47
- password: unsParameters?.password ?? "",
48
- mqttSSL: unsParameters?.mqttSSL ?? false,
49
- statusTopic: this.instanceStatusTopic,
50
- rejectUnauthorized: unsParameters?.rejectUnauthorized ?? false,
51
- clientId: unsParameters?.clientId,
52
- hosts: unsParameters?.hosts,
53
- servers: unsParameters?.servers,
54
- port: unsParameters?.port,
55
- protocol: unsParameters?.protocol,
56
- keepalive: unsParameters?.keepalive,
57
- clean: unsParameters?.clean,
58
- connectTimeout: unsParameters?.connectTimeout,
59
- reconnectPeriod: unsParameters?.reconnectPeriod,
60
- reconnectOnConnackError: unsParameters?.reconnectOnConnackError,
61
- resubscribe: unsParameters?.resubscribe,
62
- queueQoSZero: unsParameters?.queueQoSZero,
63
- properties: unsParameters?.properties,
64
- ca: unsParameters?.ca,
65
- cert: unsParameters?.cert,
66
- key: unsParameters?.key,
67
- servername: unsParameters?.servername,
68
- };
69
- this.unsParameters = unsParameters ?? {};
70
- this.startQueueWorker(mqttHost, this.instanceNameWithSuffix, mqttParameters, publisherActive, subscriberActive);
71
- }
72
- /**
73
- * Resolve object identity from explicit fields or the tail of the topic path.
74
- * Falls back to parsing when not provided for backward compatibility.
75
- */
76
- resolveObjectIdentity(msg) {
77
- const providedType = msg.objectType;
78
- const providedId = msg.objectId;
79
- const providedAsset = msg.asset;
80
- const topicParts = msg.topic.split("/").filter((part) => part.length > 0);
81
- const hasObjectTail = topicParts.length >= 2;
82
- const parsedType = hasObjectTail ? topicParts[topicParts.length - 2] : undefined;
83
- const parsedId = hasObjectTail ? topicParts[topicParts.length - 1] : undefined;
84
- const parsedAsset = hasObjectTail
85
- ? (topicParts.length >= 3 ? topicParts[topicParts.length - 3] : undefined)
86
- : (topicParts.length >= 1 ? topicParts[topicParts.length - 1] : undefined);
87
- const objectType = providedType ?? parsedType;
88
- const objectId = providedId ?? parsedId ?? "main";
89
- const asset = providedAsset ?? parsedAsset;
90
- // If values are provided, trust them; otherwise derive from topic.
91
- if (!providedType || !providedId) {
92
- if (parsedType && parsedId) {
93
- logger.warn(`${this.instanceNameWithSuffix} - objectType/objectId missing; derived from topic tail ${parsedType}/${parsedId}`);
94
- }
95
- else {
96
- logger.warn(`${this.instanceNameWithSuffix} - objectType/objectId missing; defaulting objectId to 'main' for topic '${msg.topic}'. Expected topic to end with '<objectType>/<objectId>/'`);
97
- }
98
- }
99
- // Asset is optional; no warning on mismatch to avoid noisy logs when base topics don't carry it.
100
- msg.objectType = objectType;
101
- msg.objectId = objectId;
102
- msg.asset = asset;
103
- return { objectType, objectId, asset };
104
- }
105
- /**
106
- * Ensure the topic ends with a trailing slash for attribute concatenation.
107
- */
108
- normalizeTopicWithObject(topic) {
109
- return topic.endsWith("/") ? topic : `${topic}/`;
110
- }
111
- /**
112
- * Starts a worker thread to process the throttled publish queue.
113
- */
114
- startQueueWorker(mqttHost, instanceNameWithSuffix, mqttParameters, publisherActive, subscriberActive) {
115
- const workerData = {
116
- publishThrottlingDelay: this.unsParameters.publishThrottlingDelay ?? 1,
117
- subscribeThrottlingDelay: this.unsParameters.subscribeThrottlingDelay ?? 1,
118
- persistToDisk: false,
119
- mqttHost: mqttHost,
120
- instanceNameWithSuffix: instanceNameWithSuffix,
121
- mqttParameters: mqttParameters,
122
- publisherActive,
123
- subscriberActive,
124
- defaultPublishOptions: this.unsParameters.defaultPublishOptions,
125
- };
126
- this.worker = new Worker(workerScriptPath, { workerData });
127
- this.worker.on("message", (msg) => {
128
- if (msg && msg.command === "enqueueResult" && msg.id) {
129
- const pending = this.pendingEnqueues.get(msg.id);
130
- if (pending) {
131
- if (msg.status === "success" && msg.topic && msg.message) {
132
- pending.resolve();
133
- }
134
- else {
135
- pending.reject(new Error(msg.error));
136
- }
137
- this.pendingEnqueues.delete(msg.id);
138
- }
139
- }
140
- else if (msg && msg.command === "input") {
141
- this.event.emit("input", { topic: msg.topic, message: msg.message.toString(), packet: msg.packet });
142
- }
143
- else if (msg && (msg.command === "handover_subscriber" || msg.command === "handover_publisher")) {
144
- this.event.emit("mqttWorker", { command: msg.command, batchSize: msg.batchSize, referenceHash: msg.referenceHash, instanceName: this.instanceName });
145
- }
146
- else if (msg && msg.command === "mqttProxyStatus") {
147
- this.event.emit("mqttProxyStatus", { event: msg.event, value: msg.value, uom: msg.uom, statusTopic: msg.statusTopic });
148
- }
149
- });
150
- this.worker.on("error", (err) => {
151
- logger.error("Error in worker:", err);
152
- const reason = err instanceof Error ? err : new Error(String(err));
153
- for (const pending of this.pendingEnqueues.values()) {
154
- pending.reject(reason);
155
- }
156
- this.pendingEnqueues.clear();
157
- });
158
- this.worker.on("exit", (code) => {
159
- if (code !== 0) {
160
- logger.error(`Worker exited with code ${code}`);
161
- const reason = new Error(`MQTT worker exited with code ${code}`);
162
- for (const pending of this.pendingEnqueues.values()) {
163
- pending.reject(reason);
164
- }
165
- this.pendingEnqueues.clear();
166
- }
167
- });
168
- }
169
- /**
170
- * Enqueues a message to the worker queue.
171
- *
172
- * @param topic - The topic to which the message belongs.
173
- * @param message - The message to be enqueued.
174
- * @param options - Optional publish options.
175
- * @returns A promise that resolves when the message is successfully enqueued.
176
- */
177
- async enqueueMessageToWorkerQueue(topic, message, options) {
178
- return new Promise((resolve, reject) => {
179
- // const id: string = String(this.currentSequenceId.get(topic) ?? 0);
180
- const id = `${Date.now()}-${Math.random()}`;
181
- this.pendingEnqueues.set(id, { resolve, reject });
182
- this.worker.postMessage({ command: "enqueue", id, topic, message, options });
183
- });
184
- }
185
- /**
186
- * Sets the publisher active state.
187
- *
188
- * @param batchSize - Optional batch size.
189
- * @param referenceHash - Optional reference hash.
190
- */
191
- setPublisherActive(batchSize, referenceHash) {
192
- this.worker.postMessage({ command: "setPublisherActive", batchSize, referenceHash });
193
- }
194
- /**
195
- * Sets the publisher to passive mode.
196
- * @returns A promise that resolves when the publisher is set to passive.
197
- */
198
- setPublisherPassive() {
199
- this.worker.postMessage({ command: "setPublisherPassive" });
200
- return new Promise((resolve) => {
201
- this.event.on("mqttWorker", (msg) => {
202
- if (msg.command === "handover_publisher") {
203
- logger.info(`${this.instanceNameWithSuffix} - Publisher set to passive.`);
204
- resolve(msg);
205
- }
206
- });
207
- });
208
- }
209
- /**
210
- * Sets the subscriber active state.
211
- *
212
- * @param batchSize - Optional batch size.
213
- * @param referenceHash - Optional reference hash.
214
- */
215
- setSubscriberActive(batchSize, referenceHash) {
216
- this.worker.postMessage({ command: "setSubscriberActive", batchSize, referenceHash });
217
- }
218
- /**
219
- * Sets the subscriber to passive mode.
220
- * @returns A promise that resolves when the subscriber is set to passive.
221
- */
222
- setSubscriberPassive() {
223
- this.worker.postMessage({ command: "setSubscriberPassive" });
224
- return new Promise((resolve) => {
225
- this.event.on("mqttWorker", (msg) => {
226
- if (msg.command === "handover_subscriber") {
227
- logger.info(`${this.instanceNameWithSuffix} - Publisher set to passive.`);
228
- resolve(msg);
229
- }
230
- });
231
- });
232
- }
233
- /**
234
- * Sets the subscriber to passive mode and allows the publisher to run
235
- * until the queue is empty (all messages are processed).
236
- */
237
- async setSubscriberPassiveAndDrainQueue() {
238
- return new Promise(async (resolve) => {
239
- const mqttWorkerData = await this.setSubscriberPassive();
240
- while (this.pendingEnqueues.size > 0) {
241
- await new Promise((resolve) => setTimeout(resolve, 100)); // Poll every 100ms
242
- }
243
- logger.info(`${this.instanceNameWithSuffix} - Subscriber set to passive and queue drained.`);
244
- resolve(mqttWorkerData);
245
- });
246
- }
247
- /**
248
- * Processes and publishes MQTT messages based on the selected message mode.
249
- *
250
- * @param mqttMessage - The MQTT message object.
251
- * @param mode - The message mode (Raw, Delta, or Both).
252
- */
253
- async publishMqttMessage(mqttMessage, mode = MessageMode.Raw) {
254
- if (!mqttMessage) {
255
- logger.error(`${this.instanceNameWithSuffix} - Error publishing mqtt message: mqttMessage must be defined.`);
256
- return;
257
- }
258
- const attrs = Array.isArray(mqttMessage.attributes)
259
- ? mqttMessage.attributes
260
- : [mqttMessage.attributes];
261
- const { topic, asset, assetDescription, objectType, objectTypeDescription, objectId } = mqttMessage;
262
- for (const attrEntry of attrs) {
263
- const attrDescription = attrEntry.description ?? getAttributeDescription(attrEntry.attribute);
264
- const message = "message" in attrEntry && attrEntry.message
265
- ? attrEntry.message
266
- : "data" in attrEntry && attrEntry.data
267
- ? { data: attrEntry.data, createdAt: attrEntry.createdAt, expiresAt: attrEntry.expiresAt }
268
- : "table" in attrEntry && attrEntry.table
269
- ? { table: attrEntry.table, createdAt: attrEntry.createdAt, expiresAt: attrEntry.expiresAt }
270
- : (() => { throw new Error("Attribute entry must include exactly one of data/table/message"); })();
271
- const packet = await UnsPacket.unsPacketFromUnsMessage(message);
272
- const singleMsg = {
273
- topic,
274
- asset,
275
- assetDescription,
276
- objectType,
277
- objectTypeDescription,
278
- objectId,
279
- attribute: attrEntry.attribute,
280
- description: attrDescription,
281
- tags: attrEntry.tags,
282
- attributeNeedsPersistence: attrEntry.attributeNeedsPersistence,
283
- ...(attrEntry.validityMode ? { validityMode: attrEntry.validityMode } : {}),
284
- ...(attrEntry.expectedIntervalMs ? { expectedIntervalMs: attrEntry.expectedIntervalMs } : {}),
285
- ...(attrEntry.lifecycleEndValue ? { lifecycleEndValue: attrEntry.lifecycleEndValue } : {}),
286
- packet,
287
- };
288
- // existing single-attribute flow
289
- const baseDescription = singleMsg.description ??
290
- getAttributeDescription(singleMsg.attribute) ??
291
- singleMsg.attribute;
292
- const mqttMessageWithDesc = { ...singleMsg, description: baseDescription };
293
- const time = UnsPacket.formatToISO8601(new Date());
294
- switch (mode) {
295
- case MessageMode.Raw: {
296
- await this.processAndEnqueueMessage(mqttMessageWithDesc, time, false);
297
- break;
298
- }
299
- case MessageMode.Delta: {
300
- const deltaMessage = { ...mqttMessageWithDesc };
301
- deltaMessage.attribute = `${mqttMessageWithDesc.attribute}-delta`;
302
- deltaMessage.description = `${baseDescription ?? ""} (delta)`;
303
- await this.processAndEnqueueMessage(deltaMessage, time, true);
304
- break;
305
- }
306
- case MessageMode.Both: {
307
- await this.processAndEnqueueMessage(mqttMessageWithDesc, time, false);
308
- const deltaMessageBoth = { ...mqttMessageWithDesc };
309
- deltaMessageBoth.attribute = `${mqttMessageWithDesc.attribute}-delta`;
310
- deltaMessageBoth.description = `${baseDescription ?? ""} (delta)`;
311
- await this.processAndEnqueueMessage(deltaMessageBoth, time, true);
312
- break;
313
- }
314
- }
315
- }
316
- return;
317
- }
318
- /**
319
- * Publishes a message to a specified topic.
320
- *
321
- * @param topic - The MQTT topic.
322
- * @param message - The message to publish.
323
- * @returns A promise that resolves when enqueued.
324
- */
325
- publishMessage(topic, message, options) {
326
- return this.enqueueMessageToWorkerQueue(topic, message, options);
327
- }
328
- /**
329
- * Parses an MQTT packet from a JSON string.
330
- *
331
- * @param mqttPacket - The MQTT packet string.
332
- * @returns A parsed IUnsPacket object or null.
333
- */
334
- parseMqttPacket(mqttPacket) {
335
- return UnsPacket.parseMqttPacket(mqttPacket, this.instanceNameWithSuffix);
336
- }
337
- /**
338
- * Subscribes asynchronously to one or more topics.
339
- *
340
- * @param topics - A topic or list of topics.
341
- */
342
- subscribeAsync(topics) {
343
- this.worker.postMessage({ command: "subscribeAsync", topics });
344
- }
345
- /**
346
- * Unsubscribes asynchronously from the given topics.
347
- *
348
- * @param topics - A list of topics.
349
- */
350
- unsubscribeAsync(topics) {
351
- this.worker.postMessage({ command: "unsubscribeAsync", topics });
352
- }
353
- /**
354
- * Processes and enqueues a message to the worker queue, including handling
355
- * sequencing, value differences, and tracking of unique topics.
356
- *
357
- * @param msg - The MQTT message to process.
358
- * @param time - The timestamp.
359
- * @param valueIsCumulative - Whether the value is cumulative.
360
- */
361
- async processAndEnqueueMessage(msg, time, valueIsCumulative = false) {
362
- try {
363
- const attributeType = msg.packet.message.data ? UnsAttributeType.Data :
364
- msg.packet.message.table ? UnsAttributeType.Table : null;
365
- let dataGroup = "";
366
- if (attributeType == UnsAttributeType.Data)
367
- dataGroup = msg.packet.message.data.dataGroup ?? "";
368
- if (attributeType == UnsAttributeType.Table)
369
- dataGroup = msg.packet.message.table.dataGroup ?? "";
370
- const { objectType, objectId, asset } = this.resolveObjectIdentity(msg);
371
- const normalizedTopic = this.normalizeTopicWithObject(msg.topic);
372
- msg.topic = normalizedTopic;
373
- const description = msg.description ?? getAttributeDescription(msg.attribute) ?? "";
374
- const objectTypeDescription = msg.objectTypeDescription ?? (objectType ? getObjectTypeDescription(objectType) : undefined);
375
- // Runtime validation for validity fields
376
- if (msg.validityMode) {
377
- const validModes = new Set(["interval", "lifecycle", "static"]);
378
- if (!validModes.has(msg.validityMode)) {
379
- logger.warn(`${this.instanceNameWithSuffix} - Invalid validityMode "${msg.validityMode}" for attribute "${msg.attribute}". Expected: interval | lifecycle | static.`);
380
- }
381
- if (msg.expectedIntervalMs && msg.validityMode !== "interval") {
382
- logger.warn(`${this.instanceNameWithSuffix} - expectedIntervalMs is set but validityMode is "${msg.validityMode}" (only used with "interval") for attribute "${msg.attribute}".`);
383
- }
384
- if (msg.lifecycleEndValue && msg.validityMode !== "lifecycle") {
385
- logger.warn(`${this.instanceNameWithSuffix} - lifecycleEndValue is set but validityMode is "${msg.validityMode}" (only used with "lifecycle") for attribute "${msg.attribute}".`);
386
- }
387
- if (msg.validityMode === "interval" && !msg.expectedIntervalMs) {
388
- logger.debug(`${this.instanceNameWithSuffix} - validityMode "interval" without expectedIntervalMs for attribute "${msg.attribute}" — controller will use its default.`);
389
- }
390
- }
391
- this.registerUniqueTopic({
392
- timestamp: time,
393
- topic: msg.topic,
394
- attribute: msg.attribute,
395
- attributeType: attributeType,
396
- description,
397
- tags: msg.tags,
398
- attributeNeedsPersistence: msg.attributeNeedsPersistence,
399
- dataGroup,
400
- asset,
401
- assetDescription: msg.assetDescription,
402
- objectType,
403
- objectTypeDescription,
404
- objectId,
405
- ...(msg.validityMode ? { validityMode: msg.validityMode } : {}),
406
- ...(msg.expectedIntervalMs ? { expectedIntervalMs: msg.expectedIntervalMs } : {}),
407
- ...(msg.lifecycleEndValue ? { lifecycleEndValue: msg.lifecycleEndValue } : {}),
408
- });
409
- const publishTopic = `${msg.topic}${asset ? `${asset}/` : ""}${objectType ? `${objectType}/` : ""}${objectId ? `${objectId}/` : ""}${msg.attribute}`;
410
- const sequenceId = this.currentSequenceId.get(msg.topic) ?? 0;
411
- this.currentSequenceId.set(msg.topic, sequenceId + 1);
412
- msg.packet.sequenceId = sequenceId;
413
- if (msg.packet.message.data) {
414
- const newValue = msg.packet.message.data.value;
415
- const newUom = msg.packet.message.data.uom;
416
- const lastValueEntry = this.lastValues.get(publishTopic);
417
- const currentTime = new Date(msg.packet.message.data.time);
418
- if (lastValueEntry) {
419
- const intervalBetweenMessages = currentTime.getTime() - lastValueEntry.timestamp.getTime();
420
- const lastValue = lastValueEntry.value;
421
- this.lastValues.set(publishTopic, { value: newValue, uom: newUom, timestamp: currentTime });
422
- // Compute the delta and manage cumulative resets
423
- if (valueIsCumulative == true && typeof newValue === "number" && typeof lastValue === "number") {
424
- // Skip if newValue is 0 (likely a glitch)
425
- if (newValue === 0) {
426
- return; // Don't process or enqueue
427
- }
428
- const delta = newValue - lastValue;
429
- msg.packet.message.data.value = delta < 0 ? newValue : delta;
430
- }
431
- msg.packet.interval = intervalBetweenMessages;
432
- await this.enqueueMessageToWorkerQueue(publishTopic, JSON.stringify(msg.packet));
433
- }
434
- else {
435
- this.lastValues.set(publishTopic, { value: newValue, uom: newUom, timestamp: currentTime });
436
- logger.debug(`${this.instanceNameWithSuffix} - Need one more packet to calculate interval on topic ${publishTopic}`);
437
- if (valueIsCumulative === false) {
438
- await this.enqueueMessageToWorkerQueue(publishTopic, JSON.stringify(msg.packet));
439
- }
440
- else {
441
- logger.debug(`${this.instanceNameWithSuffix} - Need one more packet to calculate difference on value in data for topic ${publishTopic}`);
442
- }
443
- }
444
- }
445
- else if (msg.packet.message.table) {
446
- await this.enqueueMessageToWorkerQueue(publishTopic, JSON.stringify(msg.packet));
447
- }
448
- else {
449
- logger.error(`${this.instanceNameWithSuffix} - Error publishing message to topic ${publishTopic}: packet.message must include data or table`);
450
- }
451
- }
452
- catch (error) {
453
- logger.error(`${this.instanceNameWithSuffix} - Error publishing message to topic ${msg.topic}${msg.attribute}: ${error.message}`);
454
- }
455
- }
456
- /**
457
- * Stops the UnsProxy instance and cleans up resources.
458
- */
459
- async stop() {
460
- super.stop();
461
- // Terminate the worker thread if it exists.
462
- if (this.worker) {
463
- try {
464
- const exitCode = await this.worker.terminate();
465
- logger.info(`${this.instanceNameWithSuffix} - Worker terminated with exit code ${exitCode}`);
466
- }
467
- catch (error) {
468
- logger.error(`${this.instanceNameWithSuffix} - Error terminating worker: ${error.message}`);
469
- }
470
- }
471
- // Optionally, handle any pending enqueues.
472
- for (const [id, pending] of this.pendingEnqueues) {
473
- pending.reject(new Error("UnsProxy has been stopped"));
474
- this.pendingEnqueues.delete(id);
475
- }
476
- }
477
- }
1
+ import { readFileSync } from "fs";
2
+ import * as path from "path";
3
+ import { Worker } from "worker_threads";
4
+ import { fileURLToPath } from "url";
5
+ import { basePath } from "../base-path.js";
6
+ import logger from "../logger.js";
7
+ import { getObjectTypeDescription } from "../uns/uns-object.js";
8
+ import { UnsPacket } from "../uns/uns-packet.js";
9
+ import { MqttTopicBuilder } from "./mqtt-topic-builder.js";
10
+ import UnsProxy from "../uns/uns-proxy.js";
11
+ import { UnsAttributeType } from "../graphql/schema.js";
12
+ import { getAttributeDescription } from "../uns/uns-attributes.js";
13
+ const packageJsonPath = path.join(basePath, "package.json");
14
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
15
+ const moduleDirectory = path.dirname(fileURLToPath(import.meta.url));
16
+ const packageRoot = path.resolve(moduleDirectory, "..", "..");
17
+ const workerScriptPath = path.join(packageRoot, "dist/uns-mqtt/mqtt-worker-init.js");
18
+ export var MessageMode;
19
+ (function (MessageMode) {
20
+ MessageMode["Raw"] = "raw";
21
+ MessageMode["Delta"] = "delta";
22
+ MessageMode["Both"] = "both"; // Send both the original and delta messages
23
+ })(MessageMode || (MessageMode = {}));
24
+ export default class UnsMqttProxy extends UnsProxy {
25
+ lastValues = new Map();
26
+ worker;
27
+ pendingEnqueues = new Map();
28
+ unsParameters;
29
+ processStatusTopic;
30
+ instanceName;
31
+ currentSequenceId = new Map();
32
+ topicBuilder;
33
+ constructor(mqttHost, processName, instanceName, unsParameters, publisherActive = false, subscriberActive = false) {
34
+ super();
35
+ this.instanceName = instanceName;
36
+ // Create the topic builder using packageJson values and the processName.
37
+ this.topicBuilder = new MqttTopicBuilder(`uns-infra/${MqttTopicBuilder.sanitizeTopicPart(packageJson.name)}/${MqttTopicBuilder.sanitizeTopicPart(packageJson.version)}/${MqttTopicBuilder.sanitizeTopicPart(processName)}/`);
38
+ // Generate the processStatusTopic using the builder.
39
+ this.processStatusTopic = this.topicBuilder.getProcessStatusTopic();
40
+ // Derive the instanceStatusTopic by appending the instance name.
41
+ this.instanceStatusTopic = this.processStatusTopic + instanceName + "/";
42
+ // Concatenate processName with instanceName for the worker identification.
43
+ this.instanceNameWithSuffix = `${processName}-${instanceName}`;
44
+ const mqttParameters = {
45
+ mqttSubToTopics: unsParameters?.mqttSubToTopics ?? [],
46
+ username: unsParameters?.username ?? "",
47
+ password: unsParameters?.password ?? "",
48
+ mqttSSL: unsParameters?.mqttSSL ?? false,
49
+ statusTopic: this.instanceStatusTopic,
50
+ rejectUnauthorized: unsParameters?.rejectUnauthorized ?? false,
51
+ clientId: unsParameters?.clientId,
52
+ hosts: unsParameters?.hosts,
53
+ servers: unsParameters?.servers,
54
+ port: unsParameters?.port,
55
+ protocol: unsParameters?.protocol,
56
+ keepalive: unsParameters?.keepalive,
57
+ clean: unsParameters?.clean,
58
+ connectTimeout: unsParameters?.connectTimeout,
59
+ reconnectPeriod: unsParameters?.reconnectPeriod,
60
+ reconnectOnConnackError: unsParameters?.reconnectOnConnackError,
61
+ resubscribe: unsParameters?.resubscribe,
62
+ queueQoSZero: unsParameters?.queueQoSZero,
63
+ properties: unsParameters?.properties,
64
+ ca: unsParameters?.ca,
65
+ cert: unsParameters?.cert,
66
+ key: unsParameters?.key,
67
+ servername: unsParameters?.servername,
68
+ };
69
+ this.unsParameters = unsParameters ?? {};
70
+ this.startQueueWorker(mqttHost, this.instanceNameWithSuffix, mqttParameters, publisherActive, subscriberActive);
71
+ }
72
+ /**
73
+ * Resolve object identity from explicit fields or the tail of the topic path.
74
+ * Falls back to parsing when not provided for backward compatibility.
75
+ */
76
+ resolveObjectIdentity(msg) {
77
+ const providedType = msg.objectType;
78
+ const providedId = msg.objectId;
79
+ const providedAsset = msg.asset;
80
+ const topicParts = msg.topic.split("/").filter((part) => part.length > 0);
81
+ const hasObjectTail = topicParts.length >= 2;
82
+ const parsedType = hasObjectTail ? topicParts[topicParts.length - 2] : undefined;
83
+ const parsedId = hasObjectTail ? topicParts[topicParts.length - 1] : undefined;
84
+ const parsedAsset = hasObjectTail
85
+ ? (topicParts.length >= 3 ? topicParts[topicParts.length - 3] : undefined)
86
+ : (topicParts.length >= 1 ? topicParts[topicParts.length - 1] : undefined);
87
+ const objectType = providedType ?? parsedType;
88
+ const objectId = providedId ?? parsedId ?? "main";
89
+ const asset = providedAsset ?? parsedAsset;
90
+ // If values are provided, trust them; otherwise derive from topic.
91
+ if (!providedType || !providedId) {
92
+ if (parsedType && parsedId) {
93
+ logger.warn(`${this.instanceNameWithSuffix} - objectType/objectId missing; derived from topic tail ${parsedType}/${parsedId}`);
94
+ }
95
+ else {
96
+ logger.warn(`${this.instanceNameWithSuffix} - objectType/objectId missing; defaulting objectId to 'main' for topic '${msg.topic}'. Expected topic to end with '<objectType>/<objectId>/'`);
97
+ }
98
+ }
99
+ // Asset is optional; no warning on mismatch to avoid noisy logs when base topics don't carry it.
100
+ msg.objectType = objectType;
101
+ msg.objectId = objectId;
102
+ msg.asset = asset;
103
+ return { objectType, objectId, asset };
104
+ }
105
+ /**
106
+ * Ensure the topic ends with a trailing slash for attribute concatenation.
107
+ */
108
+ normalizeTopicWithObject(topic) {
109
+ return topic.endsWith("/") ? topic : `${topic}/`;
110
+ }
111
+ /**
112
+ * Starts a worker thread to process the throttled publish queue.
113
+ */
114
+ startQueueWorker(mqttHost, instanceNameWithSuffix, mqttParameters, publisherActive, subscriberActive) {
115
+ const workerData = {
116
+ publishThrottlingDelay: this.unsParameters.publishThrottlingDelay ?? 1,
117
+ subscribeThrottlingDelay: this.unsParameters.subscribeThrottlingDelay ?? 1,
118
+ persistToDisk: false,
119
+ mqttHost: mqttHost,
120
+ instanceNameWithSuffix: instanceNameWithSuffix,
121
+ mqttParameters: mqttParameters,
122
+ publisherActive,
123
+ subscriberActive,
124
+ defaultPublishOptions: this.unsParameters.defaultPublishOptions,
125
+ };
126
+ this.worker = new Worker(workerScriptPath, { workerData });
127
+ this.worker.on("message", (msg) => {
128
+ if (msg && msg.command === "enqueueResult" && msg.id) {
129
+ const pending = this.pendingEnqueues.get(msg.id);
130
+ if (pending) {
131
+ if (msg.status === "success" && msg.topic && msg.message) {
132
+ pending.resolve();
133
+ }
134
+ else {
135
+ pending.reject(new Error(msg.error));
136
+ }
137
+ this.pendingEnqueues.delete(msg.id);
138
+ }
139
+ }
140
+ else if (msg && msg.command === "input") {
141
+ this.event.emit("input", { topic: msg.topic, message: msg.message.toString(), packet: msg.packet });
142
+ }
143
+ else if (msg && (msg.command === "handover_subscriber" || msg.command === "handover_publisher")) {
144
+ this.event.emit("mqttWorker", { command: msg.command, batchSize: msg.batchSize, referenceHash: msg.referenceHash, instanceName: this.instanceName });
145
+ }
146
+ else if (msg && msg.command === "mqttProxyStatus") {
147
+ this.event.emit("mqttProxyStatus", { event: msg.event, value: msg.value, uom: msg.uom, statusTopic: msg.statusTopic });
148
+ }
149
+ });
150
+ this.worker.on("error", (err) => {
151
+ logger.error("Error in worker:", err);
152
+ const reason = err instanceof Error ? err : new Error(String(err));
153
+ for (const pending of this.pendingEnqueues.values()) {
154
+ pending.reject(reason);
155
+ }
156
+ this.pendingEnqueues.clear();
157
+ });
158
+ this.worker.on("exit", (code) => {
159
+ if (code !== 0) {
160
+ logger.error(`Worker exited with code ${code}`);
161
+ const reason = new Error(`MQTT worker exited with code ${code}`);
162
+ for (const pending of this.pendingEnqueues.values()) {
163
+ pending.reject(reason);
164
+ }
165
+ this.pendingEnqueues.clear();
166
+ }
167
+ });
168
+ }
169
+ /**
170
+ * Enqueues a message to the worker queue.
171
+ *
172
+ * @param topic - The topic to which the message belongs.
173
+ * @param message - The message to be enqueued.
174
+ * @param options - Optional publish options.
175
+ * @returns A promise that resolves when the message is successfully enqueued.
176
+ */
177
+ async enqueueMessageToWorkerQueue(topic, message, options) {
178
+ return new Promise((resolve, reject) => {
179
+ // const id: string = String(this.currentSequenceId.get(topic) ?? 0);
180
+ const id = `${Date.now()}-${Math.random()}`;
181
+ this.pendingEnqueues.set(id, { resolve, reject });
182
+ this.worker.postMessage({ command: "enqueue", id, topic, message, options });
183
+ });
184
+ }
185
+ /**
186
+ * Sets the publisher active state.
187
+ *
188
+ * @param batchSize - Optional batch size.
189
+ * @param referenceHash - Optional reference hash.
190
+ */
191
+ setPublisherActive(batchSize, referenceHash) {
192
+ this.worker.postMessage({ command: "setPublisherActive", batchSize, referenceHash });
193
+ }
194
+ /**
195
+ * Sets the publisher to passive mode.
196
+ * @returns A promise that resolves when the publisher is set to passive.
197
+ */
198
+ setPublisherPassive() {
199
+ this.worker.postMessage({ command: "setPublisherPassive" });
200
+ return new Promise((resolve) => {
201
+ this.event.on("mqttWorker", (msg) => {
202
+ if (msg.command === "handover_publisher") {
203
+ logger.info(`${this.instanceNameWithSuffix} - Publisher set to passive.`);
204
+ resolve(msg);
205
+ }
206
+ });
207
+ });
208
+ }
209
+ /**
210
+ * Sets the subscriber active state.
211
+ *
212
+ * @param batchSize - Optional batch size.
213
+ * @param referenceHash - Optional reference hash.
214
+ */
215
+ setSubscriberActive(batchSize, referenceHash) {
216
+ this.worker.postMessage({ command: "setSubscriberActive", batchSize, referenceHash });
217
+ }
218
+ /**
219
+ * Sets the subscriber to passive mode.
220
+ * @returns A promise that resolves when the subscriber is set to passive.
221
+ */
222
+ setSubscriberPassive() {
223
+ this.worker.postMessage({ command: "setSubscriberPassive" });
224
+ return new Promise((resolve) => {
225
+ this.event.on("mqttWorker", (msg) => {
226
+ if (msg.command === "handover_subscriber") {
227
+ logger.info(`${this.instanceNameWithSuffix} - Publisher set to passive.`);
228
+ resolve(msg);
229
+ }
230
+ });
231
+ });
232
+ }
233
+ /**
234
+ * Sets the subscriber to passive mode and allows the publisher to run
235
+ * until the queue is empty (all messages are processed).
236
+ */
237
+ async setSubscriberPassiveAndDrainQueue() {
238
+ return new Promise(async (resolve) => {
239
+ const mqttWorkerData = await this.setSubscriberPassive();
240
+ while (this.pendingEnqueues.size > 0) {
241
+ await new Promise((resolve) => setTimeout(resolve, 100)); // Poll every 100ms
242
+ }
243
+ logger.info(`${this.instanceNameWithSuffix} - Subscriber set to passive and queue drained.`);
244
+ resolve(mqttWorkerData);
245
+ });
246
+ }
247
+ /**
248
+ * Processes and publishes MQTT messages based on the selected message mode.
249
+ *
250
+ * @param mqttMessage - The MQTT message object.
251
+ * @param mode - The message mode (Raw, Delta, or Both).
252
+ */
253
+ async publishMqttMessage(mqttMessage, mode = MessageMode.Raw) {
254
+ if (!mqttMessage) {
255
+ logger.error(`${this.instanceNameWithSuffix} - Error publishing mqtt message: mqttMessage must be defined.`);
256
+ return;
257
+ }
258
+ const attrs = Array.isArray(mqttMessage.attributes)
259
+ ? mqttMessage.attributes
260
+ : [mqttMessage.attributes];
261
+ const { topic, asset, assetDescription, objectType, objectTypeDescription, objectId } = mqttMessage;
262
+ for (const attrEntry of attrs) {
263
+ const attrDescription = attrEntry.description ?? getAttributeDescription(attrEntry.attribute);
264
+ const message = "message" in attrEntry && attrEntry.message
265
+ ? attrEntry.message
266
+ : "data" in attrEntry && attrEntry.data
267
+ ? { data: attrEntry.data, createdAt: attrEntry.createdAt, expiresAt: attrEntry.expiresAt }
268
+ : "table" in attrEntry && attrEntry.table
269
+ ? { table: attrEntry.table, createdAt: attrEntry.createdAt, expiresAt: attrEntry.expiresAt }
270
+ : (() => { throw new Error("Attribute entry must include exactly one of data/table/message"); })();
271
+ const packet = await UnsPacket.unsPacketFromUnsMessage(message);
272
+ const singleMsg = {
273
+ topic,
274
+ asset,
275
+ assetDescription,
276
+ objectType,
277
+ objectTypeDescription,
278
+ objectId,
279
+ attribute: attrEntry.attribute,
280
+ description: attrDescription,
281
+ tags: attrEntry.tags,
282
+ attributeNeedsPersistence: attrEntry.attributeNeedsPersistence,
283
+ ...(attrEntry.validityMode ? { validityMode: attrEntry.validityMode } : {}),
284
+ ...(attrEntry.expectedIntervalMs ? { expectedIntervalMs: attrEntry.expectedIntervalMs } : {}),
285
+ ...(attrEntry.lifecycleEndValue ? { lifecycleEndValue: attrEntry.lifecycleEndValue } : {}),
286
+ packet,
287
+ };
288
+ // existing single-attribute flow
289
+ const baseDescription = singleMsg.description ??
290
+ getAttributeDescription(singleMsg.attribute) ??
291
+ singleMsg.attribute;
292
+ const mqttMessageWithDesc = { ...singleMsg, description: baseDescription };
293
+ const time = UnsPacket.formatToISO8601(new Date());
294
+ switch (mode) {
295
+ case MessageMode.Raw: {
296
+ await this.processAndEnqueueMessage(mqttMessageWithDesc, time, false);
297
+ break;
298
+ }
299
+ case MessageMode.Delta: {
300
+ const deltaMessage = { ...mqttMessageWithDesc };
301
+ deltaMessage.attribute = `${mqttMessageWithDesc.attribute}-delta`;
302
+ deltaMessage.description = `${baseDescription ?? ""} (delta)`;
303
+ await this.processAndEnqueueMessage(deltaMessage, time, true);
304
+ break;
305
+ }
306
+ case MessageMode.Both: {
307
+ await this.processAndEnqueueMessage(mqttMessageWithDesc, time, false);
308
+ const deltaMessageBoth = { ...mqttMessageWithDesc };
309
+ deltaMessageBoth.attribute = `${mqttMessageWithDesc.attribute}-delta`;
310
+ deltaMessageBoth.description = `${baseDescription ?? ""} (delta)`;
311
+ await this.processAndEnqueueMessage(deltaMessageBoth, time, true);
312
+ break;
313
+ }
314
+ }
315
+ }
316
+ return;
317
+ }
318
+ /**
319
+ * Publishes a message to a specified topic.
320
+ *
321
+ * @param topic - The MQTT topic.
322
+ * @param message - The message to publish.
323
+ * @returns A promise that resolves when enqueued.
324
+ */
325
+ publishMessage(topic, message, options) {
326
+ return this.enqueueMessageToWorkerQueue(topic, message, options);
327
+ }
328
+ /**
329
+ * Parses an MQTT packet from a JSON string.
330
+ *
331
+ * @param mqttPacket - The MQTT packet string.
332
+ * @returns A parsed IUnsPacket object or null.
333
+ */
334
+ parseMqttPacket(mqttPacket) {
335
+ return UnsPacket.parseMqttPacket(mqttPacket, this.instanceNameWithSuffix);
336
+ }
337
+ /**
338
+ * Subscribes asynchronously to one or more topics.
339
+ *
340
+ * @param topics - A topic or list of topics.
341
+ */
342
+ subscribeAsync(topics) {
343
+ this.worker.postMessage({ command: "subscribeAsync", topics });
344
+ }
345
+ /**
346
+ * Unsubscribes asynchronously from the given topics.
347
+ *
348
+ * @param topics - A list of topics.
349
+ */
350
+ unsubscribeAsync(topics) {
351
+ this.worker.postMessage({ command: "unsubscribeAsync", topics });
352
+ }
353
+ /**
354
+ * Processes and enqueues a message to the worker queue, including handling
355
+ * sequencing, value differences, and tracking of unique topics.
356
+ *
357
+ * @param msg - The MQTT message to process.
358
+ * @param time - The timestamp.
359
+ * @param valueIsCumulative - Whether the value is cumulative.
360
+ */
361
+ async processAndEnqueueMessage(msg, time, valueIsCumulative = false) {
362
+ try {
363
+ const attributeType = msg.packet.message.data ? UnsAttributeType.Data :
364
+ msg.packet.message.table ? UnsAttributeType.Table : null;
365
+ let dataGroup = "";
366
+ if (attributeType == UnsAttributeType.Data)
367
+ dataGroup = msg.packet.message.data.dataGroup ?? "";
368
+ if (attributeType == UnsAttributeType.Table)
369
+ dataGroup = msg.packet.message.table.dataGroup ?? "";
370
+ const { objectType, objectId, asset } = this.resolveObjectIdentity(msg);
371
+ const normalizedTopic = this.normalizeTopicWithObject(msg.topic);
372
+ msg.topic = normalizedTopic;
373
+ const description = msg.description ?? getAttributeDescription(msg.attribute) ?? "";
374
+ const objectTypeDescription = msg.objectTypeDescription ?? (objectType ? getObjectTypeDescription(objectType) : undefined);
375
+ // Runtime validation for validity fields
376
+ if (msg.validityMode) {
377
+ const validModes = new Set(["interval", "lifecycle", "static"]);
378
+ if (!validModes.has(msg.validityMode)) {
379
+ logger.warn(`${this.instanceNameWithSuffix} - Invalid validityMode "${msg.validityMode}" for attribute "${msg.attribute}". Expected: interval | lifecycle | static.`);
380
+ }
381
+ if (msg.expectedIntervalMs && msg.validityMode !== "interval") {
382
+ logger.warn(`${this.instanceNameWithSuffix} - expectedIntervalMs is set but validityMode is "${msg.validityMode}" (only used with "interval") for attribute "${msg.attribute}".`);
383
+ }
384
+ if (msg.lifecycleEndValue && msg.validityMode !== "lifecycle") {
385
+ logger.warn(`${this.instanceNameWithSuffix} - lifecycleEndValue is set but validityMode is "${msg.validityMode}" (only used with "lifecycle") for attribute "${msg.attribute}".`);
386
+ }
387
+ if (msg.validityMode === "interval" && !msg.expectedIntervalMs) {
388
+ logger.debug(`${this.instanceNameWithSuffix} - validityMode "interval" without expectedIntervalMs for attribute "${msg.attribute}" — controller will use its default.`);
389
+ }
390
+ }
391
+ this.registerUniqueTopic({
392
+ timestamp: time,
393
+ topic: msg.topic,
394
+ attribute: msg.attribute,
395
+ attributeType: attributeType,
396
+ description,
397
+ tags: msg.tags,
398
+ attributeNeedsPersistence: msg.attributeNeedsPersistence,
399
+ dataGroup,
400
+ asset,
401
+ assetDescription: msg.assetDescription,
402
+ objectType,
403
+ objectTypeDescription,
404
+ objectId,
405
+ ...(msg.validityMode ? { validityMode: msg.validityMode } : {}),
406
+ ...(msg.expectedIntervalMs ? { expectedIntervalMs: msg.expectedIntervalMs } : {}),
407
+ ...(msg.lifecycleEndValue ? { lifecycleEndValue: msg.lifecycleEndValue } : {}),
408
+ });
409
+ const publishTopic = `${msg.topic}${asset ? `${asset}/` : ""}${objectType ? `${objectType}/` : ""}${objectId ? `${objectId}/` : ""}${msg.attribute}`;
410
+ const sequenceId = this.currentSequenceId.get(msg.topic) ?? 0;
411
+ this.currentSequenceId.set(msg.topic, sequenceId + 1);
412
+ msg.packet.sequenceId = sequenceId;
413
+ if (msg.packet.message.data) {
414
+ const newValue = msg.packet.message.data.value;
415
+ const newUom = msg.packet.message.data.uom;
416
+ const lastValueEntry = this.lastValues.get(publishTopic);
417
+ const currentTime = new Date(msg.packet.message.data.time);
418
+ if (lastValueEntry) {
419
+ const intervalBetweenMessages = currentTime.getTime() - lastValueEntry.timestamp.getTime();
420
+ const lastValue = lastValueEntry.value;
421
+ this.lastValues.set(publishTopic, { value: newValue, uom: newUom, timestamp: currentTime });
422
+ // Compute the delta and manage cumulative resets
423
+ if (valueIsCumulative == true && typeof newValue === "number" && typeof lastValue === "number") {
424
+ // Skip if newValue is 0 (likely a glitch)
425
+ if (newValue === 0) {
426
+ return; // Don't process or enqueue
427
+ }
428
+ const delta = newValue - lastValue;
429
+ msg.packet.message.data.value = delta < 0 ? newValue : delta;
430
+ }
431
+ msg.packet.interval = intervalBetweenMessages;
432
+ await this.enqueueMessageToWorkerQueue(publishTopic, JSON.stringify(msg.packet));
433
+ }
434
+ else {
435
+ this.lastValues.set(publishTopic, { value: newValue, uom: newUom, timestamp: currentTime });
436
+ logger.debug(`${this.instanceNameWithSuffix} - Need one more packet to calculate interval on topic ${publishTopic}`);
437
+ if (valueIsCumulative === false) {
438
+ await this.enqueueMessageToWorkerQueue(publishTopic, JSON.stringify(msg.packet));
439
+ }
440
+ else {
441
+ logger.debug(`${this.instanceNameWithSuffix} - Need one more packet to calculate difference on value in data for topic ${publishTopic}`);
442
+ }
443
+ }
444
+ }
445
+ else if (msg.packet.message.table) {
446
+ await this.enqueueMessageToWorkerQueue(publishTopic, JSON.stringify(msg.packet));
447
+ }
448
+ else {
449
+ logger.error(`${this.instanceNameWithSuffix} - Error publishing message to topic ${publishTopic}: packet.message must include data or table`);
450
+ }
451
+ }
452
+ catch (error) {
453
+ logger.error(`${this.instanceNameWithSuffix} - Error publishing message to topic ${msg.topic}${msg.attribute}: ${error.message}`);
454
+ }
455
+ }
456
+ /**
457
+ * Stops the UnsProxy instance and cleans up resources.
458
+ */
459
+ async stop() {
460
+ super.stop();
461
+ // Terminate the worker thread if it exists.
462
+ if (this.worker) {
463
+ try {
464
+ const exitCode = await this.worker.terminate();
465
+ logger.info(`${this.instanceNameWithSuffix} - Worker terminated with exit code ${exitCode}`);
466
+ }
467
+ catch (error) {
468
+ logger.error(`${this.instanceNameWithSuffix} - Error terminating worker: ${error.message}`);
469
+ }
470
+ }
471
+ // Optionally, handle any pending enqueues.
472
+ for (const [id, pending] of this.pendingEnqueues) {
473
+ pending.reject(new Error("UnsProxy has been stopped"));
474
+ this.pendingEnqueues.delete(id);
475
+ }
476
+ }
477
+ }
478
478
  //# sourceMappingURL=uns-mqtt-proxy.js.map