@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,1487 @@
1
+ module.exports = function (RED) {
2
+
3
+ "use strict";
4
+
5
+ const { runLicensedNode } = require("../lib/licensing/node-guard");
6
+
7
+
8
+ const http = require("http");
9
+
10
+ const https = require("https");
11
+
12
+ const { URL } = require("url");
13
+ const DEFAULT_MAX_QUEUE_SIZE = 50000;
14
+ const DEFAULT_BATCH_SIZE = 500;
15
+ const DEFAULT_FLUSH_INTERVAL_MS = 1000;
16
+ const DEFAULT_REQUEST_TIMEOUT_MS = 10000;
17
+ const DEFAULT_RETRY_BASE_MS = 1000;
18
+ const DEFAULT_RETRY_MAX_MS = 30000;
19
+ const DEFAULT_MIN_WRITE_INTERVAL_MS = 1000;
20
+ const DEFAULT_HEARTBEAT_MS = 60000;
21
+ const DEFAULT_ERROR_LOG_MAX_REPEATS = 10;
22
+ const DEFAULT_ERROR_LOG_WINDOW_MS = 60000;
23
+ const DEFAULT_ERROR_LOG_MUTE_MS = 300000;
24
+ const VALID_PRECISIONS = {
25
+ ns: 1,
26
+ us: 1000,
27
+ ms: 1000000,
28
+ s: 1000000000
29
+ };
30
+
31
+
32
+ function cleanString(value) {
33
+
34
+ return String(value || "").trim();
35
+
36
+ }
37
+
38
+
39
+ function positiveNumber(value, fallback) {
40
+
41
+ const n = Number(value);
42
+
43
+ return Number.isFinite(n) && n > 0 ? n : fallback;
44
+
45
+ }
46
+
47
+
48
+ function positiveInteger(value, fallback) {
49
+
50
+ const n = Math.round(Number(value));
51
+
52
+ return Number.isFinite(n) && n > 0 ? n : fallback;
53
+
54
+ }
55
+
56
+
57
+ function positiveIntegerWithLegacyDefault(value, fallback, legacyValues) {
58
+
59
+ const n = positiveInteger(value, fallback);
60
+ const legacy = Array.isArray(legacyValues) ? legacyValues : [];
61
+
62
+ return legacy.indexOf(n) !== -1 ? fallback : n;
63
+
64
+ }
65
+
66
+
67
+ function normalizePrecision(value) {
68
+
69
+ const precision = cleanString(value || "ns");
70
+
71
+ return VALID_PRECISIONS[precision] ? precision : "ns";
72
+
73
+ }
74
+
75
+
76
+ function nonNegativeInteger(value, fallback) {
77
+
78
+ const n = Math.round(Number(value));
79
+
80
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
81
+
82
+ }
83
+
84
+
85
+ function normalizeDropPolicy(value) {
86
+
87
+ const policy = cleanString(value || "drop_oldest");
88
+
89
+ return policy === "drop_newest" ? "drop_newest" : "drop_oldest";
90
+
91
+ }
92
+
93
+
94
+ function parseTags(value) {
95
+
96
+ const text = cleanString(value);
97
+
98
+ const tags = {};
99
+
100
+
101
+ if (!text) {
102
+
103
+ return tags;
104
+
105
+ }
106
+
107
+
108
+ if (text.charAt(0) === "{") {
109
+
110
+ try {
111
+
112
+ const parsed = JSON.parse(text);
113
+
114
+ if (isPlainObject(parsed)) {
115
+
116
+ Object.keys(parsed).forEach(function (key) {
117
+
118
+ const tagKey = cleanString(key);
119
+
120
+ const tagValue = cleanString(parsed[key]);
121
+
122
+ if (tagKey && tagValue) {
123
+
124
+ tags[tagKey] = tagValue;
125
+
126
+ }
127
+
128
+ });
129
+
130
+ return tags;
131
+
132
+ }
133
+
134
+ } catch (err) {
135
+
136
+ return tags;
137
+
138
+ }
139
+
140
+ }
141
+
142
+
143
+ text.split(",").forEach(function (part) {
144
+
145
+ const index = part.indexOf("=");
146
+
147
+ if (index <= 0) {
148
+
149
+ return;
150
+
151
+ }
152
+
153
+
154
+ const key = cleanString(part.slice(0, index));
155
+
156
+ const tagValue = cleanString(part.slice(index + 1));
157
+
158
+ if (key && tagValue) {
159
+
160
+ tags[key] = tagValue;
161
+
162
+ }
163
+
164
+ });
165
+
166
+
167
+ return tags;
168
+
169
+ }
170
+
171
+
172
+ function isPlainObject(value) {
173
+
174
+ return value !== null && typeof value === "object" && !Array.isArray(value);
175
+
176
+ }
177
+
178
+
179
+ function sanitizeKey(value) {
180
+
181
+ return cleanString(value)
182
+
183
+ .replace(/\s+/g, "_")
184
+
185
+ .replace(/[^A-Za-z0-9_.-]/g, "_");
186
+
187
+ }
188
+
189
+
190
+ function escapeMeasurement(value) {
191
+
192
+ return cleanString(value).replace(/\\/g, "\\\\").replace(/ /g, "\\ ").replace(/,/g, "\\,");
193
+
194
+ }
195
+
196
+
197
+ function escapeTag(value) {
198
+
199
+ return cleanString(value)
200
+
201
+ .replace(/\\/g, "\\\\")
202
+
203
+ .replace(/ /g, "\\ ")
204
+
205
+ .replace(/,/g, "\\,")
206
+
207
+ .replace(/=/g, "\\=");
208
+
209
+ }
210
+
211
+
212
+ function escapeFieldKey(value) {
213
+
214
+ return cleanString(value).replace(/\\/g, "\\\\").replace(/ /g, "\\ ").replace(/,/g, "\\,").replace(/=/g, "\\=");
215
+
216
+ }
217
+
218
+
219
+ function escapeStringField(value) {
220
+
221
+ return String(value).replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
222
+
223
+ }
224
+
225
+
226
+ function fieldValueToLineProtocol(value) {
227
+
228
+ if (typeof value === "number" && Number.isFinite(value)) {
229
+
230
+ return String(value);
231
+
232
+ }
233
+
234
+
235
+ if (typeof value === "string" && value.trim() !== "") {
236
+
237
+ const parsed = Number(value);
238
+
239
+ if (Number.isFinite(parsed)) {
240
+
241
+ return String(parsed);
242
+
243
+ }
244
+
245
+ return "";
246
+
247
+ }
248
+
249
+
250
+ return "";
251
+
252
+ }
253
+
254
+
255
+ function timestampToNs(value) {
256
+
257
+ if (value === undefined || value === null || value === "") {
258
+
259
+ return "";
260
+
261
+ }
262
+
263
+
264
+ if (value instanceof Date) {
265
+
266
+ const ms = value.getTime();
267
+
268
+ return Number.isFinite(ms) ? String(ms * 1000000) : "";
269
+
270
+ }
271
+
272
+
273
+ if (typeof value === "number" && Number.isFinite(value)) {
274
+
275
+ if (value > 1000000000000000000) {
276
+
277
+ return String(Math.round(value));
278
+
279
+ }
280
+
281
+ if (value > 1000000000000000) {
282
+
283
+ return String(Math.round(value * 1000));
284
+
285
+ }
286
+
287
+ if (value > 1000000000000) {
288
+
289
+ return String(Math.round(value * 1000000));
290
+
291
+ }
292
+
293
+ return String(Math.round(value * 1000000000));
294
+
295
+ }
296
+
297
+
298
+ const parsed = Date.parse(String(value));
299
+
300
+ return Number.isFinite(parsed) ? String(parsed * 1000000) : "";
301
+
302
+ }
303
+
304
+
305
+ function timestampToPrecision(value, precision) {
306
+
307
+ const ns = Number(timestampToNs(value));
308
+
309
+ if (!Number.isFinite(ns)) {
310
+
311
+ return "";
312
+
313
+ }
314
+
315
+ const factor = VALID_PRECISIONS[normalizePrecision(precision)] || 1;
316
+
317
+ return String(Math.round(ns / factor));
318
+
319
+ }
320
+
321
+
322
+ function flattenNumericFields(source, prefix, target) {
323
+
324
+ Object.keys(source || {}).forEach(function (key) {
325
+
326
+ const value = source[key];
327
+
328
+ const nextKey = prefix ? prefix + "_" + sanitizeKey(key) : sanitizeKey(key);
329
+
330
+ if (!nextKey) {
331
+
332
+ return;
333
+
334
+ }
335
+
336
+
337
+ if (typeof value === "number" && Number.isFinite(value)) {
338
+
339
+ target[nextKey] = value;
340
+
341
+ return;
342
+
343
+ }
344
+
345
+
346
+ if (typeof value === "string" && value.trim() !== "") {
347
+
348
+ const parsed = Number(value);
349
+
350
+ if (Number.isFinite(parsed)) {
351
+
352
+ target[nextKey] = parsed;
353
+
354
+ }
355
+
356
+ return;
357
+
358
+ }
359
+
360
+
361
+ if (isPlainObject(value)) {
362
+
363
+ flattenNumericFields(value, nextKey, target);
364
+
365
+ }
366
+
367
+ });
368
+
369
+ }
370
+
371
+
372
+ function normalizePoint(input, defaults) {
373
+
374
+ const point = isPlainObject(input) ? input : {};
375
+
376
+ const payload = isPlainObject(point.payload) ? point.payload : {};
377
+
378
+ const tags = Object.assign({}, defaults.tags, point.tags || payload.tags || {});
379
+
380
+ const fields = {};
381
+
382
+ const measurement = cleanString(point.measurement || payload.measurement || defaults.measurement);
383
+
384
+ const timestamp = timestampToPrecision(point.timestamp || payload.timestamp || point.time || payload.time, defaults.precision);
385
+
386
+
387
+ if (isPlainObject(point.fields)) {
388
+
389
+ flattenNumericFields(point.fields, "", fields);
390
+
391
+ } else if (isPlainObject(payload.fields)) {
392
+
393
+ flattenNumericFields(payload.fields, "", fields);
394
+
395
+ } else if (isPlainObject(payload)) {
396
+
397
+ flattenNumericFields(payload, "", fields);
398
+
399
+ delete fields.tags;
400
+
401
+ delete fields.fields;
402
+
403
+ delete fields.measurement;
404
+
405
+ delete fields.timestamp;
406
+
407
+ delete fields.time;
408
+
409
+ } else if (typeof point.payload === "number" && Number.isFinite(point.payload)) {
410
+
411
+ fields.value = point.payload;
412
+
413
+ } else if (typeof point.payload === "string" && point.payload.trim() !== "") {
414
+
415
+ const parsed = Number(point.payload);
416
+
417
+ if (Number.isFinite(parsed)) {
418
+
419
+ fields.value = parsed;
420
+
421
+ }
422
+
423
+ }
424
+
425
+
426
+ if (point.topic && !tags.topic) {
427
+
428
+ tags.topic = cleanString(point.topic);
429
+
430
+ }
431
+
432
+ if (point.meter_id && !tags.meter_id) {
433
+
434
+ tags.meter_id = cleanString(point.meter_id);
435
+
436
+ }
437
+
438
+ if (point.inverter_id && !tags.inverter_id) {
439
+
440
+ tags.inverter_id = cleanString(point.inverter_id);
441
+
442
+ }
443
+
444
+ if (point.battery_id && !tags.battery_id) {
445
+
446
+ tags.battery_id = cleanString(point.battery_id);
447
+
448
+ }
449
+
450
+
451
+ return {
452
+
453
+ measurement: measurement,
454
+
455
+ tags: tags,
456
+
457
+ fields: fields,
458
+
459
+ timestamp: timestamp
460
+
461
+ };
462
+
463
+ }
464
+
465
+
466
+ function pointToLineProtocol(point) {
467
+
468
+ const measurement = escapeMeasurement(point.measurement);
469
+
470
+ const tagParts = [];
471
+
472
+ const fieldParts = [];
473
+
474
+
475
+ if (!measurement) {
476
+
477
+ return "";
478
+
479
+ }
480
+
481
+
482
+ Object.keys(point.tags || {}).sort().forEach(function (key) {
483
+
484
+ const value = point.tags[key];
485
+ const tagKey = escapeTag(key);
486
+
487
+ if (!tagKey || value === undefined || value === null || value === "") {
488
+
489
+ return;
490
+
491
+ }
492
+
493
+ tagParts.push(tagKey + "=" + escapeTag(value));
494
+
495
+ });
496
+
497
+
498
+ Object.keys(point.fields || {}).sort().forEach(function (key) {
499
+
500
+ const encoded = fieldValueToLineProtocol(point.fields[key]);
501
+ const fieldKey = escapeFieldKey(key);
502
+
503
+ if (!fieldKey || !encoded) {
504
+
505
+ return;
506
+
507
+ }
508
+
509
+ fieldParts.push(fieldKey + "=" + encoded);
510
+
511
+ });
512
+
513
+
514
+ if (!fieldParts.length) {
515
+
516
+ return "";
517
+
518
+ }
519
+
520
+
521
+ return measurement +
522
+
523
+ (tagParts.length ? "," + tagParts.join(",") : "") +
524
+
525
+ " " +
526
+
527
+ fieldParts.join(",") +
528
+
529
+ (point.timestamp ? " " + point.timestamp : "");
530
+
531
+ }
532
+
533
+
534
+ function findUnescapedSpace(value) {
535
+
536
+ for (let i = 0; i < value.length; i += 1) {
537
+
538
+ if (value.charAt(i) === " " && value.charAt(i - 1) !== "\\") {
539
+
540
+ return i;
541
+
542
+ }
543
+
544
+ }
545
+
546
+ return -1;
547
+
548
+ }
549
+
550
+
551
+ function splitLineProtocol(line) {
552
+
553
+ const firstSpace = findUnescapedSpace(line);
554
+
555
+ if (firstSpace < 0) {
556
+
557
+ return {
558
+ seriesKey: line,
559
+ fieldsKey: ""
560
+ };
561
+
562
+ }
563
+
564
+ const rest = line.slice(firstSpace + 1);
565
+ const secondSpace = findUnescapedSpace(rest);
566
+
567
+ return {
568
+ seriesKey: line.slice(0, firstSpace),
569
+ fieldsKey: secondSpace < 0 ? rest : rest.slice(0, secondSpace)
570
+ };
571
+
572
+ }
573
+
574
+
575
+ function extractPoints(msg, defaults) {
576
+
577
+ const source = (msg.influx === true || msg.influxdb === true)
578
+
579
+ ? msg
580
+
581
+ : (msg.influx || msg.influxdb || (isPlainObject(msg.payload) && (msg.payload.influx || msg.payload.influxdb)) || msg.payload);
582
+
583
+ const candidates = Array.isArray(source)
584
+
585
+ ? source
586
+
587
+ : (source && Array.isArray(source.points) ? source.points : [source]);
588
+
589
+
590
+ return candidates
591
+
592
+ .map(function (candidate) {
593
+
594
+ if (candidate === msg.payload && !isPlainObject(candidate)) {
595
+
596
+ return normalizePoint(msg, defaults);
597
+
598
+ }
599
+
600
+ return normalizePoint(candidate, defaults);
601
+
602
+ })
603
+
604
+ .map(pointToLineProtocol)
605
+
606
+ .filter(Boolean);
607
+
608
+ }
609
+
610
+
611
+ function buildWriteEndpoint(connection) {
612
+
613
+ let endpoint;
614
+
615
+ try {
616
+
617
+ endpoint = new URL(connection.url);
618
+
619
+ } catch (err) {
620
+
621
+ throw new Error("URL InfluxDB invalid");
622
+
623
+ }
624
+
625
+ endpoint.pathname = endpoint.pathname.replace(/\/$/, "") + "/api/v2/write";
626
+ endpoint.searchParams.set("org", connection.org);
627
+ endpoint.searchParams.set("bucket", connection.bucket);
628
+ endpoint.searchParams.set("precision", normalizePrecision(connection.precision));
629
+
630
+ return endpoint;
631
+
632
+ }
633
+
634
+
635
+ function writeInfluxLines(connection, lines) {
636
+
637
+ return new Promise(function (resolve, reject) {
638
+
639
+ if (!connection.url || !connection.org || !connection.bucket || !connection.token) {
640
+
641
+ reject(new Error("config incompleta"));
642
+
643
+ return;
644
+
645
+ }
646
+
647
+ let endpoint;
648
+
649
+ try {
650
+
651
+ endpoint = buildWriteEndpoint(connection);
652
+
653
+ } catch (err) {
654
+
655
+ reject(err);
656
+
657
+ return;
658
+
659
+ }
660
+
661
+ const body = lines.join("\n");
662
+
663
+ const transport = endpoint.protocol === "https:" ? https : http;
664
+
665
+ const request = transport.request(endpoint, {
666
+
667
+ method: "POST",
668
+
669
+ headers: {
670
+
671
+ "Authorization": "Token " + connection.token,
672
+
673
+ "Content-Type": "text/plain; charset=utf-8",
674
+
675
+ "Content-Length": Buffer.byteLength(body)
676
+
677
+ }
678
+
679
+ }, function (response) {
680
+
681
+ const chunks = [];
682
+
683
+ response.on("data", function (chunk) {
684
+
685
+ chunks.push(chunk);
686
+
687
+ });
688
+
689
+ response.on("end", function () {
690
+
691
+ const responseBody = Buffer.concat(chunks).toString("utf8");
692
+
693
+ if (response.statusCode >= 200 && response.statusCode < 300) {
694
+
695
+ resolve({
696
+ statusCode: response.statusCode,
697
+ body: responseBody
698
+ });
699
+
700
+ return;
701
+
702
+ }
703
+
704
+ reject(new Error("InfluxDB HTTP " + response.statusCode + (responseBody ? ": " + responseBody : "")));
705
+
706
+ });
707
+
708
+ });
709
+
710
+ request.on("error", reject);
711
+ request.setTimeout(positiveInteger(connection.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS), function () {
712
+ request.destroy(new Error("timeout InfluxDB write"));
713
+ });
714
+ request.write(body);
715
+ request.end();
716
+
717
+ });
718
+
719
+ }
720
+
721
+
722
+ function getNodeConnection(node) {
723
+
724
+ return {
725
+ url: node.url,
726
+ org: node.org,
727
+ bucket: node.bucket,
728
+ precision: node.precision,
729
+ token: node.token,
730
+ requestTimeoutMs: node.requestTimeoutMs
731
+ };
732
+
733
+ }
734
+
735
+
736
+ function buildLogLine(connection, event, fields, extraTags) {
737
+
738
+ return pointToLineProtocol({
739
+ measurement: "Logs",
740
+ tags: Object.assign({
741
+ source: "node_red",
742
+ node_type: "adt-influxdb",
743
+ event: cleanString(event) || "unknown"
744
+ }, extraTags || {}),
745
+ fields: fields,
746
+ timestamp: timestampToPrecision(Date.now(), connection.precision)
747
+ });
748
+
749
+ }
750
+
751
+
752
+ async function writeLogPoint(connection, event, fields, extraTags) {
753
+
754
+ const line = buildLogLine(connection, event, fields, extraTags);
755
+
756
+ if (!line) {
757
+
758
+ return null;
759
+
760
+ }
761
+
762
+ return writeInfluxLines(connection, [line]);
763
+
764
+ }
765
+
766
+
767
+ function nodeToken(node) {
768
+
769
+ return cleanString((node && node.token) || (node && node.credentials && node.credentials.token));
770
+
771
+ }
772
+
773
+
774
+ function resolveTestConnection(body) {
775
+
776
+ const serverId = cleanString(body.server);
777
+ const nodeId = cleanString(body.nodeId || body.id);
778
+ const deployedNode = nodeId ? RED.nodes.getNode(nodeId) : null;
779
+ const serverNode = (serverId ? RED.nodes.getNode(serverId) : null) || (deployedNode && deployedNode.server);
780
+ const bucket = cleanString(body.bucket || (deployedNode && deployedNode.bucket));
781
+
782
+ if (serverNode) {
783
+
784
+ return {
785
+ url: cleanString(serverNode.url),
786
+ org: cleanString(serverNode.org),
787
+ bucket: bucket,
788
+ precision: normalizePrecision(body.precision || (deployedNode && deployedNode.precision)),
789
+ token: nodeToken(serverNode),
790
+ requestTimeoutMs: positiveInteger(body.requestTimeoutMs || (deployedNode && deployedNode.requestTimeoutMs), DEFAULT_REQUEST_TIMEOUT_MS)
791
+ };
792
+
793
+ }
794
+
795
+ if (deployedNode) {
796
+
797
+ return {
798
+ url: cleanString(body.url || deployedNode.url),
799
+ org: cleanString(body.org || deployedNode.org),
800
+ bucket: bucket,
801
+ precision: normalizePrecision(body.precision || deployedNode.precision),
802
+ token: nodeToken(deployedNode),
803
+ requestTimeoutMs: positiveInteger(body.requestTimeoutMs || deployedNode.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS)
804
+ };
805
+
806
+ }
807
+
808
+ return {
809
+ url: cleanString(body.url),
810
+ org: cleanString(body.org),
811
+ bucket: bucket,
812
+ precision: normalizePrecision(body.precision),
813
+ token: cleanString(body.token),
814
+ requestTimeoutMs: positiveInteger(body.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS)
815
+ };
816
+
817
+ }
818
+
819
+
820
+ RED.httpAdmin.post("/adt-influxdb/test", async function (req, res) {
821
+
822
+ const body = req && req.body && typeof req.body === "object" ? req.body : {};
823
+ const connection = resolveTestConnection(body);
824
+
825
+ try {
826
+
827
+ const result = await writeLogPoint(connection, "connection_test", {
828
+ connection_test: 1,
829
+ connection_error: 0
830
+ }, {
831
+ test: "manual",
832
+ bucket: connection.bucket || ""
833
+ });
834
+
835
+ res.json({
836
+ ok: true,
837
+ statusCode: result.statusCode,
838
+ bucket: connection.bucket,
839
+ measurement: "Logs",
840
+ precision: connection.precision
841
+ });
842
+
843
+ } catch (err) {
844
+
845
+ res.json({
846
+ ok: false,
847
+ error: err && err.message ? err.message : String(err || "Eroare necunoscuta")
848
+ });
849
+
850
+ }
851
+
852
+ });
853
+
854
+
855
+ function InfluxDbServerNode(config) {
856
+
857
+ RED.nodes.createNode(this, config);
858
+
859
+ const node = this;
860
+
861
+ node.name = cleanString(config.name);
862
+
863
+ node.url = cleanString(config.url);
864
+
865
+ node.org = cleanString(config.org);
866
+
867
+ node.token = cleanString(node.credentials && node.credentials.token);
868
+
869
+ }
870
+
871
+
872
+ function InfluxDbNode(config) {
873
+
874
+ RED.nodes.createNode(this, config);
875
+
876
+ const node = this;
877
+
878
+ runLicensedNode(node, RED, config, function startLicensedRuntime() {
879
+
880
+
881
+ node.name = cleanString(config.name);
882
+
883
+ node.server = RED.nodes.getNode(config.server);
884
+
885
+ node.url = node.server ? cleanString(node.server.url) : cleanString(config.url);
886
+
887
+ node.org = node.server ? cleanString(node.server.org) : cleanString(config.org);
888
+
889
+ node.bucket = cleanString(config.bucket);
890
+
891
+ node.measurement = cleanString(config.measurement || "dispozitiv");
892
+
893
+ node.precision = normalizePrecision(config.precision);
894
+
895
+ node.staticTags = parseTags(config.staticTags);
896
+
897
+ node.batchSize = positiveIntegerWithLegacyDefault(config.batchSize, DEFAULT_BATCH_SIZE, [1]);
898
+
899
+ node.flushIntervalMs = positiveInteger(config.flushIntervalMs, DEFAULT_FLUSH_INTERVAL_MS);
900
+
901
+ node.maxQueueSize = positiveInteger(config.maxQueueSize, DEFAULT_MAX_QUEUE_SIZE);
902
+
903
+ node.requestTimeoutMs = positiveInteger(config.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS);
904
+
905
+ node.retryBaseMs = positiveInteger(config.retryBaseMs, DEFAULT_RETRY_BASE_MS);
906
+
907
+ node.retryMaxMs = positiveInteger(config.retryMaxMs, DEFAULT_RETRY_MAX_MS);
908
+
909
+ node.minWriteIntervalMs = nonNegativeInteger(config.minWriteIntervalMs, DEFAULT_MIN_WRITE_INTERVAL_MS);
910
+
911
+ node.heartbeatMs = nonNegativeInteger(config.heartbeatMs, DEFAULT_HEARTBEAT_MS);
912
+
913
+ node.dropPolicy = normalizeDropPolicy(config.dropPolicy);
914
+
915
+ node.errorLogMaxRepeats = positiveInteger(config.errorLogMaxRepeats, DEFAULT_ERROR_LOG_MAX_REPEATS);
916
+
917
+ node.errorLogWindowMs = positiveInteger(config.errorLogWindowMs, DEFAULT_ERROR_LOG_WINDOW_MS);
918
+
919
+ node.errorLogMuteMs = positiveInteger(config.errorLogMuteMs, DEFAULT_ERROR_LOG_MUTE_MS);
920
+
921
+ node.debugMode = !!config.debugMode;
922
+
923
+ node.statusOutput = !!config.statusOutput || !!config.debugMode;
924
+
925
+ node.includeTopicTag = config.includeTopicTag !== false;
926
+
927
+
928
+ node.token = node.server ? cleanString(node.server.token) : cleanString(node.credentials && node.credentials.token);
929
+
930
+ const queue = [];
931
+ const seriesState = new Map();
932
+ const errorLogState = new Map();
933
+
934
+ let timer = null;
935
+ let retryTimer = null;
936
+
937
+ let flushing = false;
938
+
939
+ let closed = false;
940
+ let retryAttempt = 0;
941
+ let totalDropped = 0;
942
+ let totalThrottled = 0;
943
+ let lastHttpError = "";
944
+ let hasActiveErrorLogState = false;
945
+
946
+
947
+ function writeLines(lines) {
948
+ return writeInfluxLines(getNodeConnection(node), lines);
949
+
950
+ }
951
+
952
+
953
+ function statusText(text) {
954
+
955
+ const suffix = node.debugMode
956
+ ? " | q " + queue.length + " | drop " + totalDropped + " | skip " + totalThrottled + (retryAttempt ? " | retry " + retryAttempt : "")
957
+ : "";
958
+
959
+ return text + suffix;
960
+
961
+ }
962
+
963
+
964
+ function emitStatus(event, payload) {
965
+
966
+ if (!node.statusOutput) {
967
+ return;
968
+ }
969
+
970
+ node.send({
971
+ topic: "influxdb/" + event,
972
+ payload: Object.assign({
973
+ event: event,
974
+ node: node.name || "adt-influxdb",
975
+ bucket: node.bucket,
976
+ measurement: node.measurement,
977
+ precision: node.precision,
978
+ queue_length: queue.length,
979
+ retry_attempt: retryAttempt,
980
+ dropped_total: totalDropped,
981
+ throttled_total: totalThrottled,
982
+ drop_policy: node.dropPolicy,
983
+ last_error: lastHttpError || null,
984
+ timestamp: new Date().toISOString()
985
+ }, payload || {})
986
+ });
987
+
988
+ }
989
+
990
+
991
+ function errorLogKey(event, extraTags) {
992
+
993
+ const tags = extraTags || {};
994
+
995
+ return [
996
+ sanitizeKey(event || "state"),
997
+ sanitizeKey(tags.reason || "unknown"),
998
+ sanitizeKey(tags.error_class || "")
999
+ ].join("|");
1000
+
1001
+ }
1002
+
1003
+
1004
+ function writeNodeLog(event, fields, extraTags) {
1005
+
1006
+ writeLogPoint(getNodeConnection(node), event, fields, Object.assign({
1007
+ node: node.name || "adt-influxdb",
1008
+ bucket: node.bucket || "",
1009
+ measurement: node.measurement || ""
1010
+ }, extraTags || {})).catch(function () {
1011
+ // Log writes are best-effort to avoid recursive error storms.
1012
+ });
1013
+
1014
+ }
1015
+
1016
+
1017
+ function logState(event, fields, extraTags) {
1018
+
1019
+ const logFields = Object.assign({}, fields || {});
1020
+ const tags = extraTags || {};
1021
+ const isError = Number(logFields.error) === 1;
1022
+
1023
+ if (!isError) {
1024
+
1025
+ if (!hasActiveErrorLogState && !errorLogState.size) {
1026
+ return;
1027
+ }
1028
+
1029
+ errorLogState.clear();
1030
+ hasActiveErrorLogState = false;
1031
+ logFields.error = 0;
1032
+ logFields.recovered = 1;
1033
+ logFields.muted = 0;
1034
+ logFields.suppressed_count = 0;
1035
+ writeNodeLog(event, logFields, tags);
1036
+ return;
1037
+
1038
+ }
1039
+
1040
+ hasActiveErrorLogState = true;
1041
+
1042
+ const nowMs = Date.now();
1043
+ const key = errorLogKey(event, tags);
1044
+ const state = errorLogState.get(key) || {
1045
+ count: 0,
1046
+ windowStartMs: nowMs,
1047
+ mutedUntilMs: 0,
1048
+ suppressedCount: 0
1049
+ };
1050
+
1051
+ if (state.mutedUntilMs > nowMs) {
1052
+
1053
+ state.suppressedCount += 1;
1054
+ errorLogState.set(key, state);
1055
+ return;
1056
+
1057
+ }
1058
+
1059
+ if (nowMs - state.windowStartMs > node.errorLogWindowMs) {
1060
+
1061
+ state.count = 0;
1062
+ state.windowStartMs = nowMs;
1063
+ state.suppressedCount = 0;
1064
+ state.mutedUntilMs = 0;
1065
+
1066
+ }
1067
+
1068
+ state.count += 1;
1069
+
1070
+ if (state.count > node.errorLogMaxRepeats) {
1071
+
1072
+ state.suppressedCount += 1;
1073
+ state.mutedUntilMs = nowMs + node.errorLogMuteMs;
1074
+ logFields.muted = 1;
1075
+ logFields.suppressed_count = state.suppressedCount;
1076
+ errorLogState.set(key, state);
1077
+ writeNodeLog(event, logFields, Object.assign({
1078
+ muted_reason: "repeated_error"
1079
+ }, tags));
1080
+ state.suppressedCount = 0;
1081
+ return;
1082
+
1083
+ }
1084
+
1085
+ logFields.muted = 0;
1086
+ logFields.suppressed_count = 0;
1087
+ errorLogState.set(key, state);
1088
+ writeNodeLog(event, logFields, tags);
1089
+
1090
+ }
1091
+
1092
+
1093
+ function clearRetryTimer() {
1094
+
1095
+ if (retryTimer) {
1096
+ clearTimeout(retryTimer);
1097
+ retryTimer = null;
1098
+ }
1099
+
1100
+ }
1101
+
1102
+
1103
+ function trimQueueToLimit() {
1104
+
1105
+ if (queue.length <= node.maxQueueSize) {
1106
+ return 0;
1107
+ }
1108
+
1109
+ const overflow = queue.length - node.maxQueueSize;
1110
+
1111
+ if (node.dropPolicy === "drop_newest") {
1112
+
1113
+ queue.splice(node.maxQueueSize, overflow);
1114
+
1115
+ } else {
1116
+
1117
+ queue.splice(0, overflow);
1118
+
1119
+ }
1120
+
1121
+ totalDropped += overflow;
1122
+ return overflow;
1123
+
1124
+ }
1125
+
1126
+
1127
+ function filterRepeatedLines(lines, nowMs) {
1128
+
1129
+ const accepted = [];
1130
+ let skippedInterval = 0;
1131
+ let skippedHeartbeat = 0;
1132
+
1133
+ lines.forEach(function (line) {
1134
+
1135
+ const parsed = splitLineProtocol(line);
1136
+ const seriesKey = parsed.seriesKey;
1137
+ const fieldsKey = parsed.fieldsKey;
1138
+ const previous = seriesState.get(seriesKey);
1139
+
1140
+ if (previous) {
1141
+
1142
+ const elapsedMs = nowMs - previous.writtenAtMs;
1143
+ const changed = previous.fieldsKey !== fieldsKey;
1144
+
1145
+ if (changed && node.minWriteIntervalMs > 0 && elapsedMs < node.minWriteIntervalMs) {
1146
+
1147
+ skippedInterval += 1;
1148
+ return;
1149
+
1150
+ }
1151
+
1152
+ if (!changed) {
1153
+
1154
+ const holdMs = node.heartbeatMs > 0 ? node.heartbeatMs : node.minWriteIntervalMs;
1155
+
1156
+ if (holdMs > 0 && elapsedMs < holdMs) {
1157
+
1158
+ skippedHeartbeat += 1;
1159
+ return;
1160
+
1161
+ }
1162
+
1163
+ }
1164
+
1165
+ }
1166
+
1167
+ seriesState.set(seriesKey, {
1168
+ fieldsKey: fieldsKey,
1169
+ writtenAtMs: nowMs
1170
+ });
1171
+ accepted.push(line);
1172
+
1173
+ });
1174
+
1175
+ const skipped = skippedInterval + skippedHeartbeat;
1176
+ totalThrottled += skipped;
1177
+
1178
+ return {
1179
+ lines: accepted,
1180
+ skipped: skipped,
1181
+ skippedInterval: skippedInterval,
1182
+ skippedHeartbeat: skippedHeartbeat
1183
+ };
1184
+
1185
+ }
1186
+
1187
+
1188
+ function scheduleRetry() {
1189
+
1190
+ if (closed || retryTimer) {
1191
+ return;
1192
+ }
1193
+
1194
+ const waitMs = Math.min(node.retryBaseMs * Math.pow(2, retryAttempt), node.retryMaxMs);
1195
+ retryTimer = setTimeout(function () {
1196
+ retryTimer = null;
1197
+ flushQueue("retry");
1198
+ }, waitMs);
1199
+
1200
+ }
1201
+
1202
+
1203
+ async function flushQueue(reason, options) {
1204
+ const opts = options || {};
1205
+ const allowClosed = !!opts.allowClosed;
1206
+
1207
+ if (flushing || !queue.length || (closed && !allowClosed)) {
1208
+
1209
+ return;
1210
+
1211
+ }
1212
+
1213
+
1214
+ if (!node.url || !node.org || !node.bucket || !node.token) {
1215
+
1216
+ lastHttpError = "config incompleta";
1217
+ node.status({ fill: "red", shape: "ring", text: statusText("config incompleta") });
1218
+ logState("config_error", {
1219
+ error: 1,
1220
+ write_error: 0,
1221
+ config_error: 1
1222
+ }, {
1223
+ reason: "config_incomplete"
1224
+ });
1225
+ emitStatus("error", {
1226
+ error: lastHttpError,
1227
+ reason: reason || ""
1228
+ });
1229
+
1230
+ return;
1231
+
1232
+ }
1233
+
1234
+
1235
+ flushing = true;
1236
+
1237
+
1238
+ const batch = queue.splice(0, node.batchSize);
1239
+
1240
+
1241
+ try {
1242
+
1243
+ const result = await writeLines(batch);
1244
+ retryAttempt = 0;
1245
+ lastHttpError = "";
1246
+ clearRetryTimer();
1247
+ logState("write_ok", {
1248
+ error: 0,
1249
+ write_error: 0,
1250
+ config_error: 0
1251
+ }, {
1252
+ reason: reason || "write"
1253
+ });
1254
+
1255
+ node.status({
1256
+
1257
+ fill: "green",
1258
+
1259
+ shape: "dot",
1260
+
1261
+ text: statusText(String(batch.length) + " puncte trimise" + (reason ? " (" + reason + ")" : ""))
1262
+
1263
+ });
1264
+ emitStatus("write", {
1265
+ reason: reason || "",
1266
+ points: batch.length,
1267
+ http_status: result && result.statusCode ? result.statusCode : null
1268
+ });
1269
+
1270
+ } catch (err) {
1271
+
1272
+ Array.prototype.unshift.apply(queue, batch);
1273
+ const dropped = trimQueueToLimit();
1274
+ retryAttempt += 1;
1275
+ scheduleRetry();
1276
+ lastHttpError = err.message;
1277
+
1278
+ node.status({ fill: "red", shape: "ring", text: statusText(err.message) });
1279
+ logState("write_error", {
1280
+ error: 1,
1281
+ write_error: 1,
1282
+ config_error: 0
1283
+ }, {
1284
+ reason: reason || "write",
1285
+ error_class: sanitizeKey(err && err.code ? err.code : "http_or_network")
1286
+ });
1287
+ emitStatus("error", {
1288
+ reason: reason || "",
1289
+ error: err.message,
1290
+ dropped: dropped
1291
+ });
1292
+
1293
+ node.error(err.message);
1294
+
1295
+ } finally {
1296
+
1297
+ flushing = false;
1298
+
1299
+ }
1300
+
1301
+ }
1302
+
1303
+ async function drainQueue(reason) {
1304
+ let previousLength = -1;
1305
+
1306
+ while (flushing) {
1307
+ await new Promise(function (resolve) {
1308
+ setTimeout(resolve, 25);
1309
+ });
1310
+ }
1311
+
1312
+ while (queue.length && queue.length !== previousLength) {
1313
+ previousLength = queue.length;
1314
+ await flushQueue(reason, { allowClosed: true });
1315
+
1316
+ while (flushing) {
1317
+ await new Promise(function (resolve) {
1318
+ setTimeout(resolve, 25);
1319
+ });
1320
+ }
1321
+ }
1322
+ }
1323
+
1324
+
1325
+ timer = setInterval(function () {
1326
+
1327
+ flushQueue("timer");
1328
+
1329
+ }, node.flushIntervalMs);
1330
+
1331
+
1332
+ node.on("input", function (msg, send, done) {
1333
+
1334
+ done = typeof done === "function" ? done : function () {};
1335
+
1336
+ try {
1337
+
1338
+ const defaults = {
1339
+
1340
+ measurement: node.measurement,
1341
+
1342
+ tags: Object.assign({}, node.staticTags),
1343
+
1344
+ precision: node.precision
1345
+
1346
+ };
1347
+
1348
+
1349
+ if (node.includeTopicTag && msg.topic && !defaults.tags.topic) {
1350
+
1351
+ defaults.tags.topic = cleanString(msg.topic);
1352
+
1353
+ }
1354
+
1355
+
1356
+ const rawLines = extractPoints(msg, defaults);
1357
+
1358
+
1359
+ if (!rawLines.length) {
1360
+
1361
+ node.status({ fill: "yellow", shape: "ring", text: statusText("fara campuri numerice") });
1362
+ emitStatus("empty", {
1363
+ reason: "no_numeric_fields"
1364
+ });
1365
+
1366
+ done();
1367
+
1368
+ return;
1369
+
1370
+ }
1371
+
1372
+
1373
+ const filtered = filterRepeatedLines(rawLines, Date.now());
1374
+ const lines = filtered.lines;
1375
+
1376
+ if (!lines.length) {
1377
+
1378
+ node.status({ fill: "grey", shape: "ring", text: statusText("scriere amanata") });
1379
+ emitStatus("throttle", {
1380
+ skipped: filtered.skipped,
1381
+ skipped_interval: filtered.skippedInterval,
1382
+ skipped_heartbeat: filtered.skippedHeartbeat,
1383
+ min_write_interval_ms: node.minWriteIntervalMs,
1384
+ heartbeat_ms: node.heartbeatMs
1385
+ });
1386
+
1387
+ done();
1388
+
1389
+ return;
1390
+
1391
+ }
1392
+
1393
+
1394
+ Array.prototype.push.apply(queue, lines);
1395
+ if (filtered.skipped > 0) {
1396
+ emitStatus("throttle", {
1397
+ skipped: filtered.skipped,
1398
+ skipped_interval: filtered.skippedInterval,
1399
+ skipped_heartbeat: filtered.skippedHeartbeat,
1400
+ accepted: lines.length,
1401
+ min_write_interval_ms: node.minWriteIntervalMs,
1402
+ heartbeat_ms: node.heartbeatMs
1403
+ });
1404
+ }
1405
+ const dropped = trimQueueToLimit();
1406
+ if (dropped > 0) {
1407
+ node.warn("Coada InfluxDB a depasit limita, au fost eliminate " + dropped + " puncte vechi.");
1408
+ emitStatus("drop", {
1409
+ dropped: dropped
1410
+ });
1411
+ }
1412
+
1413
+ node.status({ fill: "blue", shape: "dot", text: statusText(String(queue.length) + " in queue") });
1414
+
1415
+
1416
+ if (queue.length >= node.batchSize || msg.flush === true || (msg.payload && msg.payload.flush === true)) {
1417
+
1418
+ flushQueue("batch").then(function () {
1419
+
1420
+ done();
1421
+
1422
+ }).catch(done);
1423
+
1424
+ return;
1425
+
1426
+ }
1427
+
1428
+
1429
+ done();
1430
+
1431
+ } catch (err) {
1432
+
1433
+ node.status({ fill: "red", shape: "ring", text: err.message });
1434
+
1435
+ done(err);
1436
+
1437
+ }
1438
+
1439
+ });
1440
+
1441
+
1442
+ node.on("close", function (removed, done) {
1443
+
1444
+ closed = true;
1445
+
1446
+ if (timer) {
1447
+
1448
+ clearInterval(timer);
1449
+
1450
+ timer = null;
1451
+
1452
+ }
1453
+ clearRetryTimer();
1454
+ drainQueue("close").finally(function () {
1455
+ done();
1456
+ });
1457
+
1458
+ });
1459
+
1460
+
1461
+ });
1462
+ }
1463
+
1464
+
1465
+ RED.nodes.registerType("adt-influxdb-server", InfluxDbServerNode, {
1466
+
1467
+ credentials: {
1468
+
1469
+ token: { type: "password" }
1470
+
1471
+ }
1472
+
1473
+ });
1474
+
1475
+
1476
+ RED.nodes.registerType("adt-influxdb", InfluxDbNode, {
1477
+
1478
+ credentials: {
1479
+
1480
+ token: { type: "password" }
1481
+
1482
+ }
1483
+
1484
+ });
1485
+
1486
+ };
1487
+