@atelieruldetehnologii/node-red-custome-solutions 0.62.22

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 (107) hide show
  1. package/NOTICE +56 -0
  2. package/PROPRIETARY-LICENSE.txt +157 -0
  3. package/README.md +105 -0
  4. package/TERMS-AND-CONDITIONS.md +311 -0
  5. package/advantech-read/advantech-read.html +775 -0
  6. package/advantech-read/advantech-read.js +449 -0
  7. package/advantech-write/advantech-write.html +413 -0
  8. package/advantech-write/advantech-write.js +338 -0
  9. package/bess-control/bess-control.html +569 -0
  10. package/bess-control/bess-control.js +541 -0
  11. package/bess-control/lib/allocation.js +149 -0
  12. package/bess-control/lib/batteries.js +86 -0
  13. package/bess-control/lib/helpers.js +157 -0
  14. package/bess-control/lib/messages.js +131 -0
  15. package/bess-control/lib/runtime.js +156 -0
  16. package/carlo-gavazzi/carlo-gavazzi.html +4327 -0
  17. package/carlo-gavazzi/carlo-gavazzi.js +3081 -0
  18. package/control-export/control-export.html +3644 -0
  19. package/control-export/control-export.js +2192 -0
  20. package/control-reactive/control-reactive.html +3136 -0
  21. package/control-reactive/control-reactive.js +2124 -0
  22. package/fronius-emulator/README.md +258 -0
  23. package/fronius-emulator/fronius-emulator.html +170 -0
  24. package/fronius-emulator/fronius-emulator.js +520 -0
  25. package/fronius-emulator/lib/binary-codec.js +65 -0
  26. package/fronius-emulator/lib/fronius-register-map.js +185 -0
  27. package/fronius-emulator/lib/modbus-parser.js +61 -0
  28. package/fronius-emulator/lib/modbus-tcp-server.js +224 -0
  29. package/fronius-emulator/lib/validation.js +206 -0
  30. package/fronius-emulator/lib/virtual-meter.js +132 -0
  31. package/fronius-read/fronius-read.html +3944 -0
  32. package/fronius-read/fronius-read.js +1466 -0
  33. package/fronius-read/lib/inverter-profiles.js +711 -0
  34. package/fronius-read/lib/sunspec-core.js +303 -0
  35. package/fronius-read/lib/sunspec-model-parsers.js +845 -0
  36. package/fronius-write/fronius-write.html +1789 -0
  37. package/fronius-write/fronius-write.js +2531 -0
  38. package/influxdb/influxdb.html +1077 -0
  39. package/influxdb/influxdb.js +1487 -0
  40. package/janitza/janitza.html +2562 -0
  41. package/janitza/janitza.js +1500 -0
  42. package/janitza/lib/janitza-profiles.js +371 -0
  43. package/lib/a3-test-server.js +199 -0
  44. package/lib/adam-6000-profiles.js +173 -0
  45. package/lib/licensing/constants.js +103 -0
  46. package/lib/licensing/device-id.js +251 -0
  47. package/lib/licensing/errors.js +21 -0
  48. package/lib/licensing/license-client.js +377 -0
  49. package/lib/licensing/license-state.js +50 -0
  50. package/lib/licensing/node-guard.js +150 -0
  51. package/lib/licensing/package-integrity.js +167 -0
  52. package/lib/licensing/package-integrity.json +513 -0
  53. package/lib/licensing/public-key.pem +3 -0
  54. package/lib/licensing/public-key.pem.example +2 -0
  55. package/lib/licensing/public-keys.js +18 -0
  56. package/lib/licensing/storage.js +63 -0
  57. package/lib/licensing/token-verifier.js +84 -0
  58. package/lib/numbers.js +26 -0
  59. package/license/atelier-license.html +88 -0
  60. package/license/atelier-license.js +170 -0
  61. package/modbus-tcp-discover/modbus-tcp-discover.html +190 -0
  62. package/modbus-tcp-discover/modbus-tcp-discover.js +706 -0
  63. package/multi-battery/multi-battery.html +649 -0
  64. package/multi-battery/multi-battery.js +668 -0
  65. package/multi-grid/multi-grid.html +745 -0
  66. package/multi-grid/multi-grid.js +1392 -0
  67. package/multi-inverter/lib/engine-core.js +237 -0
  68. package/multi-inverter/multi-inverter.html +2674 -0
  69. package/multi-inverter/multi-inverter.js +1393 -0
  70. package/package.json +109 -0
  71. package/phoenix-contact/lib/eem-ma370-advanced.js +175 -0
  72. package/phoenix-contact/lib/eem-ma370.js +138 -0
  73. package/phoenix-contact/lib/integrations.js +149 -0
  74. package/phoenix-contact/lib/phoenix-contact-profile.js +35030 -0
  75. package/phoenix-contact/phoenix-contact.html +840 -0
  76. package/phoenix-contact/phoenix-contact.js +679 -0
  77. package/pv-generator/pv-generator.html +284 -0
  78. package/pv-generator/pv-generator.js +387 -0
  79. package/pylontech-read/pylontech-read.html +763 -0
  80. package/pylontech-read/pylontech-read.js +518 -0
  81. package/pylontech-write/pylontech-write.html +405 -0
  82. package/pylontech-write/pylontech-write.js +546 -0
  83. package/scada-aggregator/lib/aggregate.js +72 -0
  84. package/scada-aggregator/lib/groups.js +39 -0
  85. package/scada-aggregator/scada-aggregator.html +108 -0
  86. package/scada-aggregator/scada-aggregator.js +117 -0
  87. package/scada-server/lib/aggregator-map.js +58 -0
  88. package/scada-server/lib/encoding.js +235 -0
  89. package/scada-server/lib/modbus-server.js +492 -0
  90. package/scada-server/lib/object-path.js +18 -0
  91. package/scada-server/lib/register-map.js +288 -0
  92. package/scada-server/scada-server.html +1419 -0
  93. package/scada-server/scada-server.js +704 -0
  94. package/scripts/check-syntax.js +66 -0
  95. package/scripts/package-integrity.js +46 -0
  96. package/scripts/package-release.js +116 -0
  97. package/scripts/run-tests.js +2178 -0
  98. package/scripts/tgz-integrity.js +74 -0
  99. package/sma-read/lib/sunspec-core.js +83 -0
  100. package/sma-read/lib/sunspec-model-parsers.js +853 -0
  101. package/sma-read/sma-read.html +659 -0
  102. package/sma-read/sma-read.js +458 -0
  103. package/sma-write/sma-write.html +105 -0
  104. package/sma-write/sma-write.js +270 -0
  105. package/victron-read/lib/mqtt-state.js +176 -0
  106. package/victron-read/victron-read.html +1074 -0
  107. package/victron-read/victron-read.js +1222 -0
