@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,679 @@
1
+ module.exports = function (RED) {
2
+ "use strict";
3
+
4
+ const { runLicensedNode } = require("../lib/licensing/node-guard");
5
+
6
+ const net = require("net");
7
+ const phoenixProfile = require("./lib/phoenix-contact-profile");
8
+ const { compactKey } = require("./lib/integrations");
9
+ const influxMeasurementRegistry = new Map();
10
+
11
+ const TRANSACTION_ID = 1;
12
+ const PROTOCOL_ID = 0;
13
+ const MESSAGE_LENGTH = 6;
14
+ const FUNCTION_CODE = 3;
15
+ const SOCKET_TIMEOUT_MS = 5000;
16
+ const READ_BLOCK_RETRIES = 3;
17
+ const READ_BLOCK_RETRY_DELAY_MS = 250;
18
+
19
+ const MULTI_GRID_DEFAULT_FIELDS = [
20
+ "summary.power.total_w",
21
+ "summary.power.l1_w",
22
+ "summary.power.l2_w",
23
+ "summary.power.l3_w"
24
+ ];
25
+ const BESS_CONTROL_DEFAULT_FIELDS = MULTI_GRID_DEFAULT_FIELDS.slice();
26
+ const CONTROL_EXPORT_REACTIVE_DEFAULT_FIELDS = [
27
+ "summary.power.total_w",
28
+ "summary.power.l1_w",
29
+ "summary.power.l2_w",
30
+ "summary.power.l3_w",
31
+ "summary.reactive.total_var",
32
+ "summary.reactive.l1_var",
33
+ "summary.reactive.l2_var",
34
+ "summary.reactive.l3_var"
35
+ ];
36
+ const SCADA_DEFAULT_FIELDS = [
37
+ "summary.power.total_w",
38
+ "summary.reactive.total_var",
39
+ "summary.frequency_hz",
40
+ "summary.voltage.l1_l2_v",
41
+ "summary.voltage.l2_l3_v",
42
+ "summary.voltage.l3_l1_v"
43
+ ];
44
+
45
+ function toPositiveNumber(value, fallback) {
46
+ const n = Number(value);
47
+ return Number.isFinite(n) && n > 0 ? n : fallback;
48
+ }
49
+
50
+ function toPollInterval(value, fallback) {
51
+ const text = String(value || "").trim().toLowerCase();
52
+ if (text === "off" || text === "0") return 0;
53
+ const n = Number(value);
54
+ return Number.isFinite(n) && n > 0 ? n : fallback;
55
+ }
56
+
57
+ function wait(ms) {
58
+ return new Promise(function (resolve) {
59
+ setTimeout(resolve, ms);
60
+ });
61
+ }
62
+
63
+ function isRetryableReadError(err) {
64
+ const msg = String(err && err.message ? err.message : err || "").toLowerCase();
65
+ return msg.includes("timeout") ||
66
+ msg.includes("conexiune pierduta") ||
67
+ msg.includes("econnreset") ||
68
+ msg.includes("econnrefused") ||
69
+ msg.includes("etimedout") ||
70
+ msg.includes("fara raspuns");
71
+ }
72
+
73
+ function getByPath(obj, path) {
74
+ if (!path || path === "full") {
75
+ return obj;
76
+ }
77
+ return path.split(".").reduce(function (acc, key) {
78
+ return acc !== null && acc !== undefined ? acc[key] : undefined;
79
+ }, obj);
80
+ }
81
+
82
+ function setByPath(obj, path, value) {
83
+ const parts = String(path || "").split(".");
84
+ let target = obj;
85
+
86
+ parts.forEach(function (part, index) {
87
+ if (index === parts.length - 1) {
88
+ target[part] = value;
89
+ return;
90
+ }
91
+ if (!target[part] || typeof target[part] !== "object") {
92
+ target[part] = {};
93
+ }
94
+ target = target[part];
95
+ });
96
+ }
97
+
98
+ function normalizePayloadFields(fields, allowEmpty) {
99
+ if (Array.isArray(fields) && fields.length) {
100
+ const normalized = Array.from(new Set(fields.map(function (field) {
101
+ return String(field || "").trim();
102
+ }).filter(Boolean)));
103
+
104
+ if (normalized.includes("full")) {
105
+ return ["full"];
106
+ }
107
+ return normalized.length ? normalized : (allowEmpty ? [] : ["full"]);
108
+ }
109
+
110
+ if (typeof fields === "string" && fields.trim()) {
111
+ const trimmed = fields.trim();
112
+ if (trimmed[0] === "[") {
113
+ try {
114
+ return normalizePayloadFields(JSON.parse(trimmed), allowEmpty);
115
+ } catch (err) {
116
+ return [trimmed];
117
+ }
118
+ }
119
+ return [trimmed];
120
+ }
121
+
122
+ return allowEmpty ? [] : ["full"];
123
+ }
124
+
125
+ function sanitizePayloadFieldsForMode(fields, multiGrid, bessControl, controlExportReactive, scadaServer, influxDb, expertMode) {
126
+ const hasSpecialMode = !!(multiGrid || bessControl || controlExportReactive || scadaServer || influxDb);
127
+ const normalized = normalizePayloadFields(fields, !!(hasSpecialMode && expertMode));
128
+
129
+ if (multiGrid && normalized.includes("full")) {
130
+ return MULTI_GRID_DEFAULT_FIELDS.slice();
131
+ }
132
+ if (bessControl && normalized.includes("full")) {
133
+ return BESS_CONTROL_DEFAULT_FIELDS.slice();
134
+ }
135
+ if (controlExportReactive && normalized.includes("full")) {
136
+ return CONTROL_EXPORT_REACTIVE_DEFAULT_FIELDS.slice();
137
+ }
138
+ if (scadaServer && normalized.includes("full")) {
139
+ return SCADA_DEFAULT_FIELDS.slice();
140
+ }
141
+ if (influxDb && normalized.includes("full")) {
142
+ return ["full"];
143
+ }
144
+
145
+ return normalized;
146
+ }
147
+
148
+ function getPayloadDefinition(path) {
149
+ return phoenixProfile.PAYLOAD_FIELDS.find(function (field) {
150
+ return field.path === path;
151
+ }) || phoenixProfile.getField(path);
152
+ }
153
+
154
+ function getPayloadLabel(path) {
155
+ const definition = getPayloadDefinition(path);
156
+ return definition && definition.label ? definition.label : path;
157
+ }
158
+
159
+ function getPayloadUnit(path) {
160
+ const definition = getPayloadDefinition(path);
161
+ return definition && definition.unit ? definition.unit : "";
162
+ }
163
+
164
+ function getCompactPayloadKey(path) {
165
+ return compactKey(path);
166
+ }
167
+
168
+ function getScadaPayloadKey(path) {
169
+ const mapping = {
170
+ "summary.power.total_w": "active_power_w",
171
+ "summary.reactive.total_var": "reactive_power_var",
172
+ "summary.frequency_hz": "frequency_hz",
173
+ "summary.voltage.l1_l2_v": "voltage_l1_l2_v",
174
+ "summary.voltage.l2_l3_v": "voltage_l2_l3_v",
175
+ "summary.voltage.l3_l1_v": "voltage_l1_l3_v",
176
+ "summary.power.l1_w": "power_l1_w",
177
+ "summary.power.l2_w": "power_l2_w",
178
+ "summary.power.l3_w": "power_l3_w",
179
+ "summary.current.l1_a": "current_l1_a",
180
+ "summary.current.l2_a": "current_l2_a",
181
+ "summary.current.l3_a": "current_l3_a",
182
+ "summary.voltage.l1_n_v": "voltage_l1_n_v",
183
+ "summary.voltage.l2_n_v": "voltage_l2_n_v",
184
+ "summary.voltage.l3_n_v": "voltage_l3_n_v"
185
+ };
186
+ return mapping[path] || getCompactPayloadKey(path);
187
+ }
188
+
189
+ function flattenNumericFields(source, prefix, target) {
190
+ Object.keys(source || {}).forEach(function (key) {
191
+ const value = source[key];
192
+ const nextKey = prefix ? prefix + "_" + key : key;
193
+
194
+ if (typeof value === "number" && Number.isFinite(value)) {
195
+ target[nextKey] = value;
196
+ return;
197
+ }
198
+ if (typeof value === "boolean" || typeof value === "string") {
199
+ target[nextKey] = value;
200
+ return;
201
+ }
202
+ if (value && typeof value === "object" && !Array.isArray(value)) {
203
+ flattenNumericFields(value, nextKey, target);
204
+ }
205
+ });
206
+ }
207
+
208
+ function registerInfluxMeasurement(nodeId, identityKey) {
209
+ const key = String(identityKey || "").trim().toLowerCase();
210
+ if (!key) {
211
+ return;
212
+ }
213
+ if (!influxMeasurementRegistry.has(key)) {
214
+ influxMeasurementRegistry.set(key, new Set());
215
+ }
216
+ influxMeasurementRegistry.get(key).add(nodeId);
217
+ }
218
+
219
+ function unregisterInfluxMeasurement(nodeId, identityKey) {
220
+ const key = String(identityKey || "").trim().toLowerCase();
221
+ if (!key || !influxMeasurementRegistry.has(key)) {
222
+ return;
223
+ }
224
+ const ids = influxMeasurementRegistry.get(key);
225
+ ids.delete(nodeId);
226
+ if (!ids.size) {
227
+ influxMeasurementRegistry.delete(key);
228
+ }
229
+ }
230
+
231
+ function hasDuplicateInfluxMeasurement(nodeId, identityKey) {
232
+ const key = String(identityKey || "").trim().toLowerCase();
233
+ if (!key || !influxMeasurementRegistry.has(key)) {
234
+ return false;
235
+ }
236
+ const ids = influxMeasurementRegistry.get(key);
237
+ return ids.size > 1 || (ids.size === 1 && !ids.has(nodeId));
238
+ }
239
+
240
+ function setInfluxField(target, key, value) {
241
+ if (!key) {
242
+ return;
243
+ }
244
+ if (typeof value === "number" && Number.isFinite(value)) {
245
+ target[key] = value;
246
+ return;
247
+ }
248
+ if (typeof value === "boolean" || typeof value === "string") {
249
+ target[key] = value;
250
+ }
251
+ }
252
+
253
+ function buildInfluxFields(parsed, selectedFields) {
254
+ const fields = {};
255
+
256
+ if (selectedFields.includes("full")) {
257
+ flattenNumericFields(parsed && parsed.summary ? parsed.summary : {}, "", fields);
258
+ return fields;
259
+ }
260
+
261
+ selectedFields.forEach(function (field) {
262
+ const value = getByPath(parsed, field);
263
+ if (value && typeof value === "object" && !Array.isArray(value)) {
264
+ flattenNumericFields(value, getCompactPayloadKey(field), fields);
265
+ return;
266
+ }
267
+ setInfluxField(fields, getCompactPayloadKey(field), value);
268
+ });
269
+
270
+ return fields;
271
+ }
272
+
273
+ function formatSingleStatus(path, value) {
274
+ const unit = getPayloadUnit(path);
275
+ if (value === undefined || value === null) {
276
+ return getPayloadLabel(path) + ": n/a";
277
+ }
278
+ return getPayloadLabel(path) + ": " + String(value) + (unit ? " " + unit : "");
279
+ }
280
+
281
+ function dumpBuffer(buffer) {
282
+ return buffer.toString("hex").toUpperCase();
283
+ }
284
+
285
+ function buildRequestBuffer(unitId, startAddress, registerCount) {
286
+ const buffer = Buffer.alloc(12);
287
+ buffer.writeUInt16BE(TRANSACTION_ID, 0);
288
+ buffer.writeUInt16BE(PROTOCOL_ID, 2);
289
+ buffer.writeUInt16BE(MESSAGE_LENGTH, 4);
290
+ buffer.writeUInt8(unitId, 6);
291
+ buffer.writeUInt8(FUNCTION_CODE, 7);
292
+ buffer.writeUInt16BE(startAddress, 8);
293
+ buffer.writeUInt16BE(registerCount, 10);
294
+ return buffer;
295
+ }
296
+
297
+ function PhoenixContactNode(config) {
298
+ RED.nodes.createNode(this, config);
299
+ const node = this;
300
+
301
+ runLicensedNode(node, RED, config, function startLicensedRuntime() {
302
+
303
+ node.name = String(config.name || "").trim();
304
+ node.model = String(config.model || phoenixProfile.DEFAULT_PROFILE_ID).trim();
305
+ node.host = String(config.host || "").trim();
306
+ node.port = toPositiveNumber(config.port, 502);
307
+ node.unitId = toPositiveNumber(config.unitId, 1);
308
+ node.timestep = toPollInterval(config.timestep, 1000);
309
+ node.expertMode = !!config.expertMode;
310
+ node.multiGrid = !!config.multiGrid;
311
+ node.bessControl = !!config.bessControl;
312
+ node.controlExportReactive = !!config.controlExportReactive;
313
+ node.scadaServer = !!config.scadaServer;
314
+ node.influxDb = !!config.influxDb;
315
+ if (node.multiGrid) {
316
+ node.bessControl = false; node.controlExportReactive = false; node.scadaServer = false;
317
+ } else if (node.bessControl) {
318
+ node.controlExportReactive = false; node.scadaServer = false;
319
+ } else if (node.controlExportReactive) {
320
+ node.scadaServer = false;
321
+ }
322
+ node.logReadRetries = !!config.logReadRetries;
323
+ node.influxMeasurement = String(config.influxMeasurement || "").trim();
324
+ node.influxNode = String(config.influxNode || "").trim();
325
+ node.meterId = String(config.meterId || "").trim();
326
+ if (!node.meterId) {
327
+ node.meterId = "phoenix" + String(node.unitId);
328
+ }
329
+ node.payloadFields = sanitizePayloadFieldsForMode(config.payloadFields, node.multiGrid, node.bessControl, node.controlExportReactive, node.scadaServer, node.influxDb, node.expertMode);
330
+
331
+ let intervalHandle = null;
332
+ let startupHandle = null;
333
+ let running = false;
334
+ let closed = false;
335
+ let registeredInfluxIdentityKey = "";
336
+
337
+ function getEffectiveMeterId() {
338
+ return node.meterId || ("phoenix" + String(node.unitId));
339
+ }
340
+
341
+ function getInfluxMeasurement() {
342
+ return node.influxMeasurement || getEffectiveMeterId();
343
+ }
344
+
345
+ function getInfluxIdentityKey() {
346
+ return String(getInfluxMeasurement() || "").trim().toLowerCase() + "::" + String(getEffectiveMeterId() || "").trim().toLowerCase();
347
+ }
348
+
349
+ function hasInfluxMeasurementConflict() {
350
+ return node.influxDb && hasDuplicateInfluxMeasurement(node.id, getInfluxIdentityKey());
351
+ }
352
+
353
+ function readRegisterBlock(startAddress, registerCount) {
354
+ return new Promise(function (resolve, reject) {
355
+ const socket = new net.Socket();
356
+ let chunks = [];
357
+ let finished = false;
358
+
359
+ function done(err, result) {
360
+ if (finished) {
361
+ return;
362
+ }
363
+ finished = true;
364
+ try {
365
+ socket.destroy();
366
+ } catch (cleanupErr) {
367
+ // ignore cleanup errors
368
+ }
369
+ if (err) {
370
+ reject(err);
371
+ } else {
372
+ resolve(result);
373
+ }
374
+ }
375
+
376
+ socket.setTimeout(SOCKET_TIMEOUT_MS);
377
+ socket.connect(node.port, node.host, function () {
378
+ try {
379
+ socket.write(buildRequestBuffer(node.unitId, startAddress, registerCount));
380
+ } catch (err) {
381
+ done(err);
382
+ }
383
+ });
384
+
385
+ socket.on("data", function (data) {
386
+ chunks.push(data);
387
+ const total = Buffer.concat(chunks);
388
+
389
+ if (total.length >= 9) {
390
+ const functionCode = total.readUInt8(7);
391
+ if (functionCode === (FUNCTION_CODE | 0x80)) {
392
+ const exceptionCode = total.length > 8 ? total.readUInt8(8) : -1;
393
+ done(new Error("Modbus exception code: " + exceptionCode));
394
+ return;
395
+ }
396
+ if (functionCode !== FUNCTION_CODE) {
397
+ done(new Error("Function code invalid: " + functionCode));
398
+ return;
399
+ }
400
+
401
+ const byteCount = total.readUInt8(8);
402
+ const expectedLength = 9 + byteCount;
403
+ if (total.length >= expectedLength) {
404
+ if (byteCount !== registerCount * 2) {
405
+ done(new Error("Byte count invalid: " + byteCount));
406
+ return;
407
+ }
408
+ done(null, {
409
+ buffer: total.slice(0, expectedLength),
410
+ data: total.slice(9, expectedLength)
411
+ });
412
+ }
413
+ }
414
+ });
415
+
416
+ socket.on("timeout", function () {
417
+ done(new Error("conexiune pierduta: timeout"));
418
+ });
419
+ socket.on("error", function (err) {
420
+ done(new Error("conexiune pierduta: " + err.message));
421
+ });
422
+ socket.on("close", function () {
423
+ if (!finished && chunks.length === 0) {
424
+ done(new Error("conexiune pierduta: fara raspuns"));
425
+ }
426
+ });
427
+ });
428
+ }
429
+
430
+ async function readRegisterBlockWithRetry(startAddress, registerCount) {
431
+ let lastError = null;
432
+ for (let attempt = 1; attempt <= READ_BLOCK_RETRIES; attempt += 1) {
433
+ try {
434
+ return await readRegisterBlock(startAddress, registerCount);
435
+ } catch (err) {
436
+ lastError = err;
437
+ if (attempt >= READ_BLOCK_RETRIES || !isRetryableReadError(err)) {
438
+ throw err;
439
+ }
440
+ if (node.logReadRetries) {
441
+ node.warn(
442
+ "Phoenix Contact read retry " +
443
+ attempt +
444
+ "/" +
445
+ READ_BLOCK_RETRIES +
446
+ " pentru registrele " +
447
+ startAddress +
448
+ "-" +
449
+ (startAddress + registerCount - 1) +
450
+ ": " +
451
+ (err.message || String(err))
452
+ );
453
+ }
454
+ await wait(READ_BLOCK_RETRY_DELAY_MS);
455
+ }
456
+ }
457
+ throw lastError || new Error("citire Phoenix Contact esuata");
458
+ }
459
+
460
+ async function readMeter() {
461
+ const selectedFields = normalizePayloadFields(
462
+ node.payloadFields,
463
+ !!(node.expertMode && (node.multiGrid || node.bessControl || node.controlExportReactive || node.scadaServer || node.influxDb))
464
+ );
465
+ const ranges = phoenixProfile.buildReadRanges(selectedFields, false);
466
+ const parsed = {
467
+ summary: {},
468
+ registers: {},
469
+ meta: {
470
+ model: phoenixProfile.DEFAULT_PROFILE_ID,
471
+ device: "Phoenix Contact EEM-MA370",
472
+ unit_id: node.unitId,
473
+ timestamp: new Date().toISOString(),
474
+ ranges: ranges.map(function (range) {
475
+ return { start: range.start, count: range.count };
476
+ }),
477
+ raw_hex: []
478
+ }
479
+ };
480
+
481
+ for (let i = 0; i < ranges.length; i += 1) {
482
+ const range = ranges[i];
483
+ const block = await readRegisterBlockWithRetry(range.start, range.count);
484
+ parsed.meta.raw_hex.push(dumpBuffer(block.buffer));
485
+
486
+ range.items.forEach(function (item) {
487
+ const value = phoenixProfile.readValueFromRegisterData(block.data, range.start, item.definition);
488
+ setByPath(parsed, item.field, value);
489
+ });
490
+ }
491
+
492
+ return parsed;
493
+ }
494
+
495
+ function buildSingleMessage(parsed, field, value) {
496
+ const outMsg = {
497
+ payload_field: field,
498
+ phoenixContact: parsed,
499
+ source_id: node.meterId,
500
+ timestamp: parsed && parsed.meta ? parsed.meta.timestamp : new Date().toISOString()
501
+ };
502
+ outMsg.ip = node.host;
503
+ outMsg.host = node.host;
504
+ outMsg.port = node.port;
505
+ outMsg.unit_id = node.unitId;
506
+ outMsg.device_type = "phoenix_contact";
507
+ outMsg.model = parsed && parsed.meta ? parsed.meta.model : "";
508
+
509
+ if (node.multiGrid || node.bessControl || node.controlExportReactive) {
510
+ outMsg.topic = getEffectiveMeterId();
511
+ outMsg.meter_id = getEffectiveMeterId();
512
+ outMsg.payload = field === "full" ? parsed : value;
513
+ } else if (node.scadaServer) {
514
+ outMsg.topic = getEffectiveMeterId();
515
+ outMsg.meter_id = getEffectiveMeterId();
516
+ outMsg.payload = field === "full" ? parsed : value;
517
+ } else {
518
+ outMsg.topic = getPayloadLabel(field);
519
+ outMsg.payload = value;
520
+ }
521
+
522
+ return outMsg;
523
+ }
524
+
525
+ function buildInfluxMessage(parsed, selectedFields) {
526
+ const timestamp = parsed && parsed.meta ? parsed.meta.timestamp : new Date().toISOString();
527
+ const fields = buildInfluxFields(parsed, selectedFields);
528
+ const meterId = getEffectiveMeterId();
529
+ const measurement = getInfluxMeasurement();
530
+
531
+ return {
532
+ topic: meterId,
533
+ meter_id: meterId,
534
+ source_id: node.meterId,
535
+ payload_field: selectedFields.includes("full") ? "full" : (selectedFields[0] || "full"),
536
+ phoenixContact: parsed,
537
+ timestamp: timestamp,
538
+ payload: fields,
539
+ influx: {
540
+ measurement: measurement,
541
+ tags: {
542
+ device_type: "phoenix_contact",
543
+ meter_id: meterId,
544
+ host: node.host,
545
+ unit_id: String(node.unitId)
546
+ },
547
+ fields: fields,
548
+ timestamp: timestamp
549
+ }
550
+ };
551
+ }
552
+
553
+ function setStatusForSelection(selectedFields, parsed) {
554
+ if (selectedFields.includes("full")) {
555
+ node.status({ fill: "green", shape: "dot", text: "Valori principale" });
556
+ return;
557
+ }
558
+
559
+ if (selectedFields.length === 1) {
560
+ const field = selectedFields[0];
561
+ node.status({ fill: "green", shape: "dot", text: formatSingleStatus(field, getByPath(parsed, field)) });
562
+ return;
563
+ }
564
+
565
+ node.status({ fill: "green", shape: "dot", text: String(selectedFields.length) + " valori selectate" });
566
+ }
567
+
568
+ function setDisconnectedStatus(message) {
569
+ node.status({ fill: "red", shape: "ring", text: message });
570
+ }
571
+
572
+ function sendParsedResult(parsed) {
573
+ const selectedFields = normalizePayloadFields(
574
+ node.payloadFields,
575
+ !!(node.expertMode && (node.multiGrid || node.bessControl || node.controlExportReactive || node.scadaServer || node.influxDb))
576
+ );
577
+
578
+ setStatusForSelection(selectedFields, parsed);
579
+
580
+ const secondaryInflux = node.influxDb && !!(node.multiGrid || node.bessControl || node.controlExportReactive || node.scadaServer);
581
+ let influxMessage = null;
582
+ if (node.influxDb) {
583
+ if (hasInfluxMeasurementConflict()) {
584
+ node.status({ fill: "red", shape: "ring", text: "meter_id+measurement duplicat" });
585
+ node.error("Phoenix Contact InfluxDB: combinatie duplicata Meter ID + Measurement ('" + getEffectiveMeterId() + "' @ '" + getInfluxMeasurement() + "'). Configureaza un Meter ID unic pentru acelasi measurement.");
586
+ return;
587
+ }
588
+ influxMessage = buildInfluxMessage(parsed, selectedFields);
589
+ if (!secondaryInflux) {
590
+ node.send(influxMessage);
591
+ return;
592
+ }
593
+ }
594
+
595
+ if (selectedFields.includes("full")) {
596
+ node.send(buildSingleMessage(parsed, "full", parsed));
597
+ return;
598
+ }
599
+
600
+ if (node.multiGrid || node.bessControl || node.controlExportReactive || node.scadaServer) {
601
+ const aggregatedPayload = {};
602
+ selectedFields.forEach(function (field) {
603
+ const key = node.scadaServer ? getScadaPayloadKey(field) : getCompactPayloadKey(field);
604
+ aggregatedPayload[key] = getByPath(parsed, field);
605
+ });
606
+ const integratedMessage = buildSingleMessage(parsed, "selected", aggregatedPayload);
607
+ if (influxMessage) {
608
+ const target = RED.nodes.getNode(node.influxNode);
609
+ if (target && typeof target.receive === "function") target.receive(RED.util.cloneMessage(influxMessage));
610
+ else node.warn("Phoenix Contact: selecteaza un nod adt-influxdb pentru integrarea secundara.");
611
+ }
612
+ node.send(integratedMessage);
613
+ return;
614
+ }
615
+
616
+ node.send(selectedFields.map(function (field) {
617
+ return buildSingleMessage(parsed, field, getByPath(parsed, field));
618
+ }));
619
+ }
620
+
621
+ async function executeRead() {
622
+ if (closed || running) {
623
+ return;
624
+ }
625
+ running = true;
626
+
627
+ try {
628
+ node.status({ fill: "yellow", shape: "dot", text: "citire..." });
629
+ sendParsedResult(await readMeter());
630
+ } catch (err) {
631
+ setDisconnectedStatus(err.message);
632
+ node.error("Eroare Phoenix Contact (" + node.host + ":" + node.port + ", unitId=" + node.unitId + "): " + err.message);
633
+ } finally {
634
+ running = false;
635
+ }
636
+ }
637
+
638
+ if (!node.host) {
639
+ setDisconnectedStatus("IP lipsa");
640
+ node.error("Configuratie invalida: IP lipsa");
641
+ } else {
642
+ if (node.influxDb) {
643
+ registeredInfluxIdentityKey = getInfluxIdentityKey();
644
+ registerInfluxMeasurement(node.id, registeredInfluxIdentityKey);
645
+ if (hasInfluxMeasurementConflict()) {
646
+ node.status({ fill: "red", shape: "ring", text: "meter_id+measurement duplicat" });
647
+ node.error("Phoenix Contact InfluxDB: combinatie duplicata Meter ID + Measurement ('" + getEffectiveMeterId() + "' @ '" + getInfluxMeasurement() + "'). Configureaza un Meter ID unic pentru acelasi measurement.");
648
+ }
649
+ }
650
+ if (node.timestep > 0) {
651
+ startupHandle = setTimeout(executeRead, 0);
652
+ intervalHandle = setInterval(executeRead, node.timestep);
653
+ }
654
+ }
655
+
656
+ node.on("input", function () {
657
+ executeRead();
658
+ });
659
+
660
+ node.on("close", function (done) {
661
+ closed = true;
662
+ if (registeredInfluxIdentityKey) {
663
+ unregisterInfluxMeasurement(node.id, registeredInfluxIdentityKey);
664
+ registeredInfluxIdentityKey = "";
665
+ }
666
+ if (startupHandle) {
667
+ clearTimeout(startupHandle);
668
+ }
669
+ if (intervalHandle) {
670
+ clearInterval(intervalHandle);
671
+ }
672
+ done();
673
+ });
674
+
675
+ });
676
+ }
677
+
678
+ RED.nodes.registerType("phoenix-contact", PhoenixContactNode);
679
+ };