@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,2674 @@
1
+ <script type="text/javascript">
2
+
3
+
4
+ (function() {
5
+
6
+
7
+ const EXPERT_LONG_PRESS_MS = 5000;
8
+
9
+ const COMPACT_ROW_THRESHOLD = 6;
10
+
11
+
12
+ let expertLongPressTimer = null;
13
+
14
+ let expertLongPressTriggered = false;
15
+
16
+ let expertModeUnlockedState = false;
17
+
18
+
19
+
20
+ function normalizeDebugMode(mode) {
21
+
22
+ if (mode === "off" || mode === "normal" || mode === "verbose") {
23
+
24
+ return mode;
25
+
26
+ }
27
+
28
+
29
+ if (mode === "errors_only" || mode === "state_changes") {
30
+
31
+ return "normal";
32
+
33
+ }
34
+
35
+
36
+ if (mode === "always") {
37
+
38
+ return "verbose";
39
+
40
+ }
41
+
42
+
43
+ return "off";
44
+
45
+ }
46
+
47
+
48
+
49
+ function computeOutputsCount(rowCount, debugMode) {
50
+
51
+ const base = 1;
52
+
53
+ const debugExtra = normalizeDebugMode(debugMode) === "off" ? 0 : 1;
54
+
55
+ return base + debugExtra;
56
+
57
+ }
58
+
59
+
60
+
61
+ function updateOutputCount(node) {
62
+
63
+ const debugMode = $("#node-input-debugMode").val();
64
+
65
+ const outputs = computeOutputsCount(1, debugMode);
66
+
67
+
68
+ node.outputs = outputs;
69
+
70
+ $("#node-multi-inverter-outputs-info").text(String(outputs));
71
+
72
+ }
73
+
74
+
75
+
76
+ function toggleRampValueVisibility() {
77
+
78
+
79
+ const rampType = $("#node-input-rampType").val();
80
+ const rampFromGlobal = $("#node-input-rampFromGlobal").is(":checked");
81
+ const rampEnabled = rampFromGlobal || rampType !== "none";
82
+
83
+ $("#node-row-rampType").toggle(!rampFromGlobal);
84
+ $("#node-row-rampGlobalType").toggle(rampFromGlobal);
85
+ $("#node-row-rampGlobalUpValue").toggle(rampFromGlobal);
86
+ $("#node-row-rampGlobalDownValue").toggle(rampFromGlobal);
87
+ $("#node-row-rampUpValue").toggle(rampEnabled && !rampFromGlobal);
88
+ $("#node-row-rampDownValue").toggle(rampEnabled && !rampFromGlobal);
89
+
90
+ if (rampFromGlobal) {
91
+
92
+ return;
93
+
94
+ }
95
+
96
+
97
+ if (rampType === "none") {
98
+
99
+ return;
100
+
101
+ }
102
+
103
+
104
+ let label = "Ramp Value";
105
+
106
+ if (rampType === "time") label = "Ramp Time (s)";
107
+
108
+ if (rampType === "gradient_pct") label = "Ramp Gradient (%/s)";
109
+
110
+ if (rampType === "gradient_w" || rampType === "gradient_vars" || rampType === "gradient_var") {
111
+
112
+ label = "Ramp Gradient (W/s & VAR/s)";
113
+
114
+ }
115
+
116
+
117
+ $("#node-label-rampUpValue").text(label.replace("Ramp ", "Ramp Up "));
118
+ $("#node-label-rampDownValue").text(label.replace("Ramp ", "Ramp Down "));
119
+
120
+
121
+ }
122
+
123
+
124
+
125
+ function toggleAdvancedRows() {
126
+
127
+ $("#mi-advanced-section").toggle(!!expertModeUnlockedState);
128
+
129
+
130
+ }
131
+
132
+
133
+ function updateCompactRows() {
134
+
135
+ const rowCount = $("#node-input-inverters-container .inverter-row").length;
136
+
137
+ $(".mi-inverters-container").toggleClass("mi-compact", rowCount >= COMPACT_ROW_THRESHOLD);
138
+
139
+ }
140
+
141
+
142
+
143
+ function toggleMissingSiteLimitRows() {
144
+
145
+
146
+ const behavior = $("#node-input-missingSiteLimitBehavior").val();
147
+
148
+ $("#node-row-missingSiteLimitValue").toggle(behavior === "fixed_value");
149
+
150
+
151
+ }
152
+
153
+
154
+
155
+ function toggleStaleTelemetryRows() {
156
+
157
+
158
+ const behavior = $("#node-input-staleTelemetryBehavior").val();
159
+
160
+ $("#node-row-staleTelemetryFixedValue").toggle(behavior === "fixed_value");
161
+
162
+
163
+ }
164
+
165
+
166
+
167
+ function normalizeTopics(text) {
168
+
169
+
170
+ return String(text || "")
171
+
172
+ .split(",")
173
+
174
+ .map(function (s) { return s.trim(); })
175
+
176
+ .filter(Boolean)
177
+
178
+ .filter(function (v, i, arr) { return arr.indexOf(v) === i; })
179
+
180
+ .join(",");
181
+
182
+
183
+ }
184
+
185
+
186
+
187
+ function clearExpertLongPressTimer() {
188
+
189
+ if (expertLongPressTimer) {
190
+
191
+ clearTimeout(expertLongPressTimer);
192
+
193
+ expertLongPressTimer = null;
194
+
195
+ }
196
+
197
+ }
198
+
199
+
200
+
201
+ function unlockExpertMode() {
202
+
203
+ expertModeUnlockedState = true;
204
+
205
+ toggleAdvancedRows();
206
+
207
+
208
+ if (RED && RED.notify) {
209
+
210
+ RED.notify("Setarile suplimentare au fost activate.", "success");
211
+
212
+ }
213
+
214
+
215
+ }
216
+
217
+
218
+
219
+ function validateRows() {
220
+
221
+
222
+ const rows = $("#node-input-inverters-container .inverter-row");
223
+
224
+ const seen = new Set();
225
+
226
+ const errors = [];
227
+
228
+
229
+ rows.each(function(index) {
230
+
231
+
232
+ const row = $(this);
233
+
234
+ const idInput = row.find(".inv-id");
235
+
236
+ const minInput = row.find(".inv-minw");
237
+
238
+ const maxInput = row.find(".inv-maxw");
239
+ const nominalInput = row.find(".inv-nominalw");
240
+
241
+ const enabled = row.find(".inv-enabled").is(":checked");
242
+
243
+
244
+ const id = String(idInput.val() || "").trim();
245
+
246
+ const minW = Number(minInput.val() || 0);
247
+
248
+ const maxW = Number(maxInput.val() || 0);
249
+ const nominalW = Number(nominalInput.val() || maxW || 0);
250
+
251
+
252
+ row.removeClass("mi-row-invalid");
253
+
254
+ idInput.removeClass("mi-invalid");
255
+
256
+ minInput.removeClass("mi-invalid");
257
+
258
+ maxInput.removeClass("mi-invalid");
259
+ nominalInput.removeClass("mi-invalid");
260
+
261
+
262
+ if (!enabled) {
263
+
264
+ return;
265
+
266
+ }
267
+
268
+
269
+ if (!id) {
270
+
271
+ errors.push("Rand " + (index + 1) + ": ID gol");
272
+
273
+ row.addClass("mi-row-invalid");
274
+
275
+ idInput.addClass("mi-invalid");
276
+
277
+ } else if (seen.has(id)) {
278
+
279
+ errors.push("ID duplicat: " + id);
280
+
281
+ row.addClass("mi-row-invalid");
282
+
283
+ idInput.addClass("mi-invalid");
284
+
285
+ } else {
286
+
287
+ seen.add(id);
288
+
289
+ }
290
+
291
+
292
+ if (!Number.isFinite(minW) || minW < 0) {
293
+
294
+ errors.push("Rand " + (index + 1) + ": Min W invalid");
295
+
296
+ row.addClass("mi-row-invalid");
297
+
298
+ minInput.addClass("mi-invalid");
299
+
300
+ }
301
+
302
+
303
+ if (!Number.isFinite(maxW) || maxW <= 0) {
304
+
305
+ errors.push("Rand " + (index + 1) + ": Max W trebuie sa fie > 0");
306
+
307
+ row.addClass("mi-row-invalid");
308
+
309
+ maxInput.addClass("mi-invalid");
310
+
311
+ }
312
+
313
+
314
+ if (Number.isFinite(minW) && Number.isFinite(maxW) && minW > maxW) {
315
+
316
+ errors.push("Rand " + (index + 1) + ": Min W > Max W");
317
+
318
+ row.addClass("mi-row-invalid");
319
+
320
+ minInput.addClass("mi-invalid");
321
+
322
+ maxInput.addClass("mi-invalid");
323
+
324
+ }
325
+
326
+
327
+ if (!Number.isFinite(nominalW) || nominalW <= 0) {
328
+
329
+ errors.push("Rand " + (index + 1) + ": Nominal W trebuie sa fie > 0");
330
+
331
+ row.addClass("mi-row-invalid");
332
+
333
+ nominalInput.addClass("mi-invalid");
334
+
335
+ }
336
+
337
+
338
+ if (Number.isFinite(nominalW) && Number.isFinite(maxW) && nominalW < maxW) {
339
+
340
+ errors.push("Rand " + (index + 1) + ": Nominal W trebuie sa fie >= Max W");
341
+
342
+ row.addClass("mi-row-invalid");
343
+
344
+ nominalInput.addClass("mi-invalid");
345
+
346
+ maxInput.addClass("mi-invalid");
347
+
348
+ }
349
+
350
+
351
+ });
352
+
353
+
354
+ if (errors.length) {
355
+
356
+ $("#node-multi-inverter-validation")
357
+
358
+ .text(errors[0])
359
+
360
+ .show();
361
+
362
+ } else {
363
+
364
+ $("#node-multi-inverter-validation").hide().text("");
365
+
366
+ }
367
+
368
+
369
+ return errors;
370
+
371
+
372
+ }
373
+
374
+
375
+
376
+ function validateAdvancedInputs() {
377
+
378
+ const topics = String($("#node-input-siteLimitTopics").val() || "").trim();
379
+
380
+ if (!topics) {
381
+
382
+ $("#node-multi-inverter-validation")
383
+
384
+ .text("Site Limit Topics nu poate fi gol.")
385
+
386
+ .show();
387
+
388
+ return false;
389
+
390
+ }
391
+
392
+ return true;
393
+
394
+ }
395
+
396
+
397
+
398
+ function rowTemplate(index, inv) {
399
+
400
+ const safe = inv || {};
401
+
402
+
403
+ return (
404
+
405
+ '<div class="form-row inverter-row mi-row">' +
406
+
407
+ '<div class="mi-col-index">' + (index + 1) + '</div>' +
408
+
409
+ '<input type="text" class="inv-id mi-col-id" placeholder="inv' + (index + 1) + '" value="' + String(safe.id || "") + '">' +
410
+
411
+ '<input type="number" class="inv-minw mi-col-min" placeholder="0" value="' + Number(safe.minW || 0) + '">' +
412
+
413
+ '<input type="number" class="inv-maxw mi-col-max" placeholder="10000" value="' + Number(safe.maxW || 10000) + '">' +
414
+
415
+ '<input type="number" class="inv-nominalw mi-col-nominal" placeholder="10000" value="' + Number(safe.nominalPowerW || safe.nominalW || safe.maxW || 10000) + '">' +
416
+
417
+ '<label class="mi-col-enabled mi-enabled-toggle">' +
418
+
419
+ '<input type="checkbox" class="inv-enabled" ' + (safe.enabled !== false ? "checked" : "") + '>' +
420
+
421
+ '</label>' +
422
+
423
+ '<button type="button" class="button button-small mi-row-remove" title="Remove">Remove</button>' +
424
+
425
+ '</div>'
426
+
427
+ );
428
+
429
+ }
430
+
431
+
432
+
433
+ function rebuildRowIndexes() {
434
+
435
+ $("#node-input-inverters-container .inverter-row").each(function(index) {
436
+
437
+ const row = $(this);
438
+
439
+ row.find(".mi-col-index").text(String(index + 1));
440
+
441
+ const idInput = row.find(".inv-id");
442
+
443
+ if (!String(idInput.attr("placeholder") || "").trim()) {
444
+
445
+ idInput.attr("placeholder", "inv" + (index + 1));
446
+
447
+ }
448
+
449
+ });
450
+
451
+ }
452
+
453
+
454
+
455
+ RED.nodes.registerType("multi-inverter", {
456
+
457
+
458
+ category: "Atelierul de Tehnologii",
459
+
460
+
461
+ color: "#FFD580",
462
+
463
+
464
+ defaults: {
465
+ license: { value: "", type: "atelier-license", required: true },
466
+
467
+
468
+ name: { value: "" },
469
+
470
+
471
+ strategy: { value: "current_power" },
472
+
473
+
474
+ siteLimitTopics: { value: "site-limit,control-export,control-reactive" },
475
+
476
+
477
+ telemetryTimeoutMs: {
478
+
479
+ value: 6000,
480
+
481
+ validate: function(v) {
482
+
483
+ return Number(v) >= 0;
484
+
485
+ }
486
+
487
+ },
488
+
489
+ maxFutureTimestampMs: {
490
+ value: 2000,
491
+ validate: function(v) {
492
+ return Number(v) >= 0;
493
+ }
494
+ },
495
+
496
+
497
+ siteLimitTimeoutMs: {
498
+
499
+ value: 8000,
500
+
501
+ validate: function(v) {
502
+
503
+ return Number(v) >= 0;
504
+
505
+ }
506
+
507
+ },
508
+
509
+
510
+ missingSiteLimitBehavior: { value: "set_zero" },
511
+
512
+
513
+ missingSiteLimitValueW: {
514
+
515
+ value: 0,
516
+
517
+ validate: function(v) {
518
+
519
+ return Number(v) >= 0;
520
+
521
+ }
522
+
523
+ },
524
+
525
+
526
+ staleTelemetryBehavior: { value: "set_inverter_min" },
527
+
528
+
529
+ staleTelemetryFixedValueW: {
530
+
531
+ value: 0,
532
+
533
+ validate: function(v) {
534
+
535
+ return Number(v) >= 0;
536
+
537
+ }
538
+
539
+ },
540
+
541
+
542
+ waitForFreshTelemetryOnStartup: { value: true },
543
+
544
+
545
+ recalculateOnTelemetry: { value: true },
546
+
547
+ telemetryFilterAlpha: {
548
+ value: 0.35,
549
+ validate: function(v) {
550
+ const n = Number(v);
551
+ return Number.isFinite(n) && n >= 0.05 && n <= 1;
552
+ }
553
+ },
554
+
555
+
556
+ rampType: { value: "none" },
557
+
558
+
559
+ rampFromGlobal: { value: false },
560
+
561
+
562
+ rampTypeGlobalVar: { value: "scada_ramp_type" },
563
+
564
+
565
+ rampUpGlobalVar: { value: "scada_ramp_up_value" },
566
+
567
+
568
+ rampDownGlobalVar: { value: "scada_ramp_down_value" },
569
+
570
+
571
+ rampValue: {
572
+
573
+ value: 0,
574
+
575
+ validate: function(v) {
576
+
577
+ return Number(v) >= 0;
578
+
579
+ }
580
+
581
+ },
582
+
583
+ rampUpValue: {
584
+ value: 0,
585
+ validate: function(v) {
586
+ return Number(v) >= 0;
587
+ }
588
+ },
589
+
590
+ rampDownValue: {
591
+ value: 0,
592
+ validate: function(v) {
593
+ return Number(v) >= 0;
594
+ }
595
+ },
596
+
597
+
598
+ allocationSmoothingAlpha: {
599
+
600
+ value: 0.35,
601
+
602
+ validate: function(v) {
603
+
604
+ const n = Number(v);
605
+
606
+ return Number.isFinite(n) && n > 0 && n <= 1;
607
+
608
+ }
609
+
610
+ },
611
+
612
+ allocationDeadbandW: {
613
+ value: 300,
614
+ validate: function(v) {
615
+ return Number(v) >= 0;
616
+ }
617
+ },
618
+
619
+
620
+ telemetryRecalcDebounceMs: {
621
+
622
+ value: 350,
623
+
624
+ validate: function(v) {
625
+
626
+ return Number(v) >= 0;
627
+
628
+ }
629
+
630
+ },
631
+
632
+
633
+ debugMode: { value: "off" },
634
+
635
+ publishTotalPvGlobal: { value: true },
636
+
637
+ totalPvGlobalVar: { value: "total_pv" },
638
+
639
+ influxDb: { value: false },
640
+ influxMeasurement: { value: "multi_inverter" },
641
+ influxNode: { value: "" },
642
+
643
+
644
+ inverters: {
645
+
646
+ value: [
647
+
648
+ { id: "inv1", minW: 0, maxW: 10000, nominalPowerW: 10000, enabled: true }
649
+
650
+ ],
651
+
652
+ validate: function(v) {
653
+
654
+
655
+ const arr = Array.isArray(v) ? v : [];
656
+
657
+ const seen = new Set();
658
+
659
+
660
+ for (let i = 0; i < arr.length; i += 1) {
661
+
662
+ const inv = arr[i] || {};
663
+
664
+
665
+ if (inv.enabled === false) {
666
+
667
+ continue;
668
+
669
+ }
670
+
671
+
672
+ const id = String(inv.id || "").trim();
673
+
674
+ const minW = Number(inv.minW || 0);
675
+
676
+ const maxW = Number(inv.maxW || 0);
677
+ const nominalW = Number(inv.nominalPowerW || inv.nominalW || maxW || 0);
678
+
679
+
680
+ if (!id) return false;
681
+
682
+ if (!Number.isFinite(minW) || minW < 0) return false;
683
+
684
+ if (!Number.isFinite(maxW) || maxW <= 0) return false;
685
+ if (!Number.isFinite(nominalW) || nominalW <= 0) return false;
686
+
687
+ if (minW > maxW) return false;
688
+ if (nominalW < maxW) return false;
689
+
690
+ if (seen.has(id)) return false;
691
+
692
+
693
+ seen.add(id);
694
+
695
+ }
696
+
697
+
698
+ return arr.length > 0;
699
+
700
+ }
701
+
702
+ }
703
+
704
+
705
+ },
706
+
707
+
708
+ inputs: 1,
709
+
710
+
711
+ outputs: 1,
712
+
713
+
714
+ icon: "bridge.svg",
715
+
716
+
717
+ label: function() {
718
+
719
+ return this.name || "multi-inverter";
720
+
721
+ },
722
+
723
+
724
+ paletteLabel: "multi-inverter",
725
+
726
+
727
+
728
+ oneditprepare: function() {
729
+
730
+
731
+ const node = this;
732
+ const influxSelect = $("#node-input-influxNode").empty().append($("<option></option>").val("").text("Selecteaza adt-influxdb"));
733
+ RED.nodes.eachNode(function(candidate) {
734
+ if (candidate.type === "adt-influxdb") influxSelect.append($("<option></option>").val(candidate.id).text(candidate.name || "adt-influxdb"));
735
+ });
736
+ influxSelect.val(node.influxNode || "");
737
+ function toggleInfluxRows() {
738
+ $("#node-row-influxMeasurement,#node-row-influxNode").toggle($("#node-input-influxDb").is(":checked"));
739
+ }
740
+ $("#node-input-influxDb").off(".miInflux").on("change.miInflux", toggleInfluxRows);
741
+ toggleInfluxRows();
742
+
743
+ const container = $("#node-input-inverters-container");
744
+
745
+
746
+
747
+ function bindRowEvents() {
748
+
749
+ container.find(".inv-id, .inv-enabled, .inv-minw, .inv-maxw, .inv-nominalw")
750
+
751
+ .off(".miRows")
752
+
753
+ .on("input.miRows change.miRows", function() {
754
+
755
+ validateRows();
756
+
757
+ updateOutputCount(node);
758
+
759
+ });
760
+
761
+
762
+ container.find(".mi-row-remove")
763
+
764
+ .off(".miRows")
765
+
766
+ .on("click.miRows", function(evt) {
767
+
768
+ evt.preventDefault();
769
+
770
+
771
+ const rows = container.find(".inverter-row");
772
+
773
+ if (rows.length <= 1) {
774
+
775
+ return;
776
+
777
+ }
778
+
779
+
780
+ $(this).closest(".inverter-row").remove();
781
+
782
+ rebuildRowIndexes();
783
+
784
+ validateRows();
785
+
786
+ updateOutputCount(node);
787
+
788
+ updateCompactRows();
789
+
790
+ });
791
+
792
+ }
793
+
794
+
795
+
796
+ function renderRows(inverters) {
797
+
798
+ const list = Array.isArray(inverters) && inverters.length
799
+
800
+ ? inverters
801
+
802
+ : [{ id: "inv1", minW: 0, maxW: 10000, nominalPowerW: 10000, enabled: true }];
803
+
804
+
805
+ container.empty();
806
+
807
+
808
+ list.forEach(function(inv, index) {
809
+
810
+ container.append($(rowTemplate(index, inv)));
811
+
812
+ });
813
+
814
+
815
+ bindRowEvents();
816
+
817
+ rebuildRowIndexes();
818
+
819
+ validateRows();
820
+
821
+ updateOutputCount(node);
822
+
823
+ updateCompactRows();
824
+
825
+ }
826
+
827
+
828
+
829
+ function startExpertLongPress(evt) {
830
+
831
+ if (evt && evt.preventDefault) {
832
+
833
+ evt.preventDefault();
834
+
835
+ }
836
+
837
+ expertLongPressTriggered = false;
838
+
839
+ clearExpertLongPressTimer();
840
+
841
+
842
+ expertLongPressTimer = setTimeout(function() {
843
+
844
+ expertLongPressTriggered = true;
845
+
846
+ unlockExpertMode();
847
+
848
+ }, EXPERT_LONG_PRESS_MS);
849
+
850
+ }
851
+
852
+
853
+
854
+ function stopExpertLongPress() {
855
+
856
+ clearExpertLongPressTimer();
857
+
858
+ }
859
+
860
+
861
+
862
+ renderRows(node.inverters);
863
+
864
+
865
+ const normalizedDebug = normalizeDebugMode(node.debugMode);
866
+
867
+ $("#node-input-debugMode").val(normalizedDebug);
868
+
869
+
870
+ expertModeUnlockedState = false;
871
+
872
+
873
+ toggleAdvancedRows();
874
+
875
+ $("#node-row-totalPvGlobalVar").toggle($("#node-input-publishTotalPvGlobal").is(":checked"));
876
+
877
+ toggleRampValueVisibility();
878
+
879
+ toggleMissingSiteLimitRows();
880
+
881
+ toggleStaleTelemetryRows();
882
+
883
+ updateOutputCount(node);
884
+
885
+ updateCompactRows();
886
+
887
+
888
+ $("#node-input-siteLimitTopics")
889
+
890
+ .off(".miEditor")
891
+
892
+ .on("blur.miEditor", function() {
893
+
894
+ $(this).val(normalizeTopics($(this).val()));
895
+
896
+ validateAdvancedInputs();
897
+
898
+ });
899
+
900
+
901
+ $("#node-input-debugMode")
902
+
903
+ .off(".miEditor")
904
+
905
+ .on("change.miEditor", function() {
906
+
907
+ updateOutputCount(node);
908
+
909
+ });
910
+
911
+
912
+ $("#node-input-totalPvGlobalVar")
913
+
914
+ .off(".miEditor")
915
+
916
+ .on("blur.miEditor", function() {
917
+
918
+ const key = String($(this).val() || "").trim();
919
+
920
+ $(this).val(key || "total_pv");
921
+
922
+ });
923
+
924
+
925
+ $("#node-input-publishTotalPvGlobal")
926
+
927
+ .off(".miEditor")
928
+
929
+ .on("change.miEditor", function() {
930
+
931
+ $("#node-row-totalPvGlobalVar").toggle($("#node-input-publishTotalPvGlobal").is(":checked"));
932
+
933
+ });
934
+
935
+
936
+ $("#node-input-rampType")
937
+
938
+ .off(".miEditor")
939
+
940
+ .on("change.miEditor", function() {
941
+
942
+ toggleRampValueVisibility();
943
+
944
+ });
945
+
946
+
947
+ $("#node-input-rampFromGlobal")
948
+
949
+ .off(".miEditor")
950
+
951
+ .on("change.miEditor", function() {
952
+
953
+ toggleRampValueVisibility();
954
+
955
+ });
956
+
957
+
958
+ $("#node-input-rampTypeGlobalVar, #node-input-rampUpGlobalVar, #node-input-rampDownGlobalVar")
959
+
960
+ .off(".miEditor")
961
+
962
+ .on("blur.miEditor", function() {
963
+
964
+ let fallback = "scada_ramp_type";
965
+ if (this.id === "node-input-rampUpGlobalVar") fallback = "scada_ramp_up_value";
966
+ if (this.id === "node-input-rampDownGlobalVar") fallback = "scada_ramp_down_value";
967
+
968
+ const key = String($(this).val() || "").trim();
969
+
970
+ $(this).val(key || fallback);
971
+
972
+ });
973
+
974
+
975
+ $("#node-input-missingSiteLimitBehavior")
976
+
977
+ .off(".miEditor")
978
+
979
+ .on("change.miEditor", function() {
980
+
981
+ toggleMissingSiteLimitRows();
982
+
983
+ });
984
+
985
+
986
+ $("#node-input-staleTelemetryBehavior")
987
+
988
+ .off(".miEditor")
989
+
990
+ .on("change.miEditor", function() {
991
+
992
+ toggleStaleTelemetryRows();
993
+
994
+ });
995
+
996
+
997
+ $("#node-mi-add-inverter")
998
+
999
+ .off(".miLongPress")
1000
+
1001
+ .on("pointerdown.miLongPress", function(evt) {
1002
+
1003
+ startExpertLongPress(evt);
1004
+
1005
+ })
1006
+
1007
+ .on("pointerup.miLongPress pointercancel.miLongPress", function() {
1008
+
1009
+ stopExpertLongPress();
1010
+
1011
+ })
1012
+
1013
+ .on("click.miLongPress", function(evt) {
1014
+
1015
+ if (!expertLongPressTriggered) {
1016
+
1017
+ return;
1018
+
1019
+ }
1020
+
1021
+ evt.preventDefault();
1022
+
1023
+ evt.stopImmediatePropagation();
1024
+
1025
+ expertLongPressTriggered = false;
1026
+
1027
+ });
1028
+
1029
+
1030
+ $("#node-mi-add-inverter")
1031
+
1032
+ .off(".miEditor")
1033
+
1034
+ .on("click.miEditor", function(evt) {
1035
+
1036
+ if (expertLongPressTriggered) {
1037
+
1038
+ expertLongPressTriggered = false;
1039
+
1040
+ evt.preventDefault();
1041
+
1042
+ evt.stopImmediatePropagation();
1043
+
1044
+ return;
1045
+
1046
+ }
1047
+
1048
+
1049
+ evt.preventDefault();
1050
+
1051
+
1052
+ const rows = container.find(".inverter-row");
1053
+
1054
+ const index = rows.length;
1055
+
1056
+ const defaultInv = {
1057
+
1058
+ id: "inv" + (index + 1),
1059
+
1060
+ minW: 0,
1061
+
1062
+ maxW: 10000,
1063
+
1064
+ nominalPowerW: 10000,
1065
+
1066
+ enabled: true
1067
+
1068
+ };
1069
+
1070
+
1071
+ container.append($(rowTemplate(index, defaultInv)));
1072
+
1073
+ bindRowEvents();
1074
+
1075
+ rebuildRowIndexes();
1076
+
1077
+ validateRows();
1078
+
1079
+ updateOutputCount(node);
1080
+
1081
+ updateCompactRows();
1082
+
1083
+ });
1084
+
1085
+
1086
+ },
1087
+
1088
+
1089
+
1090
+ oneditsave: function() {
1091
+
1092
+
1093
+ const errors = validateRows();
1094
+
1095
+ if (errors.length > 0) {
1096
+
1097
+ RED.notify("Configuratia multi-inverter are erori. Corecteaza randurile marcate.", "error");
1098
+
1099
+ }
1100
+
1101
+ validateAdvancedInputs();
1102
+
1103
+
1104
+ const inverters = [];
1105
+
1106
+
1107
+ $("#node-input-inverters-container .inverter-row").each(function(index) {
1108
+
1109
+ const row = $(this);
1110
+
1111
+ inverters.push({
1112
+
1113
+ id: String(row.find(".inv-id").val() || "").trim() || ("inv" + (index + 1)),
1114
+
1115
+ minW: Number(row.find(".inv-minw").val() || 0),
1116
+
1117
+ maxW: Number(row.find(".inv-maxw").val() || 0),
1118
+
1119
+ nominalPowerW: Number(row.find(".inv-nominalw").val() || row.find(".inv-maxw").val() || 0),
1120
+
1121
+ enabled: row.find(".inv-enabled").is(":checked")
1122
+
1123
+ });
1124
+
1125
+ });
1126
+
1127
+
1128
+ this.inverters = inverters;
1129
+
1130
+ this.strategy = $("#node-input-strategy").val() || "current_power";
1131
+
1132
+ this.siteLimitTopics = normalizeTopics($("#node-input-siteLimitTopics").val());
1133
+
1134
+ this.rampType = $("#node-input-rampType").val() || "none";
1135
+
1136
+ this.rampValue = Number($("#node-input-rampValue").val() || 0);
1137
+
1138
+ this.rampFromGlobal = $("#node-input-rampFromGlobal").is(":checked");
1139
+
1140
+ this.rampTypeGlobalVar = String($("#node-input-rampTypeGlobalVar").val() || "").trim() || "scada_ramp_type";
1141
+
1142
+ this.rampUpGlobalVar = String($("#node-input-rampUpGlobalVar").val() || "").trim() || "scada_ramp_up_value";
1143
+
1144
+ this.rampDownGlobalVar = String($("#node-input-rampDownGlobalVar").val() || "").trim() || "scada_ramp_down_value";
1145
+
1146
+ this.rampUpValue = Number($("#node-input-rampUpValue").val() || 0);
1147
+
1148
+ this.rampDownValue = Number($("#node-input-rampDownValue").val() || 0);
1149
+
1150
+ this.debugMode = normalizeDebugMode($("#node-input-debugMode").val());
1151
+
1152
+ this.publishTotalPvGlobal = $("#node-input-publishTotalPvGlobal").is(":checked");
1153
+
1154
+ this.totalPvGlobalVar = String($("#node-input-totalPvGlobalVar").val() || "").trim() || "total_pv";
1155
+
1156
+
1157
+ this.telemetryTimeoutMs = Number($("#node-input-telemetryTimeoutMs").val() || 6000);
1158
+
1159
+ this.siteLimitTimeoutMs = Number($("#node-input-siteLimitTimeoutMs").val() || 8000);
1160
+
1161
+ this.missingSiteLimitBehavior = $("#node-input-missingSiteLimitBehavior").val() || "set_zero";
1162
+
1163
+ this.missingSiteLimitValueW = Number($("#node-input-missingSiteLimitValueW").val() || 0);
1164
+
1165
+ this.staleTelemetryBehavior = $("#node-input-staleTelemetryBehavior").val() || "set_inverter_min";
1166
+
1167
+ this.staleTelemetryFixedValueW = Number($("#node-input-staleTelemetryFixedValueW").val() || 0);
1168
+
1169
+ this.waitForFreshTelemetryOnStartup = $("#node-input-waitForFreshTelemetryOnStartup").is(":checked");
1170
+
1171
+ this.recalculateOnTelemetry = $("#node-input-recalculateOnTelemetry").is(":checked");
1172
+
1173
+ this.allocationSmoothingAlpha = Number($("#node-input-allocationSmoothingAlpha").val() || 0.35);
1174
+
1175
+ this.telemetryFilterAlpha = Number($("#node-input-telemetryFilterAlpha").val() || 0.35);
1176
+
1177
+ this.telemetryRecalcDebounceMs = Number($("#node-input-telemetryRecalcDebounceMs").val() || 350);
1178
+
1179
+
1180
+ this.outputs = computeOutputsCount(inverters.length, this.debugMode);
1181
+
1182
+
1183
+ $("#node-input-siteLimitTopics, #node-input-debugMode, #node-input-publishTotalPvGlobal, #node-input-totalPvGlobalVar, #node-input-rampType, #node-input-rampFromGlobal, #node-input-rampTypeGlobalVar, #node-input-rampUpGlobalVar, #node-input-rampDownGlobalVar, #node-input-missingSiteLimitBehavior, #node-input-staleTelemetryBehavior, #node-mi-add-inverter").off(".miEditor");
1184
+
1185
+ $("#node-mi-add-inverter").off(".miLongPress");
1186
+
1187
+ $("#node-input-inverters-container .inv-id, #node-input-inverters-container .inv-enabled, #node-input-inverters-container .inv-minw, #node-input-inverters-container .inv-maxw, #node-input-inverters-container .inv-nominalw, #node-input-inverters-container .mi-row-remove").off(".miRows");
1188
+
1189
+ clearExpertLongPressTimer();
1190
+
1191
+ expertModeUnlockedState = false;
1192
+
1193
+
1194
+ },
1195
+
1196
+
1197
+
1198
+ oneditcancel: function() {
1199
+
1200
+ $("#node-input-siteLimitTopics, #node-input-debugMode, #node-input-publishTotalPvGlobal, #node-input-totalPvGlobalVar, #node-input-rampType, #node-input-rampFromGlobal, #node-input-rampTypeGlobalVar, #node-input-rampUpGlobalVar, #node-input-rampDownGlobalVar, #node-input-missingSiteLimitBehavior, #node-input-staleTelemetryBehavior, #node-mi-add-inverter").off(".miEditor");
1201
+
1202
+ $("#node-mi-add-inverter").off(".miLongPress");
1203
+
1204
+ $("#node-input-inverters-container .inv-id, #node-input-inverters-container .inv-enabled, #node-input-inverters-container .inv-minw, #node-input-inverters-container .inv-maxw, #node-input-inverters-container .inv-nominalw, #node-input-inverters-container .mi-row-remove").off(".miRows");
1205
+
1206
+ clearExpertLongPressTimer();
1207
+
1208
+ expertModeUnlockedState = false;
1209
+
1210
+ }
1211
+
1212
+
1213
+ });
1214
+
1215
+
1216
+ })();
1217
+
1218
+
1219
+ </script>
1220
+
1221
+
1222
+
1223
+ <script type="text/html" data-template-name="multi-inverter">
1224
+ <div class="form-row"><label for="node-input-license"><i class="fa fa-key"></i> License</label><input type="text" id="node-input-license"></div>
1225
+
1226
+ <div class="multi-inverter-editor">
1227
+
1228
+
1229
+ <div class="pr-section">
1230
+
1231
+ <div class="pr-section-title"><i class="fa fa-sliders"></i> General</div>
1232
+
1233
+ <p class="pr-section-note">Distribuie limita de site catre invertoare pe baza telemetriei curente.</p>
1234
+
1235
+
1236
+ <div class="pr-grid pr-grid-2">
1237
+
1238
+ <div class="form-row">
1239
+
1240
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
1241
+
1242
+ <input type="text" id="node-input-name" placeholder="multi-inverter">
1243
+
1244
+ </div>
1245
+
1246
+
1247
+ <div class="form-row">
1248
+
1249
+ <label for="node-input-strategy"><i class="fa fa-sitemap"></i> Allocation Strategy</label>
1250
+
1251
+ <select id="node-input-strategy">
1252
+
1253
+ <option value="current_power">Current power</option>
1254
+
1255
+ <option value="max_power">Max power</option>
1256
+
1257
+ <option value="equal">Equal share</option>
1258
+
1259
+ <option value="headroom">Headroom</option>
1260
+
1261
+ </select>
1262
+
1263
+ </div>
1264
+
1265
+
1266
+ <div class="form-row">
1267
+
1268
+ <label></label>
1269
+
1270
+ <div class="fronius-meta-info">Iesiri: <span id="node-multi-inverter-outputs-info">1</span></div>
1271
+
1272
+ </div>
1273
+
1274
+ </div>
1275
+
1276
+ </div>
1277
+
1278
+
1279
+ <div class="pr-section">
1280
+
1281
+ <div class="pr-section-title"><i class="fa fa-tachometer"></i> Ramp</div>
1282
+
1283
+ <p class="pr-section-note">Setari implicite de rampa propagate spre nodurile de write.</p>
1284
+
1285
+
1286
+ <div class="pr-grid pr-grid-2">
1287
+
1288
+ <div class="form-row pr-checkbox-row pr-span-2 mi-ramp-global-toggle">
1289
+
1290
+ <label for="node-input-rampFromGlobal"><i class="fa fa-globe"></i> Read Ramp From Global</label>
1291
+
1292
+ <input type="checkbox" id="node-input-rampFromGlobal">
1293
+
1294
+ </div>
1295
+
1296
+
1297
+ <div class="form-row" id="node-row-rampType">
1298
+
1299
+ <label for="node-input-rampType"><i class="fa fa-sliders"></i> Ramp Type</label>
1300
+
1301
+ <select id="node-input-rampType">
1302
+
1303
+ <option value="none">No ramp</option>
1304
+
1305
+ <option value="time">Ramp Time (s)</option>
1306
+
1307
+ <option value="gradient_pct">Ramp Gradient (%/s)</option>
1308
+
1309
+ <option value="gradient_w">Ramp Gradient (W/s & VAR/s)</option>
1310
+
1311
+ </select>
1312
+
1313
+ </div>
1314
+
1315
+
1316
+ <div class="form-row" id="node-row-rampGlobalType">
1317
+
1318
+ <label for="node-input-rampTypeGlobalVar"><i class="fa fa-key"></i> Ramp Type Variable</label>
1319
+
1320
+ <input type="text" id="node-input-rampTypeGlobalVar" placeholder="scada_ramp_type">
1321
+
1322
+ </div>
1323
+
1324
+
1325
+ <div class="form-row" id="node-row-rampGlobalUpValue">
1326
+
1327
+ <label for="node-input-rampUpGlobalVar"><i class="fa fa-key"></i> Ramp Up Variable</label>
1328
+
1329
+ <input type="text" id="node-input-rampUpGlobalVar" placeholder="scada_ramp_up_value">
1330
+
1331
+ </div>
1332
+
1333
+
1334
+ <div class="form-row" id="node-row-rampGlobalDownValue">
1335
+
1336
+ <label for="node-input-rampDownGlobalVar"><i class="fa fa-key"></i> Ramp Down Variable</label>
1337
+
1338
+ <input type="text" id="node-input-rampDownGlobalVar" placeholder="scada_ramp_down_value">
1339
+
1340
+ </div>
1341
+
1342
+
1343
+ <input type="hidden" id="node-input-rampValue">
1344
+
1345
+ <div class="form-row" id="node-row-rampUpValue">
1346
+
1347
+ <label for="node-input-rampUpValue"><i class="fa fa-level-up"></i> <span id="node-label-rampUpValue">Ramp Up Value</span></label>
1348
+
1349
+ <input type="number" id="node-input-rampUpValue" min="0" placeholder="0 = no ramp on up">
1350
+
1351
+ </div>
1352
+
1353
+
1354
+ <div class="form-row" id="node-row-rampDownValue">
1355
+
1356
+ <label for="node-input-rampDownValue"><i class="fa fa-level-down"></i> <span id="node-label-rampDownValue">Ramp Down Value</span></label>
1357
+
1358
+ <input type="number" id="node-input-rampDownValue" min="0" placeholder="0 = no ramp on down">
1359
+
1360
+ </div>
1361
+
1362
+ </div>
1363
+
1364
+ </div>
1365
+
1366
+
1367
+ <div class="pr-section">
1368
+
1369
+ <div class="pr-section-title"><i class="fa fa-plug"></i> Inverters</div>
1370
+
1371
+ <p class="pr-section-note">Configureaza invertoarele active. Foloseste Add/Remove pentru randuri.</p>
1372
+
1373
+
1374
+ <div class="pr-toolbar">
1375
+
1376
+ <button type="button" class="button" id="node-mi-add-inverter"><i class="fa fa-plus"></i> Add inverter</button>
1377
+
1378
+ </div>
1379
+
1380
+
1381
+ <div class="mi-inverters-container">
1382
+
1383
+ <div class="mi-row mi-row-header">
1384
+
1385
+ <div class="mi-col-index">#</div>
1386
+
1387
+ <div class="mi-col-id">ID</div>
1388
+
1389
+ <div class="mi-col-min">Min W&VAR</div>
1390
+
1391
+ <div class="mi-col-max">Max W&VAR</div>
1392
+
1393
+ <div class="mi-col-nominal">Nominal W</div>
1394
+
1395
+ <div class="mi-col-enabled">Enabled</div>
1396
+
1397
+ <div class="mi-col-actions">Actions</div>
1398
+
1399
+ </div>
1400
+
1401
+ <div id="node-input-inverters-container"></div>
1402
+
1403
+ </div>
1404
+
1405
+
1406
+ <div class="form-row" id="node-multi-inverter-validation" style="display:none;"></div>
1407
+
1408
+ </div>
1409
+
1410
+
1411
+ <div class="pr-section" id="mi-advanced-section" style="display:none;">
1412
+
1413
+ <div class="pr-section-title"><i class="fa fa-flask"></i> Additional Parameters</div>
1414
+
1415
+ <p class="pr-section-note">Timeout-uri, fail-safe si smoothing pentru scenarii speciale.</p>
1416
+
1417
+
1418
+ <div class="pr-grid pr-grid-2">
1419
+
1420
+ <div class="form-row pr-span-2">
1421
+
1422
+ <label for="node-input-siteLimitTopics"><i class="fa fa-sign-in"></i> Site Limit Topics</label>
1423
+
1424
+ <input type="text" id="node-input-siteLimitTopics" placeholder="site-limit,control-export,control-reactive">
1425
+
1426
+ </div>
1427
+
1428
+
1429
+ <div class="form-row pr-checkbox-row pr-span-2">
1430
+
1431
+ <label for="node-input-publishTotalPvGlobal"><i class="fa fa-line-chart"></i> Publish Total PV Global</label>
1432
+
1433
+ <input type="checkbox" id="node-input-publishTotalPvGlobal">
1434
+
1435
+ </div>
1436
+
1437
+
1438
+ <div class="form-row pr-span-2" id="node-row-totalPvGlobalVar">
1439
+
1440
+ <label for="node-input-totalPvGlobalVar"><i class="fa fa-globe"></i> Total PV Global Variable</label>
1441
+
1442
+ <input type="text" id="node-input-totalPvGlobalVar" placeholder="total_pv">
1443
+
1444
+ </div>
1445
+
1446
+ <div class="form-row pr-checkbox-row pr-span-2">
1447
+ <label for="node-input-influxDb"><i class="fa fa-database"></i> InfluxDB</label>
1448
+ <input type="checkbox" id="node-input-influxDb">
1449
+ <span class="pr-inline-note">Trimite intern alocarile si totalurile catre InfluxDB fara a modifica iesirea principala.</span>
1450
+ </div>
1451
+ <div class="form-row" id="node-row-influxMeasurement">
1452
+ <label for="node-input-influxMeasurement"><i class="fa fa-line-chart"></i> Measurement</label>
1453
+ <input type="text" id="node-input-influxMeasurement" placeholder="multi_inverter">
1454
+ </div>
1455
+ <div class="form-row" id="node-row-influxNode">
1456
+ <label for="node-input-influxNode"><i class="fa fa-plug"></i> Nod InfluxDB</label>
1457
+ <select id="node-input-influxNode"></select>
1458
+ </div>
1459
+
1460
+
1461
+ <div class="form-row">
1462
+
1463
+ <label for="node-input-debugMode"><i class="fa fa-bug"></i> Debug</label>
1464
+
1465
+ <select id="node-input-debugMode">
1466
+
1467
+ <option value="off">Off</option>
1468
+
1469
+ <option value="normal">Normal</option>
1470
+
1471
+ <option value="verbose">Verbose</option>
1472
+
1473
+ </select>
1474
+
1475
+ </div>
1476
+
1477
+
1478
+ <div class="form-row">
1479
+
1480
+ <label for="node-input-telemetryTimeoutMs"><i class="fa fa-clock-o"></i> Telemetry Timeout (ms)</label>
1481
+
1482
+ <input type="number" id="node-input-telemetryTimeoutMs" min="0" placeholder="6000">
1483
+
1484
+ </div>
1485
+
1486
+ <div class="form-row">
1487
+ <label for="node-input-maxFutureTimestampMs"><i class="fa fa-clock-o"></i> Max Future Timestamp (ms)</label>
1488
+ <input type="number" id="node-input-maxFutureTimestampMs" min="0" placeholder="2000">
1489
+ </div>
1490
+
1491
+
1492
+ <div class="form-row">
1493
+
1494
+ <label for="node-input-siteLimitTimeoutMs"><i class="fa fa-hourglass-half"></i> Site Limit Timeout (ms)</label>
1495
+
1496
+ <input type="number" id="node-input-siteLimitTimeoutMs" min="0" placeholder="8000">
1497
+
1498
+ </div>
1499
+
1500
+
1501
+ <div class="form-row">
1502
+
1503
+ <label for="node-input-missingSiteLimitBehavior"><i class="fa fa-shield"></i> Missing Site Limit</label>
1504
+
1505
+ <select id="node-input-missingSiteLimitBehavior">
1506
+
1507
+ <option value="set_zero">Set zero</option>
1508
+
1509
+ <option value="set_inverter_min">Set inverter min</option>
1510
+
1511
+ <option value="fixed_value">Fixed value</option>
1512
+
1513
+ </select>
1514
+
1515
+ </div>
1516
+
1517
+
1518
+ <div class="form-row" id="node-row-missingSiteLimitValue">
1519
+
1520
+ <label for="node-input-missingSiteLimitValueW"><i class="fa fa-bolt"></i> Missing Limit Value (W&VAR)</label>
1521
+
1522
+ <input type="number" id="node-input-missingSiteLimitValueW" min="0" placeholder="0">
1523
+
1524
+ </div>
1525
+
1526
+
1527
+ <div class="form-row">
1528
+
1529
+ <label for="node-input-staleTelemetryBehavior"><i class="fa fa-unlink"></i> Missing Inverter Power</label>
1530
+
1531
+ <select id="node-input-staleTelemetryBehavior">
1532
+
1533
+ <option value="set_zero">Set zero</option>
1534
+
1535
+ <option value="set_inverter_min">Set inverter min</option>
1536
+
1537
+ <option value="fixed_value">Fixed value</option>
1538
+
1539
+ </select>
1540
+
1541
+ </div>
1542
+
1543
+
1544
+ <div class="form-row" id="node-row-staleTelemetryFixedValue">
1545
+
1546
+ <label for="node-input-staleTelemetryFixedValueW"><i class="fa fa-bolt"></i> Missing Power Value (W&VAR)</label>
1547
+
1548
+ <input type="number" id="node-input-staleTelemetryFixedValueW" min="0" placeholder="0">
1549
+
1550
+ </div>
1551
+
1552
+
1553
+ <div class="form-row">
1554
+
1555
+ <label for="node-input-telemetryFilterAlpha"><i class="fa fa-signal"></i> Telemetry Filter Alpha</label>
1556
+
1557
+ <input type="text" id="node-input-telemetryFilterAlpha" placeholder="0.35">
1558
+
1559
+ </div>
1560
+
1561
+ <div class="form-row">
1562
+
1563
+ <label for="node-input-allocationSmoothingAlpha"><i class="fa fa-filter"></i> Allocation Smoothing</label>
1564
+
1565
+ <input type="text" id="node-input-allocationSmoothingAlpha" placeholder="0.35">
1566
+
1567
+ </div>
1568
+
1569
+ <div class="form-row">
1570
+
1571
+ <label for="node-input-allocationDeadbandW"><i class="fa fa-arrows-h"></i> Allocation Deadband (W&VAR)</label>
1572
+
1573
+ <input type="number" id="node-input-allocationDeadbandW" min="0" placeholder="300">
1574
+
1575
+ </div>
1576
+
1577
+ <div class="form-row">
1578
+
1579
+ <label for="node-input-telemetryRecalcDebounceMs"><i class="fa fa-tachometer"></i> Telemetry Debounce (ms)</label>
1580
+
1581
+ <input type="number" id="node-input-telemetryRecalcDebounceMs" min="0" placeholder="350">
1582
+
1583
+ </div>
1584
+
1585
+ </div>
1586
+
1587
+
1588
+ <div class="form-row pr-checkbox-row">
1589
+
1590
+ <label for="node-input-waitForFreshTelemetryOnStartup"><i class="fa fa-hourglass-start"></i> Startup Wait</label>
1591
+
1592
+ <input type="checkbox" id="node-input-waitForFreshTelemetryOnStartup">
1593
+
1594
+ <span class="pr-inline-note">Asteapta cel putin un invertor cu telemetrie fresh inainte de prima comanda operationala.</span>
1595
+
1596
+ </div>
1597
+
1598
+
1599
+ <div class="form-row pr-checkbox-row">
1600
+
1601
+ <label for="node-input-recalculateOnTelemetry"><i class="fa fa-refresh"></i> Recalc On Telemetry</label>
1602
+
1603
+ <input type="checkbox" id="node-input-recalculateOnTelemetry">
1604
+
1605
+ <span class="pr-inline-note">Recalculeaza alocarea la update de telemetrie.</span>
1606
+
1607
+ </div>
1608
+
1609
+ </div>
1610
+
1611
+
1612
+ </div>
1613
+
1614
+ </script>
1615
+
1616
+
1617
+
1618
+ <script type="text/html" data-help-name="multi-inverter">
1619
+
1620
+ <p>
1621
+
1622
+ Nodul <b>multi-inverter</b> imparte o limita totala de site catre mai multe invertoare.
1623
+
1624
+ Limita poate veni de la <code>control-export</code>, <code>control-reactive</code> sau de la orice nod
1625
+
1626
+ care trimite un payload cu <code>site_limit_w</code> sau <code>site_limit_var</code>.
1627
+
1628
+ </p>
1629
+
1630
+
1631
+ <pre>control-export -> multi-inverter -> fronius-write inv1 / inv2 / inv3</pre>
1632
+
1633
+
1634
+ <p>
1635
+
1636
+ Nodul primeste doua tipuri de mesaje: limita de site si telemetrie de invertor. Limita de site spune
1637
+
1638
+ cat are voie sa produca tot sistemul, iar telemetria spune cat produce fiecare invertor in momentul curent.
1639
+
1640
+ Pe baza acestor informatii, nodul calculeaza o limita separata pentru fiecare invertor.
1641
+
1642
+ </p>
1643
+
1644
+
1645
+ <h3>Ce face nodul</h3>
1646
+
1647
+ <ul>
1648
+
1649
+ <li>primeste limita totala activa sau reactiva pentru site</li>
1650
+
1651
+ <li>primeste telemetrie de putere pentru fiecare invertor configurat</li>
1652
+
1653
+ <li>verifica ID-urile de invertor si intra in failsafe daca exista conflicte</li>
1654
+
1655
+ <li>imparte limita totala folosind strategia aleasa</li>
1656
+
1657
+ <li>respecta <code>Min W&amp;VAR</code> si <code>Max W&amp;VAR</code> pentru fiecare invertor</li>
1658
+
1659
+ <li>trimite rampa configurata catre nodurile de write</li>
1660
+
1661
+ <li>poate publica productia PV totala intr-o variabila globala</li>
1662
+
1663
+ <li>poate emite diagnostic pe o iesire suplimentara cand debug este activ</li>
1664
+
1665
+ </ul>
1666
+
1667
+
1668
+ <h3>Input: limita de site</h3>
1669
+
1670
+ <p>
1671
+
1672
+ Un mesaj este tratat ca limita de site doar daca topicul este in lista <b>Site Limit Topics</b>
1673
+
1674
+ sau este unul dintre topicurile implicite: <code>site-limit</code>, <code>control-export</code>,
1675
+
1676
+ <code>control-reactive</code>.
1677
+
1678
+ </p>
1679
+
1680
+
1681
+ <p>
1682
+
1683
+ Pentru control activ se foloseste <code>payload.site_limit_w</code>:
1684
+
1685
+ </p>
1686
+
1687
+
1688
+ <pre>{
1689
+
1690
+ "topic": "site-limit",
1691
+
1692
+ "payload": {
1693
+
1694
+ "site_limit_w": 9000
1695
+
1696
+ }
1697
+
1698
+ }</pre>
1699
+
1700
+
1701
+ <p>
1702
+
1703
+ Pentru control reactiv se foloseste <code>payload.site_limit_var</code> sau <code>payload.limit_var</code>:
1704
+
1705
+ </p>
1706
+
1707
+
1708
+ <pre>{
1709
+
1710
+ "topic": "control-reactive",
1711
+
1712
+ "payload": {
1713
+
1714
+ "site_limit_var": 3000
1715
+
1716
+ }
1717
+
1718
+ }</pre>
1719
+
1720
+
1721
+ <p>
1722
+
1723
+ Cand intra o limita activa, iesirea foloseste <code>limit_w</code>. Cand intra o limita reactiva,
1724
+
1725
+ iesirea foloseste <code>limit_var</code>. Semnul valorii reactive se pastreaza la iesire.
1726
+
1727
+ </p>
1728
+
1729
+
1730
+ <h3>Input: telemetrie invertor</h3>
1731
+
1732
+ <p>
1733
+
1734
+ Telemetria de invertor trebuie sa contina un ID valid si o valoare numerica de putere.
1735
+
1736
+ ID-ul poate veni din <code>payload.inverter_id</code>, <code>payload.id</code>, <code>msg.inverter_id</code>,
1737
+
1738
+ <code>msg.id</code> sau <code>msg.topic</code>.
1739
+
1740
+ </p>
1741
+
1742
+
1743
+ <pre>{
1744
+
1745
+ "topic": "inv1",
1746
+
1747
+ "payload": {
1748
+
1749
+ "inverter_id": "inv1",
1750
+
1751
+ "power_w": 3200
1752
+
1753
+ }
1754
+
1755
+ }</pre>
1756
+
1757
+
1758
+ <p>
1759
+
1760
+ Pentru putere activa, nodul cauta:
1761
+
1762
+ <code>power_w</code>, <code>active_power_w</code>, <code>p_w</code>,
1763
+
1764
+ <code>ac_active_power_w</code> sau <code>summary.ac.power_w</code>.
1765
+
1766
+ </p>
1767
+
1768
+
1769
+ <p>
1770
+
1771
+ Pentru putere reactiva, nodul cauta:
1772
+
1773
+ <code>power_var</code>, <code>reactive_power_var</code>, <code>ac_reactive_power_var</code>,
1774
+
1775
+ <code>q_var</code>, <code>var</code>, <code>vars</code>, <code>limit_var</code>,
1776
+
1777
+ <code>site_limit_var</code> sau <code>summary.ac.reactive_power_var</code>.
1778
+
1779
+ </p>
1780
+
1781
+
1782
+ <h3>Validare ID</h3>
1783
+
1784
+ <p>
1785
+
1786
+ ID-ul din telemetrie trebuie sa corespunda exact cu ID-ul configurat in lista de invertoare.
1787
+
1788
+ Daca sunt trimise mai multe surse de ID in acelasi mesaj, ele trebuie sa fie identice.
1789
+
1790
+ </p>
1791
+
1792
+
1793
+ <ul>
1794
+
1795
+ <li>daca lipseste ID-ul, nodul emite failsafe</li>
1796
+
1797
+ <li>daca <code>topic</code> difera de <code>inverter_id</code>, nodul emite failsafe</li>
1798
+
1799
+ <li>daca ID-ul nu exista in lista configurata, nodul emite failsafe</li>
1800
+
1801
+ <li>daca acelasi ID apare din doua surse diferite cu valori diferite, nodul emite failsafe</li>
1802
+
1803
+ </ul>
1804
+
1805
+
1806
+ <p>
1807
+
1808
+ Failsafe inseamna ca nodul trimite limite zero pentru invertoarele valide, ca sa evite comenzi ambigue.
1809
+
1810
+ </p>
1811
+
1812
+
1813
+ <h3>Configurare invertoare</h3>
1814
+
1815
+ <p>
1816
+
1817
+ Fiecare rand din lista de invertoare are:
1818
+
1819
+ </p>
1820
+
1821
+
1822
+ <ul>
1823
+
1824
+ <li><b>ID</b> - identificatorul folosit in telemetrie si in iesirea catre nodul de write.</li>
1825
+
1826
+ <li><b>Min W&amp;VAR</b> - limita minima permisa pentru acel invertor.</li>
1827
+
1828
+ <li><b>Max W&amp;VAR</b> - limita maxima permisa pentru acel invertor.</li>
1829
+
1830
+ <li><b>Enabled</b> - daca este debifat, invertorul ramane in lista, dar primeste limita 0.</li>
1831
+
1832
+ </ul>
1833
+
1834
+
1835
+ <p>
1836
+
1837
+ ID-urile active trebuie sa fie unice. Un rand activ fara ID, cu <code>Max</code> invalid sau cu
1838
+
1839
+ <code>Min</code> mai mare decat <code>Max</code> face configuratia invalida.
1840
+
1841
+ </p>
1842
+
1843
+
1844
+ <h3>Allocation Strategy</h3>
1845
+
1846
+ <p>
1847
+
1848
+ Strategia decide cum este impartita limita totala intre invertoarele active.
1849
+
1850
+ </p>
1851
+
1852
+
1853
+ <ul>
1854
+
1855
+ <li><b>Current power</b> imparte proportional cu puterea curenta masurata. Daca lipseste telemetria fresh, foloseste automat <b>Max power</b>.</li>
1856
+
1857
+ <li><b>Max power</b> imparte proportional cu puterea maxima configurata pentru fiecare invertor.</li>
1858
+
1859
+ <li><b>Equal share</b> imparte limita in mod egal intre invertoarele active.</li>
1860
+
1861
+ <li><b>Headroom</b> imparte proportional cu spatiul ramas pana la <code>Max</code>, adica <code>Max - putere curenta</code>.</li>
1862
+
1863
+ </ul>
1864
+
1865
+
1866
+ <p>
1867
+
1868
+ Dupa impartire, fiecare limita este taiata intre minimul si maximul invertorului. Daca limita de site
1869
+
1870
+ este mai mare decat capacitatea totala disponibila, campul <code>clipped</code> devine <code>true</code>.
1871
+
1872
+ </p>
1873
+
1874
+
1875
+ <h3>Ramp</h3>
1876
+
1877
+ <p>
1878
+
1879
+ Setarea de rampa este propagata in payload-ul fiecarei limite. Nodul nu aplica local rampa;
1880
+
1881
+ doar transmite instructiunea catre nodurile de write.
1882
+
1883
+ </p>
1884
+
1885
+
1886
+ <ul>
1887
+
1888
+ <li><b>No ramp</b> nu trimite campul <code>ramp</code>.</li>
1889
+
1890
+ <li><b>Ramp Time (s)</b> trimite durata de aplicare a comenzii.</li>
1891
+
1892
+ <li><b>Ramp Gradient (%/s)</b> trimite gradient procentual.</li>
1893
+
1894
+ <li><b>Ramp Gradient (W/s &amp; VAR/s)</b> trimite gradient absolut pentru putere activa sau reactiva.</li>
1895
+
1896
+ </ul>
1897
+
1898
+ <h3>Comenzi SCADA globale</h3>
1899
+
1900
+ <p>Cu <b>Ramp From Global</b> activ, nodul citește <code>scada_ramp_type</code>, <code>scada_ramp_up_value</code> și <code>scada_ramp_down_value</code>, publicate de <code>scada-server</code>. Valorile sunt direcționale și trebuie să fie pozitive: Up pentru creșterea limitei, Down pentru scăderea ei.</p>
1901
+
1902
+ <ul>
1903
+ <li><code>scada_ramp_type = 0</code>: fără rampă; <code>1</code>: timp fix în secunde; <code>2</code>: gradient în %/s; <code>3</code>: gradient în W/s pentru P sau VAr/s pentru Q.</li>
1904
+ <li>Pentru tipul <code>2</code>, fiecare valoare de rampă trebuie să fie în domeniul <code>0…100 %/s</code>.</li>
1905
+ <li>O comandă Q procentuală, dacă este folosită de un nod de control din aval, are domeniul <code>-100…+100 %</code>; un setpoint Q în VAr rămâne semnat și este limitat de puterea reactivă nominală a echipamentului.</li>
1906
+ </ul>
1907
+
1908
+
1909
+ <h3>Telemetrie si timeout</h3>
1910
+
1911
+ <p>
1912
+
1913
+ <b>Telemetry Timeout (ms)</b> decide cat timp este considerata valida telemetria unui invertor.
1914
+
1915
+ Daca valoarea este <code>0</code>, telemetria este considerata mereu fresh.
1916
+
1917
+ </p>
1918
+
1919
+
1920
+ <p>
1921
+
1922
+ Pentru timestamp, nodul cauta <code>msg.timestamp</code>, <code>msg.ts</code>, <code>msg.time</code>
1923
+
1924
+ sau variantele echivalente din <code>msg.payload</code>. Daca lipseste timestamp-ul, se foloseste timpul curent.
1925
+
1926
+ </p>
1927
+
1928
+
1929
+ <h3>Site Limit Timeout</h3>
1930
+
1931
+ <p>
1932
+
1933
+ <b>Site Limit Timeout (ms)</b> decide cat timp este valida ultima limita de site primita.
1934
+
1935
+ Daca limita lipseste sau expira, nodul foloseste politica <b>Missing Site Limit</b>.
1936
+
1937
+ </p>
1938
+
1939
+
1940
+ <ul>
1941
+
1942
+ <li><b>Set zero</b> trimite 0 catre fiecare invertor activ.</li>
1943
+
1944
+ <li><b>Set inverter min</b> trimite minimul configurat pentru fiecare invertor activ.</li>
1945
+
1946
+ <li><b>Fixed value</b> trimite valoarea configurata la <b>Missing Limit Value</b>, taiata la maximul fiecarui invertor.</li>
1947
+
1948
+ </ul>
1949
+
1950
+
1951
+ <h3>Missing Inverter Power</h3>
1952
+
1953
+ <p>
1954
+
1955
+ Daca un invertor nu are telemetrie fresh, nodul aplica politica <b>Missing Inverter Power</b>
1956
+
1957
+ pentru acel invertor.
1958
+
1959
+ </p>
1960
+
1961
+
1962
+ <ul>
1963
+
1964
+ <li><b>Set zero</b> trimite 0 catre invertorul fara telemetrie.</li>
1965
+
1966
+ <li><b>Set inverter min</b> trimite minimul configurat pentru acel invertor.</li>
1967
+
1968
+ <li><b>Fixed value</b> trimite valoarea configurata la <b>Missing Power Value</b>, taiata la maximul invertorului.</li>
1969
+
1970
+ </ul>
1971
+
1972
+
1973
+ <h3>Startup Wait</h3>
1974
+
1975
+ <p>
1976
+
1977
+ Cand <b>Startup Wait</b> este activ, nodul asteapta pana primeste telemetrie fresh de la cel putin un invertor activ
1978
+
1979
+ inainte de prima alocare operationala. Daca alte invertoare sunt stale, ele sunt tratate prin politica <b>Missing Inverter Power</b>.
1980
+
1981
+ </p>
1982
+
1983
+
1984
+ <h3>Recalc On Telemetry</h3>
1985
+
1986
+ <p>
1987
+
1988
+ Cand <b>Recalc On Telemetry</b> este activ, nodul recalculeaza alocarea si la update-uri de telemetrie,
1989
+
1990
+ nu doar cand primeste o limita noua de site. <b>Telemetry Debounce</b> grupeaza update-urile foarte dese.
1991
+
1992
+ </p>
1993
+
1994
+
1995
+ <h3>Allocation Smoothing</h3>
1996
+
1997
+ <p>
1998
+
1999
+ <b>Allocation Smoothing</b> reduce salturile intre alocari consecutive. Valoarea este intre
2000
+
2001
+ <code>0.01</code> si <code>1</code>. Valoarea <code>1</code> inseamna fara smoothing, iar valori mai mici
2002
+
2003
+ fac tranzitia mai lenta.
2004
+
2005
+ </p>
2006
+
2007
+
2008
+ <h3>Total PV Global</h3>
2009
+
2010
+ <p>
2011
+
2012
+ Cand <b>Publish Total PV Global</b> este activ, nodul aduna puterea activa fresh a invertoarelor valide
2013
+
2014
+ si scrie totalul in variabila globala configurata, implicit <code>total_pv</code>.
2015
+
2016
+ </p>
2017
+
2018
+
2019
+ <p>
2020
+
2021
+ Aceasta valoare poate fi folosita de <code>control-export</code> pentru <b>Dynamic PV Cap</b>.
2022
+
2023
+ </p>
2024
+
2025
+
2026
+ <h3>Iesiri</h3>
2027
+
2028
+ <p>
2029
+
2030
+ Prima iesire trimite mesajul agregat pentru toate invertoarele. Daca debug este activ, apare o iesire
2031
+
2032
+ suplimentara cu diagnostic. Numarul de iesiri afisat in editor se actualizeaza in functie de debug.
2033
+
2034
+ </p>
2035
+
2036
+
2037
+ <h3>Exemplu Output</h3>
2038
+
2039
+ <pre>{
2040
+
2041
+ "topic": "multi-inverter/limits",
2042
+
2043
+ "payload": {
2044
+
2045
+ "limits": [
2046
+
2047
+ {
2048
+
2049
+ "id": "inv1",
2050
+
2051
+ "enabled": true,
2052
+
2053
+ "min_w": 0,
2054
+
2055
+ "max_w": 5000,
2056
+
2057
+ "current_power_w": 3200,
2058
+
2059
+ "current_power_fresh": true,
2060
+
2061
+ "limit_w": 2500,
2062
+
2063
+ "ramp": { "type": "gradient_w", "value": 500 }
2064
+
2065
+ },
2066
+
2067
+ {
2068
+
2069
+ "id": "inv2",
2070
+
2071
+ "enabled": true,
2072
+
2073
+ "min_w": 0,
2074
+
2075
+ "max_w": 5000,
2076
+
2077
+ "current_power_w": 1800,
2078
+
2079
+ "current_power_fresh": true,
2080
+
2081
+ "limit_w": 2500,
2082
+
2083
+ "ramp": { "type": "gradient_w", "value": 500 }
2084
+
2085
+ }
2086
+
2087
+ ],
2088
+
2089
+ "site_limit_w": 5000,
2090
+
2091
+ "site_limit_var": null,
2092
+
2093
+ "control_mode": "active",
2094
+
2095
+ "allocation_strategy": "current_power",
2096
+
2097
+ "effective_strategy": "current_power",
2098
+
2099
+ "stale_inputs": [],
2100
+
2101
+ "fresh_power_map": {
2102
+
2103
+ "inv1": 3200,
2104
+
2105
+ "inv2": 1800
2106
+
2107
+ },
2108
+
2109
+ "total_fresh_power_w": 5000,
2110
+
2111
+ "available_capacity_w": 10000,
2112
+
2113
+ "unallocated_w": 0,
2114
+
2115
+ "clipped": false,
2116
+
2117
+ "active_inverters": ["inv1", "inv2"],
2118
+
2119
+ "ramp": { "type": "gradient_w", "value": 500 },
2120
+
2121
+ "timestamp_ms": 1776692400000
2122
+
2123
+ }
2124
+
2125
+ }</pre>
2126
+
2127
+
2128
+ <h3>Output catre fronius-write</h3>
2129
+
2130
+ <p>
2131
+
2132
+ Nodul <code>fronius-write</code> in modul Multi-inverter cauta limita pentru propriul
2133
+
2134
+ <b>Inverter ID</b> in <code>payload.limits</code>. ID-ul configurat in <code>fronius-write</code>
2135
+
2136
+ trebuie sa fie identic cu ID-ul din lista <code>multi-inverter</code>.
2137
+
2138
+ </p>
2139
+
2140
+
2141
+ <h3>Debug</h3>
2142
+
2143
+ <p>
2144
+
2145
+ Debug <b>Normal</b> trimite diagnostic la schimbari de stare sau cand motivul nu este alocare normala.
2146
+
2147
+ Debug <b>Verbose</b> trimite diagnostic la fiecare calcul.
2148
+
2149
+ </p>
2150
+
2151
+
2152
+ <p>
2153
+
2154
+ Mesajul de diagnostic are topic <code>multi-inverter/diagnostic</code> si include starea configuratiei,
2155
+
2156
+ strategia efectiva, invertoarele stale, alocarile calculate, capacitatea disponibila si motivul deciziei.
2157
+
2158
+ </p>
2159
+
2160
+
2161
+ <h3>Status in editor</h3>
2162
+
2163
+ <ul>
2164
+
2165
+ <li><b>verde</b> inseamna alocare normala.</li>
2166
+
2167
+ <li><b>galben</b> inseamna telemetrie lipsa, startup wait sau site limit lipsa.</li>
2168
+
2169
+ <li><b>rosu</b> inseamna configuratie invalida, ID conflict sau failsafe.</li>
2170
+
2171
+ </ul>
2172
+
2173
+
2174
+ <h3>Recomandari practice</h3>
2175
+
2176
+ <ul>
2177
+
2178
+ <li>Foloseste aceleasi ID-uri in <code>fronius-read</code>, <code>multi-inverter</code> si <code>fronius-write</code>.</li>
2179
+
2180
+ <li>Pentru sisteme similare, <b>Max power</b> sau <b>Equal share</b> sunt cele mai usor de verificat.</li>
2181
+
2182
+ <li>Pentru sisteme cu invertoare diferite, <b>Max power</b> imparte mai corect dupa capacitate.</li>
2183
+
2184
+ <li>Pentru limitare dinamica dupa productie reala, foloseste <b>Current power</b> si telemetrie fresh.</li>
2185
+
2186
+ <li>In modul <b>Current power</b>, ponderile folosesc puterea filtrata prin <b>Telemetry Filter Alpha</b>; campul <code>current_power_w</code> ramane masurarea bruta, iar <code>allocation_power_w</code> arata valoarea folosita la impartire.</li>
2187
+
2188
+ <li>Comenzile noi de site-limit sunt aplicate imediat; Allocation Smoothing este folosit doar la redistribuirea produsa de telemetrie.</li>
2189
+
2190
+ <li>Pentru control-export/reactive closed-loop rapid, foloseste No ramp sau o rampa suficient de rapida. O rampa lenta introduce intarziere si poate produce supracorectie.</li>
2191
+
2192
+ <li>Pastreaza <b>Startup Wait</b> activ daca vrei sa eviti o comanda initiala fara niciun invertor cu telemetrie fresh.</li>
2193
+
2194
+ <li>Activeaza debug temporar cand verifici alocarea sau ID-urile, apoi revino la <b>Off</b>.</li>
2195
+
2196
+ </ul>
2197
+
2198
+ <h3>Referinta completa parametri</h3>
2199
+ <ul>
2200
+ <li><code>strategy</code>: strategia de alocare intre invertoare.</li>
2201
+ <li><code>siteLimitTopics</code>: topic-urile tratate ca limita de site.</li>
2202
+ <li><code>telemetryTimeoutMs</code>: vechimea maxima a telemetriei de invertor.</li>
2203
+ <li><code>maxFutureTimestampMs</code>: toleranta pentru timestamp-uri din viitor.</li>
2204
+ <li><code>siteLimitTimeoutMs</code>: vechimea maxima a ultimei limite de site.</li>
2205
+ <li><code>missingSiteLimitBehavior</code> si <code>missingSiteLimitValueW</code>: fallback cand limita de site lipseste.</li>
2206
+ <li><code>staleTelemetryBehavior</code> si <code>staleTelemetryFixedValueW</code>: fallback pentru invertor fara telemetrie fresh.</li>
2207
+ <li><code>waitForFreshTelemetryOnStartup</code>: asteapta telemetrie fresh dupa deploy.</li>
2208
+ <li><code>recalculateOnTelemetry</code>: recalculeaza si la update-uri de telemetrie, nu doar la limita noua.</li>
2209
+ <li><code>telemetryFilterAlpha</code>: filtreaza numai ponderile strategiei Current power; valori mai mici reduc redistribuirile rapide fara a modifica totalul PV publicat.</li>
2210
+ <li><code>rampType</code> si <code>rampValue</code>: rampa de baza trimisa catre write.</li>
2211
+ <li><code>rampFromGlobal</code>, <code>rampTypeGlobalVar</code>, <code>rampUpGlobalVar</code>, <code>rampDownGlobalVar</code>: citesc tipul si valorile directionale din context global.</li>
2212
+ <li><code>rampUpValue</code> si <code>rampDownValue</code>: suprascriu rampa de baza pentru crestere sau scadere de limita.</li>
2213
+ <li><code>allocationSmoothingAlpha</code>: filtrare a alocarii pentru a reduce salturile.</li>
2214
+ <li><code>allocationDeadbandW</code>: pastreaza ultima limita emisa daca schimbarea este mai mica decat pragul.</li>
2215
+ <li><code>telemetryRecalcDebounceMs</code>: grupeaza recalcularile declansate de telemetrie rapida.</li>
2216
+ <li><code>debugMode</code>: nivelul iesirii de diagnostic.</li>
2217
+ <li><code>publishTotalPvGlobal</code> si <code>totalPvGlobalVar</code>: publica totalul PV fresh in context global.</li>
2218
+ <li><code>inverters</code>: lista de invertoare, cu ID, min, max si enabled.</li>
2219
+ <li><code>inverters-container</code>: zona editorului in care sunt randate randurile din lista <code>inverters</code>.</li>
2220
+ </ul>
2221
+
2222
+ <h3>Model recomandat pentru control stabil de grid set point</h3>
2223
+ <pre>{
2224
+ "telemetryTimeoutMs": 6000,
2225
+ "siteLimitTimeoutMs": 8000,
2226
+ "telemetryFilterAlpha": 0.35,
2227
+ "allocationSmoothingAlpha": 0.35,
2228
+ "allocationDeadbandW": 300,
2229
+ "telemetryRecalcDebounceMs": 350,
2230
+ "rampType": "none"
2231
+ }</pre>
2232
+ <p>Acest model tolereaza jitter-ul normal de comunicatie, filtreaza ponderile Current power si reduce retrimiterile foarte mici, lasand <code>control-export</code> sa fie regulatorul principal. Timeouturile nu schimba politicile de siguranta: dupa expirare se aplica in continuare fallback-ul configurat.</p>
2233
+
2234
+
2235
+ </script>
2236
+
2237
+
2238
+
2239
+ <style>
2240
+
2241
+ .multi-inverter-editor {
2242
+
2243
+ --pr-border: #d7dde5;
2244
+
2245
+ --pr-muted: #667085;
2246
+
2247
+ --pr-surface: #f8fafc;
2248
+
2249
+ --mi-invalid: #d9534f;
2250
+
2251
+ }
2252
+
2253
+
2254
+ .multi-inverter-editor .pr-section {
2255
+
2256
+ margin: 0 0 18px;
2257
+
2258
+ padding: 14px 16px 16px;
2259
+
2260
+ border: 1px solid var(--pr-border);
2261
+
2262
+ border-radius: 10px;
2263
+
2264
+ background: var(--pr-surface);
2265
+
2266
+ }
2267
+
2268
+
2269
+ .multi-inverter-editor .pr-section-title {
2270
+
2271
+ display: flex;
2272
+
2273
+ align-items: center;
2274
+
2275
+ gap: 8px;
2276
+
2277
+ margin: 0 0 4px;
2278
+
2279
+ font-size: 15px;
2280
+
2281
+ font-weight: 600;
2282
+
2283
+ color: #243447;
2284
+
2285
+ }
2286
+
2287
+
2288
+ .multi-inverter-editor .pr-section-note {
2289
+
2290
+ margin: 0 0 14px;
2291
+
2292
+ color: var(--pr-muted);
2293
+
2294
+ font-size: 12px;
2295
+
2296
+ line-height: 1.45;
2297
+
2298
+ }
2299
+
2300
+
2301
+ .multi-inverter-editor .pr-grid {
2302
+
2303
+ display: grid;
2304
+
2305
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
2306
+
2307
+ gap: 12px 16px;
2308
+
2309
+ }
2310
+
2311
+
2312
+ .multi-inverter-editor .pr-grid-2 {
2313
+
2314
+ grid-template-columns: repeat(2, minmax(320px, 1fr));
2315
+
2316
+ }
2317
+
2318
+
2319
+ .multi-inverter-editor .pr-span-2 {
2320
+
2321
+ grid-column: 1 / -1;
2322
+
2323
+ }
2324
+
2325
+
2326
+ .multi-inverter-editor .form-row {
2327
+
2328
+ margin-bottom: 12px;
2329
+
2330
+ }
2331
+
2332
+
2333
+ .multi-inverter-editor .pr-grid .form-row,
2334
+
2335
+ .multi-inverter-editor .form-row:last-child {
2336
+
2337
+ margin-bottom: 0;
2338
+
2339
+ }
2340
+
2341
+
2342
+ .multi-inverter-editor .form-row > label {
2343
+
2344
+ width: 150px;
2345
+
2346
+ font-weight: 600;
2347
+
2348
+ }
2349
+
2350
+
2351
+ .multi-inverter-editor .form-row > input[type="text"],
2352
+
2353
+ .multi-inverter-editor .form-row > input[type="number"],
2354
+
2355
+ .multi-inverter-editor .form-row > select {
2356
+
2357
+ width: calc(100% - 160px);
2358
+
2359
+ }
2360
+
2361
+
2362
+ .multi-inverter-editor .pr-checkbox-row > input[type="checkbox"] {
2363
+
2364
+ display: inline-block;
2365
+
2366
+ width: auto;
2367
+
2368
+ vertical-align: top;
2369
+
2370
+ }
2371
+
2372
+
2373
+ .multi-inverter-editor .pr-inline-note {
2374
+
2375
+ margin-left: 8px;
2376
+
2377
+ color: var(--pr-muted);
2378
+
2379
+ line-height: 1.4;
2380
+
2381
+ }
2382
+
2383
+
2384
+ .multi-inverter-editor .fronius-meta-info {
2385
+
2386
+ color: var(--pr-muted);
2387
+
2388
+ line-height: 1.45;
2389
+
2390
+ padding-top: 2px;
2391
+
2392
+ }
2393
+
2394
+
2395
+ .multi-inverter-editor .pr-toolbar {
2396
+
2397
+ display: flex;
2398
+
2399
+ flex-wrap: wrap;
2400
+
2401
+ gap: 8px;
2402
+
2403
+ margin-bottom: 10px;
2404
+
2405
+ align-items: center;
2406
+
2407
+ }
2408
+
2409
+
2410
+ .multi-inverter-editor #node-mi-add-inverter {
2411
+
2412
+ user-select: none;
2413
+
2414
+ -webkit-user-select: none;
2415
+
2416
+ -webkit-touch-callout: none;
2417
+
2418
+ }
2419
+
2420
+
2421
+ .multi-inverter-editor .mi-inverters-container {
2422
+
2423
+ border: 1px solid var(--pr-border);
2424
+
2425
+ border-radius: 10px;
2426
+
2427
+ background: #fff;
2428
+
2429
+ padding: 10px;
2430
+
2431
+ }
2432
+
2433
+
2434
+ .multi-inverter-editor .mi-row {
2435
+
2436
+ display: grid;
2437
+
2438
+ grid-template-columns: 32px 1.2fr 0.9fr 0.9fr 0.9fr 92px 80px;
2439
+
2440
+ gap: 8px;
2441
+
2442
+ align-items: center;
2443
+
2444
+ margin-bottom: 8px;
2445
+
2446
+ }
2447
+
2448
+
2449
+ .multi-inverter-editor .mi-inverters-container.mi-compact .mi-row {
2450
+
2451
+ gap: 6px;
2452
+
2453
+ margin-bottom: 6px;
2454
+
2455
+ }
2456
+
2457
+
2458
+ .multi-inverter-editor .mi-inverters-container.mi-compact .mi-row > input[type="text"],
2459
+
2460
+ .multi-inverter-editor .mi-inverters-container.mi-compact .mi-row > input[type="number"] {
2461
+
2462
+ height: 30px;
2463
+
2464
+ padding: 3px 6px;
2465
+
2466
+ font-size: 12px;
2467
+
2468
+ }
2469
+
2470
+
2471
+ .multi-inverter-editor .mi-inverters-container.mi-compact .mi-row-remove {
2472
+
2473
+ min-width: 64px;
2474
+
2475
+ padding: 3px 6px;
2476
+
2477
+ }
2478
+
2479
+
2480
+ .multi-inverter-editor #node-input-inverters-container {
2481
+
2482
+ margin-top: 8px;
2483
+
2484
+ }
2485
+
2486
+
2487
+ .multi-inverter-editor .mi-row:last-child {
2488
+
2489
+ margin-bottom: 0;
2490
+
2491
+ }
2492
+
2493
+
2494
+ .multi-inverter-editor .mi-row-header {
2495
+
2496
+ margin-bottom: 6px;
2497
+
2498
+ padding-bottom: 8px;
2499
+
2500
+ border-bottom: 1px solid var(--pr-border);
2501
+
2502
+ font-size: 12px;
2503
+
2504
+ font-weight: 700;
2505
+
2506
+ color: #344054;
2507
+
2508
+ }
2509
+
2510
+
2511
+ .multi-inverter-editor .mi-col-index {
2512
+
2513
+ text-align: center;
2514
+
2515
+ color: #667085;
2516
+
2517
+ font-weight: 600;
2518
+
2519
+ }
2520
+
2521
+
2522
+ .multi-inverter-editor .mi-col-enabled,
2523
+
2524
+ .multi-inverter-editor .mi-col-actions {
2525
+
2526
+ text-align: center;
2527
+
2528
+ }
2529
+
2530
+
2531
+ .multi-inverter-editor .mi-enabled-toggle {
2532
+
2533
+ display: flex;
2534
+
2535
+ align-items: center;
2536
+
2537
+ justify-content: center;
2538
+
2539
+ margin: 0;
2540
+
2541
+ font-weight: 400;
2542
+
2543
+ width: 100%;
2544
+
2545
+ height: 100%;
2546
+
2547
+ justify-self: center;
2548
+
2549
+ align-self: center;
2550
+
2551
+ }
2552
+
2553
+
2554
+ .multi-inverter-editor .mi-enabled-toggle > input[type="checkbox"] {
2555
+
2556
+ display: block;
2557
+
2558
+ width: auto;
2559
+
2560
+ margin: 0 auto;
2561
+
2562
+ flex: 0 0 auto;
2563
+
2564
+ }
2565
+
2566
+
2567
+ .multi-inverter-editor .mi-ramp-global-toggle {
2568
+
2569
+ padding-bottom: 10px;
2570
+
2571
+ border-bottom: 1px solid var(--pr-border);
2572
+
2573
+ }
2574
+
2575
+
2576
+ .multi-inverter-editor #node-row-rampGlobalType > label,
2577
+
2578
+ .multi-inverter-editor #node-row-rampGlobalUpValue > label,
2579
+
2580
+ .multi-inverter-editor #node-row-rampGlobalDownValue > label {
2581
+
2582
+ width: 165px;
2583
+
2584
+ }
2585
+
2586
+
2587
+ .multi-inverter-editor #node-row-rampGlobalType > input,
2588
+
2589
+ .multi-inverter-editor #node-row-rampGlobalUpValue > input,
2590
+
2591
+ .multi-inverter-editor #node-row-rampGlobalDownValue > input {
2592
+
2593
+ width: calc(100% - 175px);
2594
+
2595
+ }
2596
+
2597
+
2598
+ .multi-inverter-editor .mi-row-remove {
2599
+
2600
+ min-width: 72px;
2601
+
2602
+ }
2603
+
2604
+
2605
+ .multi-inverter-editor .mi-row > input[type="text"],
2606
+
2607
+ .multi-inverter-editor .mi-row > input[type="number"] {
2608
+
2609
+ width: 100%;
2610
+
2611
+ min-width: 0;
2612
+
2613
+ }
2614
+
2615
+
2616
+ .multi-inverter-editor #node-multi-inverter-validation {
2617
+
2618
+ color: var(--mi-invalid);
2619
+
2620
+ font-weight: 600;
2621
+
2622
+ }
2623
+
2624
+
2625
+ .multi-inverter-editor .mi-invalid {
2626
+
2627
+ border-color: var(--mi-invalid) !important;
2628
+
2629
+ }
2630
+
2631
+
2632
+ .multi-inverter-editor .mi-row-invalid {
2633
+
2634
+ background: rgba(217, 83, 79, 0.06);
2635
+
2636
+ border-radius: 8px;
2637
+
2638
+ padding: 6px;
2639
+
2640
+ }
2641
+
2642
+
2643
+ @media (max-width: 1100px) {
2644
+
2645
+ .multi-inverter-editor .pr-grid-2 {
2646
+
2647
+ grid-template-columns: 1fr;
2648
+
2649
+ }
2650
+
2651
+
2652
+ .multi-inverter-editor .mi-row {
2653
+
2654
+ grid-template-columns: 32px 1fr;
2655
+
2656
+ }
2657
+
2658
+
2659
+ .multi-inverter-editor .mi-row-header {
2660
+
2661
+ display: none;
2662
+
2663
+ }
2664
+
2665
+
2666
+ .multi-inverter-editor .mi-row-remove {
2667
+
2668
+ justify-self: start;
2669
+
2670
+ }
2671
+
2672
+ }
2673
+
2674
+ </style>