@@ -0,0 +1,1392 @@
1
+ module.exports = function (RED) {
2
+ "use strict";
3
+
4
+ const { runLicensedNode } = require("../lib/licensing/node-guard");
5
+
6
+ const {
7
+ atLeast,
8
+ clampNumber,
9
+ roundedAtLeast
10
+ } = require("../lib/numbers");
11
+
12
+ function normalizeMeterOrder(value) {
13
+ return String(value || "")
14
+ .split(",")
15
+ .map(function (x) { return x.trim(); })
16
+ .filter(Boolean)
17
+ .filter(function (v, i, arr) { return arr.indexOf(v) === i; });
18
+ }
19
+
20
+ function normalizeOutputKeys(value) {
21
+ return String(value || "")
22
+ .split(",")
23
+ .map(function (x) { return x.trim(); })
24
+ .filter(Boolean)
25
+ .filter(function (v, i, arr) { return arr.indexOf(v) === i; });
26
+ }
27
+
28
+ function normalizeOutputCount(value, fallback) {
29
+ const parsed = Number(value);
30
+ if (Number.isFinite(parsed) && parsed >= 1) {
31
+ return Math.max(1, Math.floor(parsed));
32
+ }
33
+ return fallback;
34
+ }
35
+
36
+ function extractMeterId(msg, source, customPath) {
37
+ if (source === "topic") {
38
+ return String(msg.topic || "").trim();
39
+ }
40
+
41
+ if (source === "meterId") {
42
+ return String(msg.meterId || "").trim();
43
+ }
44
+
45
+ if (source === "meter_id") {
46
+ return String(msg.meter_id || "").trim();
47
+ }
48
+
49
+ if (source === "payload.meterId") {
50
+ return String(msg.payload && msg.payload.meterId || "").trim();
51
+ }
52
+
53
+ if (source === "custom") {
54
+ const path = String(customPath || "").trim();
55
+ if (!path) {
56
+ return "";
57
+ }
58
+
59
+ const value = path.split(".").reduce(function (acc, key) {
60
+ return acc !== null && acc !== undefined ? acc[key] : undefined;
61
+ }, msg);
62
+
63
+ return String(value || "").trim();
64
+ }
65
+
66
+ return String(msg.topic || "").trim();
67
+ }
68
+
69
+ function isPlainObject(value) {
70
+ return value !== null && typeof value === "object" && !Array.isArray(value);
71
+ }
72
+
73
+ function cloneObject(obj) {
74
+ return JSON.parse(JSON.stringify(obj));
75
+ }
76
+
77
+ function onlyNumericEntries(obj) {
78
+ const result = {};
79
+
80
+ Object.keys(obj || {}).forEach(function (key) {
81
+ const value = obj[key];
82
+
83
+ if (typeof value === "number" && Number.isFinite(value)) {
84
+ result[key] = value;
85
+ return;
86
+ }
87
+
88
+ if (typeof value === "string" && value.trim() !== "") {
89
+ const parsed = Number(value);
90
+ if (Number.isFinite(parsed)) {
91
+ result[key] = parsed;
92
+ }
93
+ }
94
+ });
95
+
96
+ return result;
97
+ }
98
+
99
+ function firstDefined(values) {
100
+ for (let i = 0; i < values.length; i += 1) {
101
+ if (values[i] !== undefined && values[i] !== null && String(values[i]).trim() !== "") {
102
+ return values[i];
103
+ }
104
+ }
105
+ return "";
106
+ }
107
+
108
+ function extractMeterMetadata(msg, meterId) {
109
+ const influxTags = msg && msg.influx && msg.influx.tags || {};
110
+ return {
111
+ meter_id: meterId,
112
+ ip: String(firstDefined([msg.ip, msg.host, influxTags.host])).trim(),
113
+ port: String(firstDefined([msg.port, influxTags.port])).trim(),
114
+ unit_id: String(firstDefined([msg.unit_id, msg.unitId, influxTags.unit_id])).trim(),
115
+ device_type: String(firstDefined([msg.device_type, influxTags.device_type])).trim(),
116
+ model: String(firstDefined([msg.model, influxTags.model])).trim()
117
+ };
118
+ }
119
+
120
+ function aggregateValues(operation, values) {
121
+ if (!Array.isArray(values) || !values.length) {
122
+ return undefined;
123
+ }
124
+
125
+ if (operation === "sum") {
126
+ return values.reduce(function (acc, v) { return acc + v; }, 0);
127
+ }
128
+
129
+ if (operation === "subtract") {
130
+ return values.slice(1).reduce(function (acc, v) { return acc - v; }, values[0]);
131
+ }
132
+
133
+ if (operation === "min") {
134
+ return Math.min.apply(null, values);
135
+ }
136
+
137
+ if (operation === "max") {
138
+ return Math.max.apply(null, values);
139
+ }
140
+
141
+ if (operation === "avg") {
142
+ const total = values.reduce(function (acc, v) { return acc + v; }, 0);
143
+ return total / values.length;
144
+ }
145
+
146
+ return values.reduce(function (acc, v) { return acc + v; }, 0);
147
+ }
148
+
149
+ function normalizeMissingDataStrategy(value) {
150
+ const normalized = String(value || "skip").trim().toLowerCase();
151
+ if (normalized === "zero-fill" || normalized === "zero_fill" || normalized === "zerofill") {
152
+ return "zero-fill";
153
+ }
154
+ if (normalized === "use-last" || normalized === "use_last" || normalized === "uselast") {
155
+ return "use-last";
156
+ }
157
+ return "skip";
158
+ }
159
+
160
+ function buildOrderedMeterIds(store, configuredOrder) {
161
+ const seen = new Set();
162
+ const result = [];
163
+
164
+ configuredOrder.forEach(function (id) {
165
+ if (store[id] && !seen.has(id)) {
166
+ seen.add(id);
167
+ result.push(id);
168
+ }
169
+ });
170
+
171
+ Object.keys(store).forEach(function (id) {
172
+ if (!seen.has(id)) {
173
+ seen.add(id);
174
+ result.push(id);
175
+ }
176
+ });
177
+
178
+ return result;
179
+ }
180
+
181
+ function parseExpression(expression) {
182
+ const expr = String(expression || "").replace(/\s+/g, "");
183
+ if (!expr) {
184
+ return [];
185
+ }
186
+
187
+ const tokens = expr.match(/[+-]?[^+-]+/g) || [];
188
+ const result = [];
189
+
190
+ tokens.forEach(function (token) {
191
+ if (!token) {
192
+ return;
193
+ }
194
+
195
+ let sign = 1;
196
+ let meterId = token;
197
+
198
+ if (token[0] === "+") {
199
+ sign = 1;
200
+ meterId = token.slice(1);
201
+ } else if (token[0] === "-") {
202
+ sign = -1;
203
+ meterId = token.slice(1);
204
+ }
205
+
206
+ meterId = String(meterId || "").trim();
207
+
208
+ if (!meterId) {
209
+ return;
210
+ }
211
+
212
+ result.push({
213
+ meterId: meterId,
214
+ sign: sign
215
+ });
216
+ });
217
+
218
+ return result;
219
+ }
220
+
221
+ function expressionMeterIds(tokens) {
222
+ return tokens.map(function (item) {
223
+ return item.meterId;
224
+ }).filter(function (v, i, arr) {
225
+ return arr.indexOf(v) === i;
226
+ });
227
+ }
228
+
229
+ function evaluateExpressionForKey(tokens, store, key, options) {
230
+ const zeroFill = !!(options && options.zeroFill);
231
+ let hasAny = false;
232
+ let result = 0;
233
+
234
+ tokens.forEach(function (token) {
235
+ const entry = store[token.meterId];
236
+ if (!entry || !entry.payload) {
237
+ if (zeroFill) {
238
+ hasAny = true;
239
+ }
240
+ return;
241
+ }
242
+
243
+ const value = entry.payload[key];
244
+ if (typeof value !== "number" || !Number.isFinite(value)) {
245
+ if (zeroFill) {
246
+ hasAny = true;
247
+ }
248
+ return;
249
+ }
250
+
251
+ result += token.sign * value;
252
+ hasAny = true;
253
+ });
254
+
255
+ return hasAny ? result : undefined;
256
+ }
257
+
258
+ function detectCategoryFromKey(key) {
259
+ const k = String(key || "").toLowerCase();
260
+
261
+ if (k.indexOf("voltage") === 0 || k.indexOf("u_") === 0) {
262
+ return "voltage";
263
+ }
264
+ if (k.indexOf("current") === 0 || k.indexOf("i_") === 0) {
265
+ return "current";
266
+ }
267
+ if (k.indexOf("power_factor") === 0 || k === "pf" || k.indexOf("pf_") === 0) {
268
+ return "power_factor";
269
+ }
270
+ if (k.indexOf("frequency") === 0 || k.indexOf("freq") === 0) {
271
+ return "frequency";
272
+ }
273
+ if (k.indexOf("reactive") === 0 || k.indexOf("_var") !== -1 || k === "var") {
274
+ return "reactive";
275
+ }
276
+ if (k.indexOf("apparent") === 0 || k.indexOf("_va") !== -1 || k === "va") {
277
+ return "apparent";
278
+ }
279
+ if (k.indexOf("power") === 0 || k.indexOf("_w") !== -1 || k === "w") {
280
+ return "power";
281
+ }
282
+ if (k.indexOf("energy") === 0 || k.indexOf("kwh") !== -1 || k.indexOf("kvarh") !== -1) {
283
+ return "energy";
284
+ }
285
+
286
+ return "other";
287
+ }
288
+
289
+ function getPayloadCategories(payload) {
290
+ const categories = [];
291
+
292
+ Object.keys(payload || {}).forEach(function (key) {
293
+ const category = detectCategoryFromKey(key);
294
+ if (categories.indexOf(category) === -1) {
295
+ categories.push(category);
296
+ }
297
+ });
298
+
299
+ return categories.sort();
300
+ }
301
+
302
+ function validateMeterCategoryConsistency(meterIds, store) {
303
+ const result = {
304
+ ok: true,
305
+ reason: "",
306
+ categoriesByMeter: {}
307
+ };
308
+
309
+ const uniqueSignatureMap = {};
310
+
311
+ meterIds.forEach(function (meterId) {
312
+ const entry = store[meterId];
313
+ if (!entry || !entry.payload) {
314
+ return;
315
+ }
316
+
317
+ const categories = getPayloadCategories(entry.payload);
318
+ result.categoriesByMeter[meterId] = categories;
319
+
320
+ const signature = categories.join("|");
321
+ if (!uniqueSignatureMap[signature]) {
322
+ uniqueSignatureMap[signature] = [];
323
+ }
324
+ uniqueSignatureMap[signature].push(meterId);
325
+ });
326
+
327
+ const signatures = Object.keys(uniqueSignatureMap);
328
+
329
+ if (signatures.length <= 1) {
330
+ return result;
331
+ }
332
+
333
+ result.ok = false;
334
+ result.reason = signatures.map(function (signature) {
335
+ const label = signature || "none";
336
+ return uniqueSignatureMap[signature].join(",") + " -> [" + label + "]";
337
+ }).join(" ; ");
338
+
339
+ return result;
340
+ }
341
+
342
+ function sumValuesByCategory(payload) {
343
+ const result = {};
344
+
345
+ Object.keys(payload || {}).forEach(function (key) {
346
+ const value = payload[key];
347
+ if (typeof value !== "number" || !Number.isFinite(value)) {
348
+ return;
349
+ }
350
+
351
+ const category = detectCategoryFromKey(key);
352
+ if (!Object.prototype.hasOwnProperty.call(result, category)) {
353
+ result[category] = 0;
354
+ }
355
+
356
+ result[category] += value;
357
+ });
358
+
359
+ return result;
360
+ }
361
+
362
+ function collectRawCategoryValues(meterIds, store) {
363
+ const valuesByCategory = {};
364
+
365
+ meterIds.forEach(function (meterId) {
366
+ const entry = store[meterId];
367
+ if (!entry || !entry.payload) {
368
+ return;
369
+ }
370
+
371
+ Object.keys(entry.payload).forEach(function (key) {
372
+ const value = entry.payload[key];
373
+ if (typeof value !== "number" || !Number.isFinite(value)) {
374
+ return;
375
+ }
376
+
377
+ const category = detectCategoryFromKey(key);
378
+
379
+ if (!valuesByCategory[category]) {
380
+ valuesByCategory[category] = [];
381
+ }
382
+
383
+ valuesByCategory[category].push(value);
384
+ });
385
+ });
386
+
387
+ return valuesByCategory;
388
+ }
389
+
390
+ function collectMeterCategoryTotals(meterIds, store) {
391
+ const result = {};
392
+
393
+ meterIds.forEach(function (meterId) {
394
+ const entry = store[meterId];
395
+ if (!entry || !entry.payload) {
396
+ return;
397
+ }
398
+
399
+ result[meterId] = sumValuesByCategory(entry.payload);
400
+ });
401
+
402
+ return result;
403
+ }
404
+
405
+ function buildScadaPayload(payload) {
406
+ const src = isPlainObject(payload) ? payload : {};
407
+ const out = {};
408
+
409
+ function copyNumber(targetKey, sourceKeys) {
410
+ let i;
411
+ for (i = 0; i < sourceKeys.length; i += 1) {
412
+ const key = sourceKeys[i];
413
+ const value = src[key];
414
+ if (typeof value === "number" && Number.isFinite(value)) {
415
+ out[targetKey] = value;
416
+ return;
417
+ }
418
+ }
419
+ }
420
+
421
+ copyNumber("active_power_w", ["active_power_w", "power_w", "total_power_w"]);
422
+ copyNumber("reactive_power_var", ["reactive_power_var", "reactive_total_var", "reactive_var"]);
423
+ copyNumber("frequency_hz", ["frequency_hz", "freq_hz"]);
424
+ copyNumber("voltage_l1_l2_v", ["voltage_l1_l2_v"]);
425
+ copyNumber("voltage_l2_l3_v", ["voltage_l2_l3_v"]);
426
+ copyNumber("voltage_l1_l3_v", ["voltage_l1_l3_v", "voltage_l3_l1_v"]);
427
+
428
+ Object.keys(src).forEach(function (key) {
429
+ const value = src[key];
430
+ if (!Object.prototype.hasOwnProperty.call(out, key) && typeof value === "number" && Number.isFinite(value)) {
431
+ out[key] = value;
432
+ }
433
+ });
434
+
435
+ return out;
436
+ }
437
+
438
+ function buildControlExportReactivePayload(payload) {
439
+ const src = isPlainObject(payload) ? payload : {};
440
+ const out = cloneObject(src);
441
+
442
+ function firstNumber(keys) {
443
+ let i;
444
+ for (i = 0; i < keys.length; i += 1) {
445
+ const value = src[keys[i]];
446
+ if (typeof value === "number" && Number.isFinite(value)) {
447
+ return value;
448
+ }
449
+ }
450
+ return undefined;
451
+ }
452
+
453
+ const activeTotal = firstNumber(["power_w", "power_total_w", "active_power_w", "total_power_w", "grid_reference_w", "grid_power_w"]);
454
+ if (typeof activeTotal === "number") {
455
+ out.power_w = activeTotal;
456
+ out.active_power_w = activeTotal;
457
+ out.grid_reference_w = activeTotal;
458
+ }
459
+
460
+ const reactiveTotal = firstNumber(["reactive_total_var", "reactive_power_var", "grid_reactive_power_var", "q_total_var"]);
461
+ if (typeof reactiveTotal === "number") {
462
+ out.reactive_total_var = reactiveTotal;
463
+ out.reactive_power_var = reactiveTotal;
464
+ out.grid_reactive_power_var = reactiveTotal;
465
+ }
466
+
467
+ return out;
468
+ }
469
+
470
+ function MultiGridNode(config) {
471
+ RED.nodes.createNode(this, config);
472
+
473
+ const node = this;
474
+
475
+ runLicensedNode(node, RED, config, function startLicensedRuntime() {
476
+
477
+ node.name = config.name;
478
+ node.operation = config.operation || "sum";
479
+ node.expression = String(config.expression || "").trim();
480
+ node.meterIdSource = config.meterIdSource || "topic";
481
+ node.customMeterPath = config.customMeterPath || "";
482
+ node.meterOrder = normalizeMeterOrder(config.meterOrder);
483
+ node.primaryMeterId = String(config.primaryMeterId || "").trim();
484
+ node.secondaryMeterId = String(config.secondaryMeterId || "").trim();
485
+ node.expectedMeterCount = atLeast(config.expectedMeterCount, 0, 0);
486
+ node.emitPartial = config.emitPartial !== false;
487
+ node.valueTimeoutMs = atLeast(config.valueTimeoutMs, 0, 0);
488
+ node.roundDigits = clampNumber(config.roundDigits, 0, 8, 3);
489
+ node.preserveOriginalMsg = config.preserveOriginalMsg !== false;
490
+ node.strictCategoryCheck = config.strictCategoryCheck !== false;
491
+ node.groupByCategory = config.groupByCategory === true;
492
+ node.scadaServer = config.scadaServer === true;
493
+ node.controlExportReactive = config.controlExportReactive === true;
494
+ node.influxDb = config.influxDb === true;
495
+ node.influxMeasurement = String(config.influxMeasurement || "multi_grid").trim() || "multi_grid";
496
+ node.influxNode = String(config.influxNode || "").trim();
497
+ node.splitValueOutputs = config.splitValueOutputs === true;
498
+ node.splitOutputCount = normalizeOutputCount(config.splitOutputCount, normalizeOutputKeys(config.outputKeys).length || 4);
499
+ node.missingDataStrategy = normalizeMissingDataStrategy(config.missingDataStrategy);
500
+ node.primarySecondaryTimeoutMs = atLeast(
501
+ config.primarySecondaryTimeoutMs || config.valueTimeoutMs,
502
+ 0,
503
+ 10000
504
+ );
505
+ node.failSecureEnabled = !!config.failSecureEnabled;
506
+ node.failSecureTimeSec = roundedAtLeast(config.failSecureTimeSec, 1, 10);
507
+
508
+ node.meterStore = {};
509
+ let failSecureBreachStartMs = null;
510
+ let failSecureActive = false;
511
+
512
+ function roundValue(value) {
513
+ if (!Number.isFinite(value)) {
514
+ return value;
515
+ }
516
+
517
+ const factor = Math.pow(10, node.roundDigits);
518
+ return Math.round(value * factor) / factor;
519
+ }
520
+
521
+ function cleanupExpiredMeters() {
522
+ if (!node.valueTimeoutMs) {
523
+ return;
524
+ }
525
+
526
+ const now = Date.now();
527
+
528
+ Object.keys(node.meterStore).forEach(function (meterId) {
529
+ const entry = node.meterStore[meterId];
530
+ if (!entry || !entry.ts) {
531
+ return;
532
+ }
533
+
534
+ if ((now - entry.ts) > node.valueTimeoutMs) {
535
+ delete node.meterStore[meterId];
536
+ }
537
+ });
538
+ }
539
+
540
+ function getActiveMeterIds() {
541
+ cleanupExpiredMeters();
542
+ return buildOrderedMeterIds(node.meterStore, node.meterOrder);
543
+ }
544
+
545
+ function getExpressionTokens() {
546
+ return parseExpression(node.expression);
547
+ }
548
+
549
+ function getRequiredMeterIds() {
550
+ if (node.operation === "expression") {
551
+ return expressionMeterIds(getExpressionTokens());
552
+ }
553
+ return [];
554
+ }
555
+
556
+ function getExpectedMeterIds() {
557
+ if (node.operation === "expression") {
558
+ return getRequiredMeterIds();
559
+ }
560
+
561
+ if (node.operation === "primary-secondary") {
562
+ const ids = getPrimarySecondaryMeterIds();
563
+ return [ids.primaryMeterId, ids.secondaryMeterId].filter(Boolean);
564
+ }
565
+
566
+ if (!node.meterOrder.length) {
567
+ return [];
568
+ }
569
+
570
+ if (node.expectedMeterCount > 0) {
571
+ return node.meterOrder.slice(0, node.expectedMeterCount);
572
+ }
573
+
574
+ return node.meterOrder.slice();
575
+ }
576
+
577
+ function formatAge(ms) {
578
+ if (!Number.isFinite(ms) || ms < 0) {
579
+ return "n/a";
580
+ }
581
+ if (ms < 1000) {
582
+ return String(Math.round(ms)) + "ms";
583
+ }
584
+ if (ms < 60000) {
585
+ return (Math.round(ms / 100) / 10) + "s";
586
+ }
587
+ return (Math.round(ms / 6000) / 10) + "m";
588
+ }
589
+
590
+ function buildHealthSnapshot() {
591
+ const meterIds = getActiveMeterIds();
592
+ const requiredMeterIds = getExpectedMeterIds();
593
+ const now = Date.now();
594
+ const availableSet = new Set(meterIds);
595
+ const missingMeterIds = requiredMeterIds.filter(function (id) {
596
+ return !availableSet.has(id);
597
+ });
598
+ const meterAgeMs = {};
599
+
600
+ meterIds.forEach(function (id) {
601
+ const entry = node.meterStore[id];
602
+ if (!entry || !entry.ts) {
603
+ meterAgeMs[id] = null;
604
+ return;
605
+ }
606
+ meterAgeMs[id] = Math.max(0, now - entry.ts);
607
+ });
608
+
609
+ return {
610
+ meter_ids: meterIds,
611
+ meter_count: meterIds.length,
612
+ required_meter_ids: requiredMeterIds,
613
+ missing_meter_ids: missingMeterIds,
614
+ meter_age_ms: meterAgeMs
615
+ };
616
+ }
617
+
618
+ function buildAgesStatusText(meterIds, meterAgeMs) {
619
+ if (!meterIds.length) {
620
+ return "";
621
+ }
622
+
623
+ return meterIds.map(function (id) {
624
+ return id + ":" + formatAge(meterAgeMs[id]);
625
+ }).join(",");
626
+ }
627
+
628
+ function updateStatus(customWarningText, health) {
629
+ const snapshot = health || buildHealthSnapshot();
630
+ const meterIds = snapshot.meter_ids;
631
+ const opLabelMap = {
632
+ sum: "SUM",
633
+ subtract: "SUB",
634
+ min: "MIN",
635
+ max: "MAX",
636
+ avg: "AVG",
637
+ expression: "EXPR",
638
+ "primary-secondary": "PRI/SEC"
639
+ };
640
+
641
+ let text = opLabelMap[node.operation] + " | " + meterIds.length + " metere";
642
+
643
+ if (node.operation === "expression" && node.expression) {
644
+ text = "EXPR | " + node.expression;
645
+ }
646
+
647
+ if (node.groupByCategory) {
648
+ text += " | cat";
649
+ }
650
+
651
+ if (node.missingDataStrategy !== "skip") {
652
+ text += " | " + node.missingDataStrategy;
653
+ }
654
+
655
+ if (snapshot.missing_meter_ids.length) {
656
+ text += " | lipsă:" + snapshot.missing_meter_ids.join(",");
657
+ }
658
+
659
+ const agesText = buildAgesStatusText(meterIds, snapshot.meter_age_ms);
660
+ if (agesText) {
661
+ text += " | age " + agesText;
662
+ }
663
+
664
+ if (customWarningText) {
665
+ node.status({
666
+ fill: "yellow",
667
+ shape: "ring",
668
+ text: customWarningText
669
+ });
670
+ return;
671
+ }
672
+
673
+ if (node.operation === "expression" && !node.emitPartial) {
674
+ const requiredIds = getRequiredMeterIds();
675
+ const availableSet = new Set(snapshot.meter_ids);
676
+ const missing = requiredIds.filter(function (id) {
677
+ return !availableSet.has(id);
678
+ });
679
+
680
+ if (missing.length) {
681
+ node.status({
682
+ fill: "yellow",
683
+ shape: "ring",
684
+ text: "EXPR | lipsă: " + missing.join(",")
685
+ });
686
+ return;
687
+ }
688
+ }
689
+
690
+ if (node.expectedMeterCount > 0 && node.operation !== "expression" && node.operation !== "primary-secondary" && snapshot.meter_count < node.expectedMeterCount) {
691
+ node.status({
692
+ fill: "yellow",
693
+ shape: "ring",
694
+ text: text + " / " + node.expectedMeterCount
695
+ });
696
+ return;
697
+ }
698
+
699
+ if (!snapshot.meter_count) {
700
+ node.status({
701
+ fill: "grey",
702
+ shape: "ring",
703
+ text: "fără date"
704
+ });
705
+ return;
706
+ }
707
+
708
+ node.status({
709
+ fill: "green",
710
+ shape: "dot",
711
+ text: text
712
+ });
713
+ }
714
+
715
+ function buildAggregatePayload(meterIds) {
716
+ const keys = {};
717
+ const payload = {};
718
+ const targetMeterCount = (
719
+ node.missingDataStrategy === "zero-fill" &&
720
+ node.expectedMeterCount > meterIds.length
721
+ )
722
+ ? node.expectedMeterCount
723
+ : meterIds.length;
724
+
725
+ meterIds.forEach(function (meterId) {
726
+ const entry = node.meterStore[meterId];
727
+ if (!entry || !isPlainObject(entry.payload)) {
728
+ return;
729
+ }
730
+
731
+ Object.keys(entry.payload).forEach(function (key) {
732
+ keys[key] = true;
733
+ });
734
+ });
735
+
736
+ Object.keys(keys).forEach(function (key) {
737
+ const values = [];
738
+
739
+ meterIds.forEach(function (meterId) {
740
+ const entry = node.meterStore[meterId];
741
+ if (!entry || !entry.payload) {
742
+ if (node.missingDataStrategy === "zero-fill") {
743
+ values.push(0);
744
+ }
745
+ return;
746
+ }
747
+
748
+ const value = entry.payload[key];
749
+ if (typeof value === "number" && Number.isFinite(value)) {
750
+ values.push(value);
751
+ } else if (node.missingDataStrategy === "zero-fill") {
752
+ values.push(0);
753
+ }
754
+ });
755
+
756
+ if (!values.length) {
757
+ return;
758
+ }
759
+
760
+ if (node.missingDataStrategy === "zero-fill" && values.length < targetMeterCount) {
761
+ while (values.length < targetMeterCount) {
762
+ values.push(0);
763
+ }
764
+ }
765
+
766
+ payload[key] = roundValue(aggregateValues(node.operation, values));
767
+ });
768
+
769
+ return payload;
770
+ }
771
+
772
+ function buildExpressionPayload() {
773
+ const tokens = getExpressionTokens();
774
+ const payload = {};
775
+ const keys = {};
776
+ const zeroFill = node.missingDataStrategy === "zero-fill";
777
+
778
+ tokens.forEach(function (token) {
779
+ const entry = node.meterStore[token.meterId];
780
+ if (!entry || !entry.payload) {
781
+ return;
782
+ }
783
+
784
+ Object.keys(entry.payload).forEach(function (key) {
785
+ keys[key] = true;
786
+ });
787
+ });
788
+
789
+ Object.keys(keys).forEach(function (key) {
790
+ const value = evaluateExpressionForKey(tokens, node.meterStore, key, { zeroFill: zeroFill });
791
+ if (typeof value === "number" && Number.isFinite(value)) {
792
+ payload[key] = roundValue(value);
793
+ }
794
+ });
795
+
796
+ return payload;
797
+ }
798
+
799
+ function buildCategoryAggregatePayload(meterIds) {
800
+ const payload = {};
801
+ const zeroFill = node.missingDataStrategy === "zero-fill";
802
+ const targetMeterCount = (
803
+ zeroFill &&
804
+ node.expectedMeterCount > meterIds.length
805
+ )
806
+ ? node.expectedMeterCount
807
+ : meterIds.length;
808
+
809
+ if (node.operation === "subtract") {
810
+ const meterCategoryTotals = collectMeterCategoryTotals(meterIds, node.meterStore);
811
+ const categoryMap = {};
812
+
813
+ meterIds.forEach(function (meterId) {
814
+ const categories = meterCategoryTotals[meterId] || {};
815
+ Object.keys(categories).forEach(function (category) {
816
+ if (!categoryMap[category]) {
817
+ categoryMap[category] = [];
818
+ }
819
+ categoryMap[category].push(categories[category]);
820
+ });
821
+ });
822
+
823
+ Object.keys(categoryMap).forEach(function (category) {
824
+ const values = categoryMap[category].slice();
825
+ if (zeroFill && values.length < targetMeterCount) {
826
+ while (values.length < targetMeterCount) {
827
+ values.push(0);
828
+ }
829
+ }
830
+ const value = aggregateValues("subtract", values);
831
+ if (typeof value === "number" && Number.isFinite(value)) {
832
+ payload[category] = roundValue(value);
833
+ }
834
+ });
835
+
836
+ return payload;
837
+ }
838
+
839
+ const rawValuesByCategory = collectRawCategoryValues(meterIds, node.meterStore);
840
+
841
+ Object.keys(rawValuesByCategory).forEach(function (category) {
842
+ const values = rawValuesByCategory[category].slice();
843
+ if (zeroFill && values.length < targetMeterCount) {
844
+ while (values.length < targetMeterCount) {
845
+ values.push(0);
846
+ }
847
+ }
848
+ const value = aggregateValues(node.operation, values);
849
+ if (typeof value === "number" && Number.isFinite(value)) {
850
+ payload[category] = roundValue(value);
851
+ }
852
+ });
853
+
854
+ return payload;
855
+ }
856
+
857
+ function buildCategoryExpressionPayload() {
858
+ const tokens = getExpressionTokens();
859
+ const payload = {};
860
+ const categoryTotalsByMeter = {};
861
+ const allCategories = {};
862
+ const zeroFill = node.missingDataStrategy === "zero-fill";
863
+
864
+ tokens.forEach(function (token) {
865
+ const entry = node.meterStore[token.meterId];
866
+ if (!entry || !entry.payload) {
867
+ return;
868
+ }
869
+
870
+ const totals = sumValuesByCategory(entry.payload);
871
+ categoryTotalsByMeter[token.meterId] = totals;
872
+
873
+ Object.keys(totals).forEach(function (category) {
874
+ allCategories[category] = true;
875
+ });
876
+ });
877
+
878
+ Object.keys(allCategories).forEach(function (category) {
879
+ let hasAny = false;
880
+ let value = 0;
881
+
882
+ tokens.forEach(function (token) {
883
+ const totals = categoryTotalsByMeter[token.meterId] || {};
884
+ if (!Object.prototype.hasOwnProperty.call(totals, category)) {
885
+ if (zeroFill) {
886
+ hasAny = true;
887
+ }
888
+ return;
889
+ }
890
+
891
+ value += token.sign * totals[category];
892
+ hasAny = true;
893
+ });
894
+
895
+ if (hasAny && Number.isFinite(value)) {
896
+ payload[category] = roundValue(value);
897
+ }
898
+ });
899
+
900
+ return payload;
901
+ }
902
+
903
+ function getOutputPayloadKeys(payload) {
904
+ return Object.keys(payload || {}).slice(0, node.splitOutputCount);
905
+ }
906
+
907
+ function buildSplitOutputMessages(baseMsg, payload) {
908
+ const keys = getOutputPayloadKeys(payload);
909
+ const aggregatePayload = cloneObject(payload || {});
910
+
911
+ return Array.from({ length: node.splitOutputCount }, function (_, index) {
912
+ const key = keys[index];
913
+ if (!key || !Object.prototype.hasOwnProperty.call(payload || {}, key)) {
914
+ return null;
915
+ }
916
+
917
+ const msg = RED.util.cloneMessage(baseMsg);
918
+ msg.topic = String(baseMsg.topic || "multi-grid") + "/" + key;
919
+ msg.payload = payload[key];
920
+ msg.payload_key = key;
921
+ msg.output_index = index + 1;
922
+ msg.output_count = node.splitOutputCount;
923
+ msg.aggregate_payload = aggregatePayload;
924
+ return msg;
925
+ });
926
+ }
927
+
928
+ function buildInfluxMessage(aggregatedPayload, meterIds) {
929
+ if (!node.influxDb) {
930
+ return null;
931
+ }
932
+
933
+ const fields = {};
934
+ Object.keys(aggregatedPayload || {}).forEach(function (key) {
935
+ const value = aggregatedPayload[key];
936
+ if (typeof value === "number" && Number.isFinite(value)) {
937
+ fields[key] = value;
938
+ }
939
+ });
940
+
941
+ if (!Object.keys(fields).length) {
942
+ return null;
943
+ }
944
+
945
+ const meters = (meterIds || []).map(function (meterId) {
946
+ const entry = node.meterStore[meterId];
947
+ return cloneObject(entry && entry.metadata || { meter_id: meterId });
948
+ });
949
+ function joinedTag(key) {
950
+ return meters.map(function (meter) { return meter[key]; }).filter(Boolean).join(",");
951
+ }
952
+
953
+ return {
954
+ topic: "multi-grid/influx",
955
+ payload: fields,
956
+ influx: {
957
+ measurement: node.influxMeasurement,
958
+ tags: {
959
+ source: "multi-grid",
960
+ operation: node.operation,
961
+ meter_count: String(meters.length),
962
+ meter_ids: joinedTag("meter_id"),
963
+ meter_ips: joinedTag("ip"),
964
+ unit_ids: joinedTag("unit_id"),
965
+ device_types: joinedTag("device_type"),
966
+ meter_models: joinedTag("model")
967
+ },
968
+ fields: fields,
969
+ timestamp: new Date().toISOString()
970
+ },
971
+ influx_meter_ids: (meterIds || []).slice(),
972
+ influx_meters: meters
973
+ };
974
+ }
975
+
976
+ function sendToInfluxNode(influxMsg) {
977
+ if (!node.influxDb || !node.influxNode || !influxMsg || !influxMsg.influx) {
978
+ return;
979
+ }
980
+
981
+ const influxNode = RED.nodes.getNode(node.influxNode);
982
+ if (!influxNode || typeof influxNode.receive !== "function") {
983
+ node.warn("multi-grid: nodul adt-influxdb selectat nu este disponibil.");
984
+ return;
985
+ }
986
+
987
+ influxNode.receive(influxMsg);
988
+ }
989
+
990
+ function sendValueResult(send, outMsg, payload, failMsg) {
991
+ if (!node.splitValueOutputs) {
992
+ if (failMsg) {
993
+ send([outMsg, failMsg]);
994
+ } else {
995
+ send(outMsg);
996
+ }
997
+ return;
998
+ }
999
+
1000
+ const messages = buildSplitOutputMessages(outMsg, payload);
1001
+ if (failMsg) {
1002
+ messages.push(failMsg);
1003
+ }
1004
+ send(messages);
1005
+ }
1006
+
1007
+ function canEmit(meterIds) {
1008
+ if (!meterIds.length) {
1009
+ return false;
1010
+ }
1011
+
1012
+ if (node.operation === "expression") {
1013
+ const tokens = getExpressionTokens();
1014
+ if (!tokens.length) {
1015
+ return false;
1016
+ }
1017
+
1018
+ if (node.emitPartial) {
1019
+ return true;
1020
+ }
1021
+
1022
+ const requiredIds = expressionMeterIds(tokens);
1023
+ return requiredIds.every(function (id) {
1024
+ return !!node.meterStore[id];
1025
+ });
1026
+ }
1027
+
1028
+ if (node.operation === "primary-secondary") {
1029
+ return !!selectPrimarySecondaryMeter();
1030
+ }
1031
+
1032
+ if (node.emitPartial) {
1033
+ return true;
1034
+ }
1035
+
1036
+ if (node.expectedMeterCount > 0) {
1037
+ return meterIds.length >= node.expectedMeterCount;
1038
+ }
1039
+
1040
+ return false;
1041
+ }
1042
+
1043
+ function getPrimarySecondaryMeterIds() {
1044
+ return {
1045
+ primaryMeterId: node.primaryMeterId || node.meterOrder[0] || "",
1046
+ secondaryMeterId: node.secondaryMeterId || node.meterOrder[1] || ""
1047
+ };
1048
+ }
1049
+
1050
+ function isMeterFresh(meterId, timeoutMs, now) {
1051
+ if (!meterId) {
1052
+ return false;
1053
+ }
1054
+
1055
+ const entry = node.meterStore[meterId];
1056
+ if (!entry || !entry.ts) {
1057
+ return false;
1058
+ }
1059
+
1060
+ if (!timeoutMs) {
1061
+ return true;
1062
+ }
1063
+
1064
+ return (now - entry.ts) <= timeoutMs;
1065
+ }
1066
+
1067
+ function selectPrimarySecondaryMeter() {
1068
+ const ids = getPrimarySecondaryMeterIds();
1069
+ const now = Date.now();
1070
+ const primaryActive = isMeterFresh(ids.primaryMeterId, node.primarySecondaryTimeoutMs, now);
1071
+ const secondaryActive = isMeterFresh(ids.secondaryMeterId, node.primarySecondaryTimeoutMs, now);
1072
+ let selectedMeterId = "";
1073
+
1074
+ if (primaryActive) {
1075
+ selectedMeterId = ids.primaryMeterId;
1076
+ } else if (secondaryActive) {
1077
+ selectedMeterId = ids.secondaryMeterId;
1078
+ }
1079
+
1080
+ if (!selectedMeterId) {
1081
+ return null;
1082
+ }
1083
+
1084
+ return {
1085
+ selectedMeterId: selectedMeterId,
1086
+ primaryMeterId: ids.primaryMeterId,
1087
+ secondaryMeterId: ids.secondaryMeterId,
1088
+ primaryActive: primaryActive,
1089
+ secondaryActive: secondaryActive
1090
+ };
1091
+ }
1092
+
1093
+ function evaluateFailSecurePrimaryInactive(primaryMeterId) {
1094
+ if (!node.failSecureEnabled || node.operation !== "primary-secondary") {
1095
+ failSecureBreachStartMs = null;
1096
+ failSecureActive = false;
1097
+ return {
1098
+ enabled: false,
1099
+ active: false,
1100
+ durationMs: 0,
1101
+ timeoutSec: node.failSecureTimeSec
1102
+ };
1103
+ }
1104
+
1105
+ const nowMs = Date.now();
1106
+ const primaryActive = isMeterFresh(primaryMeterId, node.primarySecondaryTimeoutMs, nowMs);
1107
+
1108
+ if (!primaryActive) {
1109
+ if (!Number.isFinite(failSecureBreachStartMs)) {
1110
+ failSecureBreachStartMs = nowMs;
1111
+ }
1112
+ const durationMs = Math.max(0, nowMs - failSecureBreachStartMs);
1113
+ const triggerMs = Math.max(1000, Math.round(node.failSecureTimeSec * 1000));
1114
+ failSecureActive = durationMs >= triggerMs;
1115
+ return {
1116
+ enabled: true,
1117
+ active: failSecureActive,
1118
+ durationMs: durationMs,
1119
+ timeoutSec: node.failSecureTimeSec
1120
+ };
1121
+ }
1122
+
1123
+ failSecureBreachStartMs = null;
1124
+ failSecureActive = false;
1125
+ return {
1126
+ enabled: true,
1127
+ active: false,
1128
+ durationMs: 0,
1129
+ timeoutSec: node.failSecureTimeSec
1130
+ };
1131
+ }
1132
+
1133
+ function getMetersUsedForValidation(allActiveMeterIds) {
1134
+ if (node.operation === "expression") {
1135
+ return getExpressionTokens()
1136
+ .map(function (token) { return token.meterId; })
1137
+ .filter(function (id, index, arr) {
1138
+ return arr.indexOf(id) === index && !!node.meterStore[id];
1139
+ });
1140
+ }
1141
+
1142
+ if (node.operation === "primary-secondary") {
1143
+ const ids = getPrimarySecondaryMeterIds();
1144
+ return [ids.primaryMeterId, ids.secondaryMeterId].filter(function (id, index, arr) {
1145
+ return !!id && arr.indexOf(id) === index && !!node.meterStore[id];
1146
+ });
1147
+ }
1148
+
1149
+ return allActiveMeterIds.slice();
1150
+ }
1151
+
1152
+ node.on("input", function (msg, send, done) {
1153
+ send = send || function () {
1154
+ node.send.apply(node, arguments);
1155
+ };
1156
+
1157
+ try {
1158
+ const meterId = extractMeterId(msg, node.meterIdSource, node.customMeterPath);
1159
+
1160
+ if (!meterId) {
1161
+ node.status({
1162
+ fill: "red",
1163
+ shape: "ring",
1164
+ text: "meterId lipsă"
1165
+ });
1166
+ done(new Error("multi-grid: mesajul nu conține un meterId valid."));
1167
+ return;
1168
+ }
1169
+
1170
+ if (!isPlainObject(msg.payload)) {
1171
+ node.status({
1172
+ fill: "red",
1173
+ shape: "ring",
1174
+ text: "payload invalid"
1175
+ });
1176
+ done(new Error("multi-grid: msg.payload trebuie să fie un obiect."));
1177
+ return;
1178
+ }
1179
+
1180
+ const numericPayload = onlyNumericEntries(msg.payload);
1181
+
1182
+ if (!Object.keys(numericPayload).length) {
1183
+ node.status({
1184
+ fill: "red",
1185
+ shape: "ring",
1186
+ text: "fără valori numerice"
1187
+ });
1188
+ done(new Error("multi-grid: msg.payload nu conține valori numerice."));
1189
+ return;
1190
+ }
1191
+
1192
+ node.meterStore[meterId] = {
1193
+ payload: (function () {
1194
+ if (node.missingDataStrategy !== "use-last" || !node.meterStore[meterId] || !isPlainObject(node.meterStore[meterId].payload)) {
1195
+ return cloneObject(numericPayload);
1196
+ }
1197
+ const merged = cloneObject(node.meterStore[meterId].payload);
1198
+ Object.keys(numericPayload).forEach(function (key) {
1199
+ merged[key] = numericPayload[key];
1200
+ });
1201
+ return merged;
1202
+ })(),
1203
+ ts: Date.now(),
1204
+ metadata: extractMeterMetadata(msg, meterId)
1205
+ };
1206
+
1207
+ const health = buildHealthSnapshot();
1208
+ const meterIds = health.meter_ids;
1209
+ const validationMeterIds = getMetersUsedForValidation(meterIds);
1210
+
1211
+ if (node.strictCategoryCheck && validationMeterIds.length >= 2) {
1212
+ const validation = validateMeterCategoryConsistency(validationMeterIds, node.meterStore);
1213
+
1214
+ if (!validation.ok) {
1215
+ updateStatus("categorii diferite", health);
1216
+ node.warn("multi-grid: datele primite nu sunt din aceeași categorie. " + validation.reason);
1217
+ done();
1218
+ return;
1219
+ }
1220
+ }
1221
+
1222
+ updateStatus("", health);
1223
+
1224
+ if (node.operation === "primary-secondary") {
1225
+ const primarySecondaryIds = getPrimarySecondaryMeterIds();
1226
+ const primarySecondarySelection = selectPrimarySecondaryMeter();
1227
+ const failSecureState = evaluateFailSecurePrimaryInactive(primarySecondaryIds.primaryMeterId);
1228
+
1229
+ if (!primarySecondarySelection && !node.failSecureEnabled) {
1230
+ done();
1231
+ return;
1232
+ }
1233
+
1234
+ const outMsg = primarySecondarySelection ? (node.preserveOriginalMsg ? RED.util.cloneMessage(msg) : {}) : null;
1235
+ if (outMsg) {
1236
+ const aggregatedPayload = cloneObject(node.meterStore[primarySecondarySelection.selectedMeterId].payload || {});
1237
+ outMsg.topic = node.scadaServer ? "scada-server" : "multi-grid";
1238
+ outMsg.payload = node.scadaServer
1239
+ ? buildScadaPayload(aggregatedPayload)
1240
+ : node.controlExportReactive
1241
+ ? buildControlExportReactivePayload(aggregatedPayload)
1242
+ : aggregatedPayload;
1243
+ outMsg.operation = node.operation;
1244
+ outMsg.expression = "";
1245
+ outMsg.meter_count = meterIds.length;
1246
+ outMsg.meter_ids = meterIds.slice();
1247
+ outMsg.missing_data_strategy = node.missingDataStrategy;
1248
+ outMsg.health = {
1249
+ meter_count: health.meter_count,
1250
+ meter_ids: health.meter_ids.slice(),
1251
+ required_meter_ids: health.required_meter_ids.slice(),
1252
+ missing_meter_ids: health.missing_meter_ids.slice(),
1253
+ meter_age_ms: cloneObject(health.meter_age_ms)
1254
+ };
1255
+ outMsg.parts = meterIds.reduce(function (acc, id) {
1256
+ acc[id] = cloneObject(node.meterStore[id].payload);
1257
+ return acc;
1258
+ }, {});
1259
+ outMsg.updated_meter_id = meterId;
1260
+ outMsg.categories = validationMeterIds.reduce(function (acc, id) {
1261
+ const entry = node.meterStore[id];
1262
+ acc[id] = entry && entry.payload ? getPayloadCategories(entry.payload) : [];
1263
+ return acc;
1264
+ }, {});
1265
+ outMsg.group_by_category = node.groupByCategory;
1266
+ outMsg.scada_server = node.scadaServer;
1267
+ outMsg.control_export_reactive = node.controlExportReactive;
1268
+ sendToInfluxNode(buildInfluxMessage(aggregatedPayload, meterIds));
1269
+ delete outMsg.influx;
1270
+ delete outMsg.influx_meter_ids;
1271
+ delete outMsg.influx_meters;
1272
+ outMsg.primary_secondary = {
1273
+ primary_meter_id: primarySecondarySelection.primaryMeterId,
1274
+ secondary_meter_id: primarySecondarySelection.secondaryMeterId,
1275
+ selected_meter_id: primarySecondarySelection.selectedMeterId,
1276
+ primary_active: primarySecondarySelection.primaryActive,
1277
+ secondary_active: primarySecondarySelection.secondaryActive,
1278
+ timeout_ms: node.primarySecondaryTimeoutMs
1279
+ };
1280
+ }
1281
+
1282
+ if (node.failSecureEnabled) {
1283
+ const failMsg = {
1284
+ topic: "multi-grid-primary-health",
1285
+ payload: failSecureState.active ? 1 : 0,
1286
+ fail_secure: {
1287
+ active: failSecureState.active,
1288
+ duration_ms: failSecureState.durationMs,
1289
+ timeout_s: failSecureState.timeoutSec
1290
+ },
1291
+ primary_meter_id: primarySecondaryIds.primaryMeterId,
1292
+ secondary_meter_id: primarySecondaryIds.secondaryMeterId,
1293
+ selected_meter_id: primarySecondarySelection ? primarySecondarySelection.selectedMeterId : "",
1294
+ primary_active: !!(primarySecondarySelection && primarySecondarySelection.primaryActive),
1295
+ secondary_active: !!(primarySecondarySelection && primarySecondarySelection.secondaryActive),
1296
+ timeout_ms: node.primarySecondaryTimeoutMs
1297
+ };
1298
+ if (outMsg) {
1299
+ sendValueResult(send, outMsg, outMsg.payload, failMsg);
1300
+ } else {
1301
+ const messages = node.splitValueOutputs ? new Array(node.splitOutputCount).fill(null) : [null];
1302
+ messages.push(failMsg);
1303
+ send(messages);
1304
+ }
1305
+ } else if (outMsg) {
1306
+ sendValueResult(send, outMsg, outMsg.payload);
1307
+ }
1308
+ done();
1309
+ return;
1310
+ }
1311
+
1312
+ if (!canEmit(meterIds)) {
1313
+ done();
1314
+ return;
1315
+ }
1316
+
1317
+ let aggregatedPayload;
1318
+
1319
+ if (node.groupByCategory) {
1320
+ aggregatedPayload = node.operation === "expression"
1321
+ ? buildCategoryExpressionPayload()
1322
+ : buildCategoryAggregatePayload(meterIds);
1323
+ } else {
1324
+ aggregatedPayload = node.operation === "expression"
1325
+ ? buildExpressionPayload()
1326
+ : buildAggregatePayload(meterIds);
1327
+ }
1328
+
1329
+ const outMsg = node.preserveOriginalMsg ? RED.util.cloneMessage(msg) : {};
1330
+
1331
+ outMsg.topic = node.scadaServer ? "scada-server" : "multi-grid";
1332
+ outMsg.payload = node.scadaServer
1333
+ ? buildScadaPayload(aggregatedPayload)
1334
+ : node.controlExportReactive
1335
+ ? buildControlExportReactivePayload(aggregatedPayload)
1336
+ : aggregatedPayload;
1337
+ outMsg.operation = node.operation;
1338
+ outMsg.expression = node.operation === "expression" ? node.expression : "";
1339
+ outMsg.meter_count = meterIds.length;
1340
+ outMsg.meter_ids = meterIds.slice();
1341
+ outMsg.missing_data_strategy = node.missingDataStrategy;
1342
+ outMsg.health = {
1343
+ meter_count: health.meter_count,
1344
+ meter_ids: health.meter_ids.slice(),
1345
+ required_meter_ids: health.required_meter_ids.slice(),
1346
+ missing_meter_ids: health.missing_meter_ids.slice(),
1347
+ meter_age_ms: cloneObject(health.meter_age_ms)
1348
+ };
1349
+ outMsg.parts = meterIds.reduce(function (acc, id) {
1350
+ acc[id] = cloneObject(node.meterStore[id].payload);
1351
+ return acc;
1352
+ }, {});
1353
+ outMsg.updated_meter_id = meterId;
1354
+ outMsg.categories = validationMeterIds.reduce(function (acc, id) {
1355
+ const entry = node.meterStore[id];
1356
+ acc[id] = entry && entry.payload ? getPayloadCategories(entry.payload) : [];
1357
+ return acc;
1358
+ }, {});
1359
+ outMsg.group_by_category = node.groupByCategory;
1360
+ outMsg.scada_server = node.scadaServer;
1361
+ outMsg.control_export_reactive = node.controlExportReactive;
1362
+ sendToInfluxNode(buildInfluxMessage(aggregatedPayload, meterIds));
1363
+ delete outMsg.influx;
1364
+ delete outMsg.influx_meter_ids;
1365
+ delete outMsg.influx_meters;
1366
+
1367
+ sendValueResult(send, outMsg, outMsg.payload);
1368
+ done();
1369
+ } catch (err) {
1370
+ node.status({
1371
+ fill: "red",
1372
+ shape: "ring",
1373
+ text: err.message
1374
+ });
1375
+ done(err);
1376
+ }
1377
+ });
1378
+
1379
+ node.on("close", function (done) {
1380
+ node.meterStore = {};
1381
+ if (typeof done === "function") {
1382
+ done();
1383
+ }
1384
+ });
1385
+
1386
+ updateStatus();
1387
+
1388
+ });
1389
+ }
1390
+
1391
+ RED.nodes.registerType("multi-grid", MultiGridNode);
1392
+ };