@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,1466 @@
1
+ module.exports = function (RED) {
2
+
3
+ const { runLicensedNode } = require("../lib/licensing/node-guard");
4
+
5
+ const {
6
+ ModbusRTU,
7
+ connectClient,
8
+ closeClient,
9
+ findBase,
10
+ scanModelDescriptors,
11
+ readModelByDescriptor,
12
+ readModelsByDescriptors,
13
+ probeInverterProfile
14
+ } = require("./lib/sunspec-core");
15
+
16
+ const {
17
+ buildSummary
18
+ } = require("./lib/sunspec-model-parsers");
19
+
20
+ const {
21
+ detectInverterProfile
22
+ } = require("./lib/inverter-profiles");
23
+
24
+ const {
25
+ atLeast
26
+ } = require("../lib/numbers");
27
+
28
+ const nodeRuntimeState = new Map();
29
+ const influxMeasurementRegistry = new Map();
30
+ const DEFAULT_INFLUX_PAYLOAD_FIELDS = [
31
+ "summary.ac.power_w",
32
+ "summary.dc.dc_total_power_w",
33
+ "summary.ac.voltage_l1_v",
34
+ "summary.ac.voltage_l2_v",
35
+ "summary.ac.voltage_l3_v"
36
+ ];
37
+ const DEFAULT_MULTI_GRID_PAYLOAD_FIELDS = [
38
+ "summary.meter.power_w",
39
+ "summary.meter.power_l1_w",
40
+ "summary.meter.power_l2_w",
41
+ "summary.meter.power_l3_w"
42
+ ];
43
+ const DEFAULT_METER_INFLUX_PAYLOAD_FIELDS = [
44
+ "summary.meter.power_w",
45
+ "summary.meter.power_l1_w",
46
+ "summary.meter.power_l2_w",
47
+ "summary.meter.power_l3_w",
48
+ "summary.meter.current_total_a",
49
+ "summary.meter.current_l1_a",
50
+ "summary.meter.current_l2_a",
51
+ "summary.meter.current_l3_a",
52
+ "summary.meter.voltage_ln_avg_v",
53
+ "summary.meter.voltage_l1_v",
54
+ "summary.meter.voltage_l2_v",
55
+ "summary.meter.voltage_l3_v",
56
+ "summary.meter.voltage_ll_avg_v",
57
+ "summary.meter.voltage_ll_ab_v",
58
+ "summary.meter.voltage_ll_bc_v",
59
+ "summary.meter.voltage_ll_ca_v",
60
+ "summary.meter.frequency_hz",
61
+ "summary.meter.apparent_power_va",
62
+ "summary.meter.apparent_power_l1_va",
63
+ "summary.meter.apparent_power_l2_va",
64
+ "summary.meter.apparent_power_l3_va",
65
+ "summary.meter.reactive_power_var",
66
+ "summary.meter.reactive_power_l1_var",
67
+ "summary.meter.reactive_power_l2_var",
68
+ "summary.meter.reactive_power_l3_var",
69
+ "summary.meter.power_factor"
70
+ ];
71
+ const METER_MODEL_IDS = [201, 202, 203, 204, 211, 212, 213, 214];
72
+
73
+ function toPollInterval(value, fallback) {
74
+ const text = String(value || "").trim().toLowerCase();
75
+ if (text === "off" || text === "0") return 0;
76
+ const n = Math.round(Number(value));
77
+ return Number.isFinite(n) && n > 0 ? n : fallback;
78
+ }
79
+
80
+ function getDefaultInfluxPayloadFieldsForDeviceType(deviceType) {
81
+ return deviceType === "smart_meter"
82
+ ? DEFAULT_METER_INFLUX_PAYLOAD_FIELDS.slice()
83
+ : DEFAULT_INFLUX_PAYLOAD_FIELDS.slice();
84
+ }
85
+
86
+ function setNodeRuntimeState(nodeId, state) {
87
+ nodeRuntimeState.set(nodeId, {
88
+ ...state,
89
+ updated_at: new Date().toISOString()
90
+ });
91
+ }
92
+
93
+ function clearNodeRuntimeState(nodeId) {
94
+ nodeRuntimeState.delete(nodeId);
95
+ }
96
+
97
+ function getByPath(obj, path) {
98
+
99
+ if (!path || path === "full") {
100
+ return obj;
101
+ }
102
+
103
+ return path
104
+ .split(".")
105
+ .reduce(function (acc, key) {
106
+ return acc !== null && acc !== undefined ? acc[key] : undefined;
107
+ }, obj);
108
+
109
+ }
110
+
111
+ function setByPath(obj, path, value) {
112
+
113
+ if (!path || path === "full") {
114
+ return;
115
+ }
116
+
117
+ const parts = path.split(".");
118
+ let current = obj;
119
+
120
+ for (let i = 0; i < parts.length - 1; i++) {
121
+ const key = parts[i];
122
+
123
+ if (current[key] === undefined) {
124
+ current[key] = /^\d+$/.test(parts[i + 1]) ? [] : {};
125
+ }
126
+
127
+ current = current[key];
128
+ }
129
+
130
+ current[parts[parts.length - 1]] = value;
131
+
132
+ }
133
+
134
+ function normalizePayloadFields(fields) {
135
+
136
+ if (Array.isArray(fields) && fields.length) {
137
+ const normalized = Array.from(new Set(fields.filter(function (f) {
138
+ return !!String(f || "").trim();
139
+ })));
140
+
141
+ if (!normalized.length) {
142
+ return ["full"];
143
+ }
144
+
145
+ if (normalized.includes("full")) {
146
+ return ["full"];
147
+ }
148
+
149
+ return normalized;
150
+ }
151
+
152
+ if (typeof fields === "string" && fields.trim()) {
153
+ const trimmed = fields.trim();
154
+
155
+ if (trimmed[0] === "[") {
156
+ try {
157
+ const parsed = JSON.parse(trimmed);
158
+ if (Array.isArray(parsed)) {
159
+ return normalizePayloadFields(parsed);
160
+ }
161
+ } catch (e) {
162
+ // ignore
163
+ }
164
+ }
165
+
166
+ return [trimmed];
167
+ }
168
+
169
+ return ["full"];
170
+
171
+ }
172
+
173
+ function getPayloadLabel(path) {
174
+
175
+ const labels = {
176
+
177
+ "full": "Toate valorile",
178
+
179
+ "summary.ac.power_w": "Putere AC",
180
+ "summary.ac.current_total_a": "Curent total",
181
+ "summary.ac.current_l1_a": "Curent L1",
182
+ "summary.ac.current_l2_a": "Curent L2",
183
+ "summary.ac.current_l3_a": "Curent L3",
184
+ "summary.ac.voltage_ll_ab_v": "Tensiune AB",
185
+ "summary.ac.voltage_ll_bc_v": "Tensiune BC",
186
+ "summary.ac.voltage_ll_ca_v": "Tensiune CA",
187
+ "summary.ac.voltage_l1_v": "Tensiune L1",
188
+ "summary.ac.voltage_l2_v": "Tensiune L2",
189
+ "summary.ac.voltage_l3_v": "Tensiune L3",
190
+ "summary.ac.frequency_hz": "Frecvență",
191
+ "summary.ac.apparent_power_va": "Putere aparentă",
192
+ "summary.ac.reactive_power_var": "Putere reactivă",
193
+
194
+ "summary.meter.model_id": "Model ID",
195
+ "summary.meter.meter_model": "Model",
196
+ "summary.meter.power_w": "Putere activa",
197
+ "summary.meter.power_l1_w": "Putere L1",
198
+ "summary.meter.power_l2_w": "Putere L2",
199
+ "summary.meter.power_l3_w": "Putere L3",
200
+ "summary.meter.current_total_a": "Curent total",
201
+ "summary.meter.current_l1_a": "Curent L1",
202
+ "summary.meter.current_l2_a": "Curent L2",
203
+ "summary.meter.current_l3_a": "Curent L3",
204
+ "summary.meter.voltage_ln_avg_v": "Tensiune L-N medie",
205
+ "summary.meter.voltage_l1_v": "Tensiune L1",
206
+ "summary.meter.voltage_l2_v": "Tensiune L2",
207
+ "summary.meter.voltage_l3_v": "Tensiune L3",
208
+ "summary.meter.voltage_ll_avg_v": "Tensiune L-L medie",
209
+ "summary.meter.voltage_ll_ab_v": "Tensiune AB",
210
+ "summary.meter.voltage_ll_bc_v": "Tensiune BC",
211
+ "summary.meter.voltage_ll_ca_v": "Tensiune CA",
212
+ "summary.meter.frequency_hz": "Frecvență",
213
+ "summary.meter.apparent_power_va": "Putere aparentă",
214
+ "summary.meter.apparent_power_l1_va": "Putere aparentă L1",
215
+ "summary.meter.apparent_power_l2_va": "Putere aparentă L2",
216
+ "summary.meter.apparent_power_l3_va": "Putere aparentă L3",
217
+ "summary.meter.reactive_power_var": "Putere reactivă",
218
+ "summary.meter.reactive_power_l1_var": "Putere reactivă L1",
219
+ "summary.meter.reactive_power_l2_var": "Putere reactivă L2",
220
+ "summary.meter.reactive_power_l3_var": "Putere reactivă L3",
221
+ "summary.meter.power_factor": "Factor putere",
222
+ "summary.meter.power_factor_l1": "Factor putere L1",
223
+ "summary.meter.power_factor_l2": "Factor putere L2",
224
+ "summary.meter.power_factor_l3": "Factor putere L3",
225
+ "summary.meter.energy_import_wh": "Energie import",
226
+ "summary.meter.energy_import_l1_wh": "Energie import L1",
227
+ "summary.meter.energy_import_l2_wh": "Energie import L2",
228
+ "summary.meter.energy_import_l3_wh": "Energie import L3",
229
+ "summary.meter.energy_export_wh": "Energie export",
230
+ "summary.meter.energy_export_l1_wh": "Energie export L1",
231
+ "summary.meter.energy_export_l2_wh": "Energie export L2",
232
+ "summary.meter.energy_export_l3_wh": "Energie export L3",
233
+ "summary.meter.apparent_energy_import_vah": "Energie aparentă import",
234
+ "summary.meter.apparent_energy_export_vah": "Energie aparentă export",
235
+ "summary.meter.reactive_energy_import_q1_varh": "Energie reactivă import Q1",
236
+ "summary.meter.reactive_energy_import_q2_varh": "Energie reactivă import Q2",
237
+ "summary.meter.reactive_energy_export_q3_varh": "Energie reactivă export Q3",
238
+ "summary.meter.reactive_energy_export_q4_varh": "Energie reactivă export Q4",
239
+ "summary.meter.event_flags": "Event flags",
240
+
241
+ "summary.dc.dc_total_power_w": "Putere DC totală",
242
+ "summary.dc.module_count": "Număr module/stringuri DC",
243
+ "summary.dc.global_event": "Eveniment DC global",
244
+
245
+ "summary.dc.mppts.0.dc_voltage_v": "MPPT1 Tensiune",
246
+ "summary.dc.mppts.0.dc_current_a": "MPPT1 Curent",
247
+ "summary.dc.mppts.0.dc_power_w": "MPPT1 Putere",
248
+ "summary.dc.mppts.0.dc_energy_wh": "MPPT1 Energie",
249
+
250
+ "summary.dc.mppts.1.dc_voltage_v": "MPPT2 Tensiune",
251
+ "summary.dc.mppts.1.dc_current_a": "MPPT2 Curent",
252
+ "summary.dc.mppts.1.dc_power_w": "MPPT2 Putere",
253
+ "summary.dc.mppts.1.dc_energy_wh": "MPPT2 Energie",
254
+
255
+ "summary.dc.mppts.2.dc_voltage_v": "MPPT3 Tensiune",
256
+ "summary.dc.mppts.2.dc_current_a": "MPPT3 Curent",
257
+ "summary.dc.mppts.2.dc_power_w": "MPPT3 Putere",
258
+ "summary.dc.mppts.2.dc_energy_wh": "MPPT3 Energie",
259
+
260
+ "summary.dc.mppts.3.dc_voltage_v": "MPPT4 Tensiune",
261
+ "summary.dc.mppts.3.dc_current_a": "MPPT4 Curent",
262
+ "summary.dc.mppts.3.dc_power_w": "MPPT4 Putere",
263
+ "summary.dc.mppts.3.dc_energy_wh": "MPPT4 Energie",
264
+
265
+ "summary.common.manufacturer": "Producător",
266
+ "summary.common.model": "Model",
267
+ "summary.common.options": "Opțiuni",
268
+ "summary.common.version": "Versiune",
269
+ "summary.common.serial": "Serie",
270
+ "summary.common.device_address": "Adresă device"
271
+ };
272
+
273
+ return labels[path] || path;
274
+
275
+ }
276
+
277
+ function formatStatusValue(path, value, models, detection) {
278
+
279
+ if (path === "full") {
280
+
281
+ const profileText = detection && detection.profile_label
282
+ ? " | " + detection.profile_label
283
+ : "";
284
+
285
+ if (
286
+ value &&
287
+ value.summary &&
288
+ value.summary.ac &&
289
+ value.summary.ac.power_w !== null &&
290
+ value.summary.ac.power_w !== undefined
291
+ ) {
292
+ return String(value.summary.ac.power_w) + " W" + profileText;
293
+ }
294
+
295
+ if (
296
+ value &&
297
+ value.summary &&
298
+ value.summary.meter &&
299
+ value.summary.meter.power_w !== null &&
300
+ value.summary.meter.power_w !== undefined
301
+ ) {
302
+ return "meter " + String(value.summary.meter.power_w) + " W";
303
+ }
304
+
305
+ return String(models.length) + " models" + profileText;
306
+
307
+ }
308
+
309
+ if (value === undefined || value === null) {
310
+ return getPayloadLabel(path) + ": n/a";
311
+ }
312
+
313
+ const unitMap = {
314
+ "summary.ac.power_w": "W",
315
+ "summary.ac.current_total_a": "A",
316
+ "summary.ac.current_l1_a": "A",
317
+ "summary.ac.current_l2_a": "A",
318
+ "summary.ac.current_l3_a": "A",
319
+ "summary.ac.voltage_ll_ab_v": "V",
320
+ "summary.ac.voltage_ll_bc_v": "V",
321
+ "summary.ac.voltage_ll_ca_v": "V",
322
+ "summary.ac.voltage_l1_v": "V",
323
+ "summary.ac.voltage_l2_v": "V",
324
+ "summary.ac.voltage_l3_v": "V",
325
+ "summary.ac.frequency_hz": "Hz",
326
+ "summary.ac.apparent_power_va": "VA",
327
+ "summary.ac.reactive_power_var": "var",
328
+ "summary.meter.power_w": "W",
329
+ "summary.meter.power_l1_w": "W",
330
+ "summary.meter.power_l2_w": "W",
331
+ "summary.meter.power_l3_w": "W",
332
+ "summary.meter.current_total_a": "A",
333
+ "summary.meter.current_l1_a": "A",
334
+ "summary.meter.current_l2_a": "A",
335
+ "summary.meter.current_l3_a": "A",
336
+ "summary.meter.voltage_ln_avg_v": "V",
337
+ "summary.meter.voltage_l1_v": "V",
338
+ "summary.meter.voltage_l2_v": "V",
339
+ "summary.meter.voltage_l3_v": "V",
340
+ "summary.meter.voltage_ll_avg_v": "V",
341
+ "summary.meter.voltage_ll_ab_v": "V",
342
+ "summary.meter.voltage_ll_bc_v": "V",
343
+ "summary.meter.voltage_ll_ca_v": "V",
344
+ "summary.meter.frequency_hz": "Hz",
345
+ "summary.meter.apparent_power_va": "VA",
346
+ "summary.meter.apparent_power_l1_va": "VA",
347
+ "summary.meter.apparent_power_l2_va": "VA",
348
+ "summary.meter.apparent_power_l3_va": "VA",
349
+ "summary.meter.reactive_power_var": "var",
350
+ "summary.meter.reactive_power_l1_var": "var",
351
+ "summary.meter.reactive_power_l2_var": "var",
352
+ "summary.meter.reactive_power_l3_var": "var",
353
+ "summary.meter.energy_import_wh": "Wh",
354
+ "summary.meter.energy_import_l1_wh": "Wh",
355
+ "summary.meter.energy_import_l2_wh": "Wh",
356
+ "summary.meter.energy_import_l3_wh": "Wh",
357
+ "summary.meter.energy_export_wh": "Wh",
358
+ "summary.meter.energy_export_l1_wh": "Wh",
359
+ "summary.meter.energy_export_l2_wh": "Wh",
360
+ "summary.meter.energy_export_l3_wh": "Wh",
361
+ "summary.meter.apparent_energy_import_vah": "VAh",
362
+ "summary.meter.apparent_energy_export_vah": "VAh",
363
+ "summary.meter.reactive_energy_import_q1_varh": "varh",
364
+ "summary.meter.reactive_energy_import_q2_varh": "varh",
365
+ "summary.meter.reactive_energy_export_q3_varh": "varh",
366
+ "summary.meter.reactive_energy_export_q4_varh": "varh",
367
+ "summary.dc.dc_total_power_w": "W",
368
+
369
+ "summary.dc.mppts.0.dc_voltage_v": "V",
370
+ "summary.dc.mppts.0.dc_current_a": "A",
371
+ "summary.dc.mppts.0.dc_power_w": "W",
372
+ "summary.dc.mppts.0.dc_energy_wh": "Wh",
373
+
374
+ "summary.dc.mppts.1.dc_voltage_v": "V",
375
+ "summary.dc.mppts.1.dc_current_a": "A",
376
+ "summary.dc.mppts.1.dc_power_w": "W",
377
+ "summary.dc.mppts.1.dc_energy_wh": "Wh",
378
+
379
+ "summary.dc.mppts.2.dc_voltage_v": "V",
380
+ "summary.dc.mppts.2.dc_current_a": "A",
381
+ "summary.dc.mppts.2.dc_power_w": "W",
382
+ "summary.dc.mppts.2.dc_energy_wh": "Wh",
383
+
384
+ "summary.dc.mppts.3.dc_voltage_v": "V",
385
+ "summary.dc.mppts.3.dc_current_a": "A",
386
+ "summary.dc.mppts.3.dc_power_w": "W",
387
+ "summary.dc.mppts.3.dc_energy_wh": "Wh"
388
+ };
389
+
390
+ const label = getPayloadLabel(path);
391
+ const unit = unitMap[path] ? " " + unitMap[path] : "";
392
+
393
+ return label + ": " + value + unit;
394
+
395
+ }
396
+
397
+ function getTargetModelIdsForPath(path) {
398
+
399
+ if (!path || path === "full") {
400
+ return [];
401
+ }
402
+
403
+ if (path.indexOf("summary.common.") === 0) {
404
+ return [1];
405
+ }
406
+
407
+ if (path.indexOf("summary.ac.") === 0) {
408
+ return [101, 102, 103, 111, 112, 113];
409
+ }
410
+
411
+ if (path.indexOf("summary.dc.") === 0) {
412
+ return [160];
413
+ }
414
+
415
+ if (path.indexOf("summary.meter.") === 0) {
416
+ return METER_MODEL_IDS.slice();
417
+ }
418
+
419
+ return [];
420
+
421
+ }
422
+
423
+ function fieldPathToCompactKey(path) {
424
+
425
+ if (!path) {
426
+ return "value";
427
+ }
428
+
429
+ const trimmed = String(path).trim();
430
+
431
+ if (trimmed === "summary.ac.power_w") {
432
+ return "power_w";
433
+ }
434
+
435
+ const meterMapping = {
436
+ "summary.meter.power_w": "power_w",
437
+ "summary.meter.power_l1_w": "power_l1_w",
438
+ "summary.meter.power_l2_w": "power_l2_w",
439
+ "summary.meter.power_l3_w": "power_l3_w",
440
+ "summary.meter.current_total_a": "current_total_a",
441
+ "summary.meter.current_l1_a": "current_l1_a",
442
+ "summary.meter.current_l2_a": "current_l2_a",
443
+ "summary.meter.current_l3_a": "current_l3_a",
444
+ "summary.meter.voltage_ln_avg_v": "voltage_ln_avg_v",
445
+ "summary.meter.voltage_ll_avg_v": "voltage_ll_avg_v",
446
+ "summary.meter.frequency_hz": "frequency_hz",
447
+ "summary.meter.reactive_power_var": "reactive_power_var",
448
+ "summary.meter.apparent_power_va": "apparent_power_va",
449
+ "summary.meter.power_factor": "power_factor"
450
+ };
451
+
452
+ if (meterMapping[trimmed]) {
453
+ return meterMapping[trimmed];
454
+ }
455
+
456
+ return trimmed
457
+ .replace(/^summary\./, "")
458
+ .replace(/[^\w]+/g, "_")
459
+ .replace(/^_+|_+$/g, "");
460
+
461
+ }
462
+
463
+ function flattenNumericFields(source, prefix, target) {
464
+
465
+ Object.keys(source || {}).forEach(function (key) {
466
+ const value = source[key];
467
+ const nextKey = prefix ? prefix + "_" + key : key;
468
+
469
+ if (typeof value === "number" && Number.isFinite(value)) {
470
+ target[nextKey] = value;
471
+ return;
472
+ }
473
+
474
+ if (typeof value === "boolean") {
475
+ target[nextKey] = value;
476
+ return;
477
+ }
478
+
479
+ if (value && typeof value === "object" && !Array.isArray(value)) {
480
+ flattenNumericFields(value, nextKey, target);
481
+ }
482
+ });
483
+
484
+ }
485
+
486
+ function registerInfluxMeasurement(nodeId, identityKey) {
487
+
488
+ const key = String(identityKey || "").trim().toLowerCase();
489
+ if (!key) {
490
+ return;
491
+ }
492
+
493
+ if (!influxMeasurementRegistry.has(key)) {
494
+ influxMeasurementRegistry.set(key, new Set());
495
+ }
496
+
497
+ influxMeasurementRegistry.get(key).add(nodeId);
498
+
499
+ }
500
+
501
+ function unregisterInfluxMeasurement(nodeId, identityKey) {
502
+
503
+ const key = String(identityKey || "").trim().toLowerCase();
504
+ if (!key || !influxMeasurementRegistry.has(key)) {
505
+ return;
506
+ }
507
+
508
+ const ids = influxMeasurementRegistry.get(key);
509
+ ids.delete(nodeId);
510
+
511
+ if (!ids.size) {
512
+ influxMeasurementRegistry.delete(key);
513
+ }
514
+
515
+ }
516
+
517
+ function hasDuplicateInfluxMeasurement(nodeId, identityKey) {
518
+
519
+ const key = String(identityKey || "").trim().toLowerCase();
520
+ if (!key || !influxMeasurementRegistry.has(key)) {
521
+ return false;
522
+ }
523
+
524
+ const ids = influxMeasurementRegistry.get(key);
525
+ return ids.size > 1 || (ids.size === 1 && !ids.has(nodeId));
526
+
527
+ }
528
+
529
+ function getTargetModelIdsForPaths(paths) {
530
+
531
+ const ids = new Set();
532
+
533
+ normalizePayloadFields(paths).forEach(function (path) {
534
+ getTargetModelIdsForPath(path).forEach(function (modelId) {
535
+ ids.add(modelId);
536
+ });
537
+ });
538
+
539
+ return Array.from(ids);
540
+
541
+ }
542
+
543
+ function detectDeviceType(summary) {
544
+ if (summary && summary.meter) return "smart_meter";
545
+ if (summary && (summary.ac || summary.dc)) return "inverter";
546
+ return "unknown";
547
+ }
548
+
549
+ function getDefaultInfluxPayloadFields(summary) {
550
+ return detectDeviceType(summary) === "smart_meter"
551
+ ? DEFAULT_METER_INFLUX_PAYLOAD_FIELDS.slice()
552
+ : DEFAULT_INFLUX_PAYLOAD_FIELDS.slice();
553
+ }
554
+
555
+ function getDefaultControlField(summary) {
556
+ return detectDeviceType(summary) === "smart_meter"
557
+ ? "summary.meter.power_w"
558
+ : "summary.ac.power_w";
559
+ }
560
+
561
+ function toScadaNumber(value) {
562
+ const number = Number(value);
563
+ return Number.isFinite(number) ? number : null;
564
+ }
565
+
566
+ function buildScadaPayload(summary, deviceType) {
567
+ const values = deviceType === "smart_meter"
568
+ ? ((summary && summary.meter) || {})
569
+ : ((summary && summary.ac) || {});
570
+ return {
571
+ p: toScadaNumber(values.power_w),
572
+ q: toScadaNumber(values.reactive_power_var),
573
+ f: toScadaNumber(values.frequency_hz),
574
+ u_l1n: toScadaNumber(values.voltage_l1_v),
575
+ u_l2n: toScadaNumber(values.voltage_l2_v),
576
+ u_l3n: toScadaNumber(values.voltage_l3_v),
577
+ u_ln_avg: toScadaNumber(values.voltage_ln_avg_v),
578
+ u_l12: toScadaNumber(values.voltage_ll_ab_v),
579
+ u_l23: toScadaNumber(values.voltage_ll_bc_v),
580
+ u_l31: toScadaNumber(values.voltage_ll_ca_v),
581
+ u_ll_avg: toScadaNumber(values.voltage_ll_avg_v)
582
+ };
583
+ }
584
+
585
+ function buildDescriptorMap(descriptors) {
586
+
587
+ const map = {};
588
+
589
+ descriptors.forEach(function (descriptor) {
590
+ map[descriptor.model_id] = descriptor;
591
+ });
592
+
593
+ return map;
594
+
595
+ }
596
+
597
+ function pickSelectedEnvelope(envelope, payloadFields) {
598
+
599
+ const normalized = normalizePayloadFields(payloadFields);
600
+
601
+ if (normalized.includes("full")) {
602
+ return envelope;
603
+ }
604
+
605
+ const result = {};
606
+
607
+ normalized.forEach(function (path) {
608
+ setByPath(result, path, getByPath(envelope, path));
609
+ });
610
+
611
+ return result;
612
+
613
+ }
614
+
615
+ RED.httpAdmin.get("/fronius-read/probe", async function (req, res) {
616
+
617
+ const host = req.query.host;
618
+ const port = atLeast(req.query.port, 1, 502);
619
+ const unitId = atLeast(req.query.unitId, 1, 1);
620
+
621
+ if (!host) {
622
+ res.json({ ok: false, error: "Missing host" });
623
+ return;
624
+ }
625
+
626
+ try {
627
+ const result = await probeInverterProfile(host, port, unitId);
628
+ res.json(result);
629
+ } catch (e) {
630
+ res.json({
631
+ ok: false,
632
+ error: e.message || String(e)
633
+ });
634
+ }
635
+
636
+ });
637
+
638
+ RED.httpAdmin.get("/fronius-read/state/:id", function (req, res) {
639
+
640
+ const nodeId = req.params.id;
641
+
642
+ if (!nodeId) {
643
+ res.json({ ok: false, error: "Missing node id" });
644
+ return;
645
+ }
646
+
647
+ const state = nodeRuntimeState.get(nodeId);
648
+
649
+ if (!state) {
650
+ res.json({ ok: false, error: "No cached state" });
651
+ return;
652
+ }
653
+
654
+ res.json({
655
+ ok: true,
656
+ state: state
657
+ });
658
+
659
+ });
660
+
661
+ function FroniusReadNode(config) {
662
+
663
+ RED.nodes.createNode(this, config);
664
+ const node = this;
665
+
666
+ runLicensedNode(node, RED, config, function startLicensedRuntime() {
667
+
668
+ node.host = config.host;
669
+ node.port = atLeast(config.port, 1, 502);
670
+ node.unitId = atLeast(config.unitId, 1, 1);
671
+ node.pollInterval = toPollInterval(config.pollInterval, 5000);
672
+ node.inverterType = "auto";
673
+ node.payloadField = config.payloadField || "full";
674
+ node.payloadFields = normalizePayloadFields(
675
+ config.payloadFields !== undefined
676
+ ? config.payloadFields
677
+ : config.payloadField
678
+ );
679
+ node.debugMode = !!config.debugMode;
680
+ node.multiInverter = !!config.multiInverter;
681
+ node.controlExport = !!config.controlExport;
682
+ node.multiGrid = !!config.multiGrid;
683
+ node.scadaAggregator = !!config.scadaAggregator;
684
+ node.multiGridUseSelectedFields = config.multiGridUseSelectedFields !== undefined
685
+ ? !!config.multiGridUseSelectedFields
686
+ : false;
687
+ if (node.multiGrid) {
688
+ node.multiInverter = false;
689
+ node.controlExport = false;
690
+ }
691
+ if (node.multiInverter && node.controlExport) {
692
+ node.controlExport = false;
693
+ }
694
+ node.influxDb = !!config.influxDb;
695
+ if (node.scadaAggregator) {
696
+ node.multiInverter = false;
697
+ node.controlExport = false;
698
+ node.multiGrid = false;
699
+ node.influxDb = false;
700
+ }
701
+ node.influxMeasurement = String(config.influxMeasurement || "").trim();
702
+ node.influxNode = String(config.influxNode || "").trim();
703
+ node.multiInverterExpertMode = !!config.multiInverterExpertMode;
704
+ node.multiInverterUseSelectedFields = config.multiInverterUseSelectedFields !== undefined
705
+ ? !!config.multiInverterUseSelectedFields
706
+ : !!config.multiInverterExpertMode;
707
+ if (node.influxDb && !(node.multiInverter || node.controlExport || node.multiGrid)) {
708
+ const selectedInfluxFields = normalizePayloadFields(node.payloadFields).filter(function (field) {
709
+ return field !== "full";
710
+ });
711
+ node.payloadFields = node.multiInverterUseSelectedFields
712
+ ? (selectedInfluxFields.length ? selectedInfluxFields : DEFAULT_INFLUX_PAYLOAD_FIELDS.slice())
713
+ : DEFAULT_INFLUX_PAYLOAD_FIELDS.slice();
714
+ node.payloadField = node.payloadFields[0];
715
+ }
716
+ node.inverterId = (config.inverterId || "").trim();
717
+ const compactOutputMode = node.multiInverter || node.controlExport || node.multiGrid;
718
+ const singleOutputMode = compactOutputMode || node.influxDb || node.scadaAggregator;
719
+ const primaryOutputs = compactOutputMode
720
+ ? 1
721
+ : (node.payloadFields.includes("full") ? 1 : Math.max(1, node.payloadFields.length));
722
+ node.outputs = (singleOutputMode ? 1 : primaryOutputs) + (node.debugMode ? 1 : 0);
723
+
724
+ let client = new ModbusRTU();
725
+ let timer = null;
726
+ let busy = false;
727
+ let closing = false;
728
+
729
+ let sunspecBase = null;
730
+ let modelDescriptors = null;
731
+ let modelDescriptorMap = null;
732
+ let cachedDetection = null;
733
+ let cachedDeviceType = null;
734
+ let registeredInfluxIdentityKey = "";
735
+
736
+ async function connect() {
737
+ await connectClient(client, node.host, node.port, node.unitId);
738
+ }
739
+
740
+ async function resetConnection() {
741
+
742
+ try {
743
+ await closeClient(client);
744
+ } catch (e) {
745
+ // ignore
746
+ }
747
+
748
+ client = new ModbusRTU();
749
+ sunspecBase = null;
750
+ modelDescriptors = null;
751
+ modelDescriptorMap = null;
752
+ cachedDetection = null;
753
+ cachedDeviceType = null;
754
+
755
+ }
756
+
757
+ async function ensureDiscovery() {
758
+
759
+ await connect();
760
+
761
+ if (!sunspecBase) {
762
+ sunspecBase = await findBase(client);
763
+ }
764
+
765
+ if (!modelDescriptors) {
766
+ modelDescriptors = await scanModelDescriptors(client, sunspecBase);
767
+ modelDescriptorMap = buildDescriptorMap(modelDescriptors);
768
+ }
769
+
770
+ if (!cachedDetection) {
771
+ const discoveryModels = await readModelsByDescriptors(client, modelDescriptors);
772
+ const discoverySummary = buildSummary(discoveryModels);
773
+ cachedDetection = detectInverterProfile(discoverySummary);
774
+ cachedDeviceType = detectDeviceType(discoverySummary);
775
+ }
776
+
777
+ }
778
+
779
+ async function readFullPayload() {
780
+
781
+ const models = await readModelsByDescriptors(client, modelDescriptors);
782
+ const summary = buildSummary(models);
783
+ const detection = detectInverterProfile(summary);
784
+
785
+ cachedDetection = detection;
786
+ cachedDeviceType = detectDeviceType(summary);
787
+
788
+ return {
789
+ mode: "full",
790
+ models: models,
791
+ summary: summary,
792
+ detection: detection
793
+ };
794
+
795
+ }
796
+
797
+ async function readOptimizedPayload(payloadFields) {
798
+
799
+ const targetModelIds = getTargetModelIdsForPaths(payloadFields);
800
+
801
+ if (!targetModelIds.length || !modelDescriptorMap) {
802
+ return readFullPayload();
803
+ }
804
+
805
+ const descriptors = targetModelIds
806
+ .map(function (id) {
807
+ return modelDescriptorMap[id];
808
+ })
809
+ .filter(Boolean);
810
+
811
+ if (!descriptors.length) {
812
+ return readFullPayload();
813
+ }
814
+
815
+ let models;
816
+
817
+ if (descriptors.length === 1) {
818
+ models = [await readModelByDescriptor(client, descriptors[0])];
819
+ } else {
820
+ models = await readModelsByDescriptors(client, descriptors);
821
+ }
822
+
823
+ const summary = buildSummary(models);
824
+ const detection = cachedDetection || detectInverterProfile(summary);
825
+ cachedDeviceType = detectDeviceType(summary);
826
+
827
+ return {
828
+ mode: "optimized",
829
+ models: models,
830
+ summary: summary,
831
+ detection: detection
832
+ };
833
+
834
+ }
835
+
836
+ function getEffectiveInverterId() {
837
+
838
+ return node.inverterId || (
839
+ node.multiGrid || cachedDeviceType === "smart_meter"
840
+ ? ("meter" + String(node.unitId))
841
+ : ("inv" + String(node.unitId))
842
+ );
843
+
844
+ }
845
+
846
+ function getInfluxMeasurement() {
847
+
848
+ return node.influxMeasurement || getEffectiveInverterId();
849
+
850
+ }
851
+
852
+ function getInfluxIdentityKey() {
853
+
854
+ return String(getInfluxMeasurement() || "").trim().toLowerCase() + "::" + String(getEffectiveInverterId() || "").trim().toLowerCase();
855
+
856
+ }
857
+
858
+ function hasInfluxMeasurementConflict() {
859
+
860
+ return node.influxDb && hasDuplicateInfluxMeasurement(node.id, getInfluxIdentityKey());
861
+
862
+ }
863
+
864
+ function buildInfluxFields(envelope, selectedFields) {
865
+
866
+ const fields = {};
867
+
868
+ if (selectedFields.includes("full")) {
869
+ flattenNumericFields(envelope && envelope.summary ? envelope.summary : {}, "", fields);
870
+ return fields;
871
+ }
872
+
873
+ selectedFields.forEach(function (fieldPath) {
874
+ const value = getByPath(envelope, fieldPath);
875
+ const key = fieldPathToCompactKey(fieldPath);
876
+
877
+ if (typeof value === "number" && Number.isFinite(value)) {
878
+ fields[key] = value;
879
+ return;
880
+ }
881
+
882
+ if (typeof value === "boolean") {
883
+ fields[key] = value;
884
+ return;
885
+ }
886
+
887
+ if (value && typeof value === "object" && !Array.isArray(value)) {
888
+ flattenNumericFields(value, key, fields);
889
+ }
890
+ });
891
+
892
+ return fields;
893
+
894
+ }
895
+
896
+ function scheduleNextPoll() {
897
+
898
+ if (closing) {
899
+ return;
900
+ }
901
+
902
+ timer = setTimeout(function () {
903
+ pollLoop();
904
+ }, node.pollInterval);
905
+
906
+ }
907
+
908
+ async function pollOnce() {
909
+
910
+ if (busy) {
911
+ return;
912
+ }
913
+
914
+ busy = true;
915
+
916
+ try {
917
+
918
+ await ensureDiscovery();
919
+
920
+ const discoveredDeviceType = cachedDeviceType || "unknown";
921
+ const inverterCompactRequested = node.multiInverter || node.controlExport;
922
+ const multiGridRequested = node.multiGrid;
923
+ const multiGridOutputMode = multiGridRequested && discoveredDeviceType === "smart_meter";
924
+ const compactOutputMode = inverterCompactRequested && discoveredDeviceType !== "smart_meter";
925
+ const effectivePayloadFields = multiGridOutputMode
926
+ ? (
927
+ node.multiGridUseSelectedFields
928
+ ? (function () {
929
+ const selected = normalizePayloadFields(node.payloadFields).filter(function (field) {
930
+ return field !== "full";
931
+ });
932
+ return selected.length ? selected : DEFAULT_MULTI_GRID_PAYLOAD_FIELDS.slice();
933
+ })()
934
+ : DEFAULT_MULTI_GRID_PAYLOAD_FIELDS.slice()
935
+ )
936
+ : multiGridRequested
937
+ ? ["full"]
938
+ : inverterCompactRequested && discoveredDeviceType === "smart_meter"
939
+ ? ["full"]
940
+ : compactOutputMode
941
+ ? (
942
+ node.multiInverterUseSelectedFields
943
+ ? (function () {
944
+ const selected = normalizePayloadFields(node.payloadFields).filter(function (field) {
945
+ return field !== "full";
946
+ });
947
+ return selected.length ? selected : ["summary.ac.power_w"];
948
+ })()
949
+ : (node.controlExport ? ["summary.ac.power_w", "summary.meter.power_w"] : ["summary.ac.power_w"])
950
+ )
951
+ : node.influxDb
952
+ ? (function () {
953
+ const defaultInfluxFields = getDefaultInfluxPayloadFieldsForDeviceType(discoveredDeviceType);
954
+ if (!node.multiInverterUseSelectedFields) {
955
+ return defaultInfluxFields;
956
+ }
957
+ const selected = normalizePayloadFields(node.payloadFields).filter(function (field) {
958
+ return field !== "full";
959
+ });
960
+ return selected.length ? selected : defaultInfluxFields;
961
+ })()
962
+ : normalizePayloadFields(node.payloadFields);
963
+
964
+ const readResult =
965
+ effectivePayloadFields.includes("full")
966
+ ? await readFullPayload()
967
+ : await readOptimizedPayload(effectivePayloadFields);
968
+
969
+ const models = readResult.models;
970
+ const summary = readResult.summary;
971
+ const detection = readResult.detection;
972
+ const deviceType = detectDeviceType(summary);
973
+ const defaultControlField = getDefaultControlField(summary);
974
+
975
+ const modelList = modelDescriptors
976
+ ? modelDescriptors.map(function (m) {
977
+ return {
978
+ model_id: m.model_id,
979
+ addr: m.addr,
980
+ len: m.len
981
+ };
982
+ })
983
+ : [];
984
+
985
+ const envelope = {
986
+ status: "ok",
987
+ ip: node.host,
988
+ unit_id: node.unitId,
989
+ device_type: deviceType,
990
+ inverter_type: node.inverterType,
991
+ inverter_profile: detection.profile_key,
992
+ inverter_profile_label: detection.profile_label,
993
+ manufacturer: detection.manufacturer,
994
+ family: detection.family,
995
+ phase_mode: detection.phase_mode,
996
+ mppt_count: detection.mppt_count,
997
+ detection_source: detection.detection_source,
998
+ optimized_read: readResult.mode === "optimized",
999
+ sunspec_base: sunspecBase,
1000
+ model_count: modelDescriptors ? modelDescriptors.length : models.length,
1001
+ models: modelList,
1002
+ summary: summary,
1003
+ ts: new Date().toISOString()
1004
+ };
1005
+
1006
+ if (effectivePayloadFields.includes("full")) {
1007
+ envelope.raw_models = models;
1008
+ }
1009
+
1010
+ const debugMessage = node.debugMode
1011
+ ? {
1012
+ topic: "fronius/raw",
1013
+ payload: {
1014
+ status: "ok",
1015
+ ip: node.host,
1016
+ unit_id: node.unitId,
1017
+ device_type: deviceType,
1018
+ inverter_type: node.inverterType,
1019
+ inverter_profile: detection.profile_key,
1020
+ inverter_profile_label: detection.profile_label,
1021
+ phase_mode: detection.phase_mode,
1022
+ mppt_count: detection.mppt_count,
1023
+ optimized_read: readResult.mode === "optimized",
1024
+ sunspec_base: sunspecBase,
1025
+ raw_models: models,
1026
+ summary: summary,
1027
+ ts: new Date().toISOString()
1028
+ },
1029
+ payload_field: "raw_models",
1030
+ payload_fields: effectivePayloadFields.slice()
1031
+ }
1032
+ : null;
1033
+
1034
+ const selectedPayload =
1035
+ effectivePayloadFields.includes("full")
1036
+ ? envelope
1037
+ : pickSelectedEnvelope(envelope, effectivePayloadFields);
1038
+
1039
+ const statusText =
1040
+ effectivePayloadFields.includes("full")
1041
+ ? formatStatusValue("full", selectedPayload, models, detection)
1042
+ : (
1043
+ effectivePayloadFields.length === 1
1044
+ ? formatStatusValue(
1045
+ effectivePayloadFields[0],
1046
+ getByPath(envelope, effectivePayloadFields[0]),
1047
+ models,
1048
+ detection
1049
+ )
1050
+ : String(effectivePayloadFields.length) + " valori"
1051
+ );
1052
+
1053
+ node.status({
1054
+ fill: "green",
1055
+ shape: "dot",
1056
+ text: statusText
1057
+ });
1058
+
1059
+ setNodeRuntimeState(node.id, {
1060
+ status: "ok",
1061
+ node_id: node.id,
1062
+ ip: node.host,
1063
+ unit_id: node.unitId,
1064
+ device_type: deviceType,
1065
+ payload_field: effectivePayloadFields[0] || "full",
1066
+ payload_fields: effectivePayloadFields,
1067
+ inverter_profile: detection.profile_key,
1068
+ inverter_profile_label: detection.profile_label,
1069
+ manufacturer: detection.manufacturer,
1070
+ family: detection.family,
1071
+ phase_mode: detection.phase_mode,
1072
+ mppt_count: detection.mppt_count,
1073
+ detection_source: detection.detection_source,
1074
+ optimized_read: readResult.mode === "optimized",
1075
+ summary: summary
1076
+ });
1077
+
1078
+ let message;
1079
+
1080
+ if (node.scadaAggregator) {
1081
+ const sourceId = getEffectiveInverterId();
1082
+ message = {
1083
+ topic: sourceId,
1084
+ scadaSource: sourceId,
1085
+ scadaGroup: deviceType === "smart_meter" ? "grid_meter" : "pv_inverter",
1086
+ payload: buildScadaPayload(summary, deviceType),
1087
+ device_type: deviceType,
1088
+ inverter_id: sourceId,
1089
+ ip: node.host,
1090
+ unit_id: node.unitId,
1091
+ summary_fragment: summary,
1092
+ ts: envelope.ts
1093
+ };
1094
+ if (debugMessage) node.send([message, debugMessage]); else node.send(message);
1095
+ return;
1096
+ }
1097
+
1098
+ if (node.influxDb) {
1099
+
1100
+ if (hasInfluxMeasurementConflict()) {
1101
+ node.status({
1102
+ fill: "red",
1103
+ shape: "ring",
1104
+ text: "inverter_id+measurement duplicat"
1105
+ });
1106
+ node.error("Fronius InfluxDB: combinatie duplicata Inverter ID + Measurement ('" + getEffectiveInverterId() + "' @ '" + getInfluxMeasurement() + "'). Configureaza un Inverter ID unic pentru acelasi measurement.");
1107
+ return;
1108
+ }
1109
+
1110
+ const inverterId = getEffectiveInverterId();
1111
+ const influxFields = buildInfluxFields(envelope, effectivePayloadFields);
1112
+ const influxMessage = {
1113
+ topic: inverterId,
1114
+ payload: influxFields,
1115
+ payload_field: effectivePayloadFields.includes("full") ? "full" : (effectivePayloadFields[0] || "full"),
1116
+ payload_fields: effectivePayloadFields.slice(),
1117
+ inverter_id: inverterId,
1118
+ device_type: deviceType,
1119
+ inverter_type: node.inverterType,
1120
+ inverter_profile: detection.profile_key,
1121
+ inverter_profile_label: detection.profile_label,
1122
+ manufacturer: detection.manufacturer,
1123
+ family: detection.family,
1124
+ phase_mode: detection.phase_mode,
1125
+ mppt_count: detection.mppt_count,
1126
+ detection_source: detection.detection_source,
1127
+ optimized_read: readResult.mode === "optimized",
1128
+ summary_fragment: summary,
1129
+ influx: {
1130
+ measurement: getInfluxMeasurement(),
1131
+ tags: {
1132
+ device_type: deviceType === "smart_meter" ? "fronius_smart_meter" : "fronius_inverter",
1133
+ inverter_id: inverterId,
1134
+ host: node.host,
1135
+ unit_id: String(node.unitId),
1136
+ inverter_profile: detection.profile_key || ""
1137
+ },
1138
+ fields: influxFields,
1139
+ timestamp: envelope.ts
1140
+ }
1141
+ };
1142
+
1143
+ if (compactOutputMode) {
1144
+ const target = RED.nodes.getNode(node.influxNode);
1145
+ if (target && typeof target.receive === "function") target.receive(RED.util.cloneMessage(influxMessage));
1146
+ else node.warn("Fronius Read: selecteaza un nod adt-influxdb pentru integrarea secundara.");
1147
+ } else {
1148
+ if (debugMessage) node.send([influxMessage, debugMessage]);
1149
+ else node.send(influxMessage);
1150
+ return;
1151
+ }
1152
+ }
1153
+
1154
+ if (multiGridOutputMode) {
1155
+ const meterId = getEffectiveInverterId();
1156
+ const multiGridPayload = {};
1157
+
1158
+ effectivePayloadFields.forEach(function (fieldPath) {
1159
+ multiGridPayload[fieldPathToCompactKey(fieldPath)] = getByPath(envelope, fieldPath);
1160
+ });
1161
+
1162
+ const multiGridMessage = {
1163
+ topic: meterId,
1164
+ meter_id: meterId,
1165
+ payload: multiGridPayload,
1166
+ payload_field: "multi",
1167
+ payload_fields: effectivePayloadFields.slice(),
1168
+ device_type: deviceType,
1169
+ meter_model: summary && summary.meter ? summary.meter.meter_model : null,
1170
+ manufacturer: detection.manufacturer,
1171
+ family: detection.family,
1172
+ phase_mode: detection.phase_mode,
1173
+ detection_source: detection.detection_source,
1174
+ optimized_read: readResult.mode === "optimized",
1175
+ summary_fragment: summary
1176
+ };
1177
+
1178
+ if (debugMessage) {
1179
+ node.send([multiGridMessage, debugMessage]);
1180
+ } else {
1181
+ node.send(multiGridMessage);
1182
+ }
1183
+ return;
1184
+ }
1185
+
1186
+ if (compactOutputMode) {
1187
+
1188
+ const inverterId = getEffectiveInverterId();
1189
+ const useSelectedFields = !!node.multiInverterUseSelectedFields;
1190
+ const compactPayload = {};
1191
+
1192
+ if (useSelectedFields) {
1193
+ effectivePayloadFields.forEach(function (fieldPath) {
1194
+ compactPayload[fieldPathToCompactKey(fieldPath)] = getByPath(envelope, fieldPath);
1195
+ });
1196
+ }
1197
+
1198
+ message = {
1199
+ topic: inverterId,
1200
+ payload: useSelectedFields
1201
+ ? compactPayload
1202
+ : {
1203
+ power_w: getByPath(envelope, defaultControlField)
1204
+ },
1205
+ payload_field: useSelectedFields
1206
+ ? (effectivePayloadFields[0] || defaultControlField)
1207
+ : defaultControlField,
1208
+ payload_fields: useSelectedFields
1209
+ ? effectivePayloadFields.slice()
1210
+ : [defaultControlField],
1211
+ inverter_id: inverterId,
1212
+ device_type: deviceType,
1213
+ inverter_type: node.inverterType,
1214
+ inverter_profile: detection.profile_key,
1215
+ inverter_profile_label: detection.profile_label,
1216
+ manufacturer: detection.manufacturer,
1217
+ family: detection.family,
1218
+ phase_mode: detection.phase_mode,
1219
+ mppt_count: detection.mppt_count,
1220
+ detection_source: detection.detection_source,
1221
+ optimized_read: readResult.mode === "optimized",
1222
+ summary_fragment: summary
1223
+ };
1224
+
1225
+ if (debugMessage) {
1226
+ node.send([message, debugMessage]);
1227
+ } else {
1228
+ node.send(message);
1229
+ }
1230
+
1231
+ } else if (effectivePayloadFields.includes("full")) {
1232
+
1233
+ message = {
1234
+ topic: "full",
1235
+ payload: selectedPayload,
1236
+ payload_field: "full",
1237
+ payload_fields: ["full"],
1238
+ device_type: deviceType,
1239
+ inverter_type: node.inverterType,
1240
+ inverter_profile: detection.profile_key,
1241
+ inverter_profile_label: detection.profile_label,
1242
+ manufacturer: detection.manufacturer,
1243
+ family: detection.family,
1244
+ phase_mode: detection.phase_mode,
1245
+ mppt_count: detection.mppt_count,
1246
+ detection_source: detection.detection_source,
1247
+ optimized_read: readResult.mode === "optimized",
1248
+ full_payload: envelope
1249
+ };
1250
+
1251
+ if (debugMessage) {
1252
+ node.send([message, debugMessage]);
1253
+ } else {
1254
+ node.send(message);
1255
+ }
1256
+
1257
+ } else if (effectivePayloadFields.length === 1) {
1258
+
1259
+ const singleField = effectivePayloadFields[0];
1260
+
1261
+ message = {
1262
+ topic: getPayloadLabel(singleField),
1263
+ payload: getByPath(envelope, singleField),
1264
+ payload_field: singleField,
1265
+ payload_fields: effectivePayloadFields,
1266
+ device_type: deviceType,
1267
+ inverter_type: node.inverterType,
1268
+ inverter_profile: detection.profile_key,
1269
+ inverter_profile_label: detection.profile_label,
1270
+ manufacturer: detection.manufacturer,
1271
+ family: detection.family,
1272
+ phase_mode: detection.phase_mode,
1273
+ mppt_count: detection.mppt_count,
1274
+ detection_source: detection.detection_source,
1275
+ optimized_read: readResult.mode === "optimized",
1276
+ summary_fragment: summary
1277
+ };
1278
+
1279
+ if (debugMessage) {
1280
+ node.send([message, debugMessage]);
1281
+ } else {
1282
+ node.send(message);
1283
+ }
1284
+
1285
+ } else {
1286
+
1287
+ const primaryOutputCount = Math.max(1, effectivePayloadFields.length);
1288
+ const outputCount = primaryOutputCount + (debugMessage ? 1 : 0);
1289
+ const outputMessages = new Array(outputCount).fill(null);
1290
+
1291
+ effectivePayloadFields.forEach(function (fieldPath, index) {
1292
+ outputMessages[index] = {
1293
+ topic: getPayloadLabel(fieldPath),
1294
+ payload: getByPath(envelope, fieldPath),
1295
+ payload_field: fieldPath,
1296
+ payload_fields: effectivePayloadFields,
1297
+ output_index: index,
1298
+ device_type: deviceType,
1299
+ inverter_type: node.inverterType,
1300
+ inverter_profile: detection.profile_key,
1301
+ inverter_profile_label: detection.profile_label,
1302
+ manufacturer: detection.manufacturer,
1303
+ family: detection.family,
1304
+ phase_mode: detection.phase_mode,
1305
+ mppt_count: detection.mppt_count,
1306
+ detection_source: detection.detection_source,
1307
+ optimized_read: readResult.mode === "optimized",
1308
+ summary_fragment: summary
1309
+ };
1310
+ });
1311
+
1312
+ if (debugMessage) {
1313
+ outputMessages[primaryOutputCount] = debugMessage;
1314
+ }
1315
+
1316
+ node.send(outputMessages);
1317
+
1318
+ }
1319
+
1320
+ } catch (e) {
1321
+
1322
+ node.status({
1323
+ fill: "red",
1324
+ shape: "ring",
1325
+ text: "error"
1326
+ });
1327
+
1328
+ setNodeRuntimeState(node.id, {
1329
+ status: "error",
1330
+ node_id: node.id,
1331
+ ip: node.host,
1332
+ unit_id: node.unitId,
1333
+ payload_field: node.multiInverter
1334
+ ? (
1335
+ node.multiInverterUseSelectedFields
1336
+ ? (normalizePayloadFields(node.payloadFields).filter(function (field) { return field !== "full"; })[0] || "summary.ac.power_w")
1337
+ : "summary.ac.power_w"
1338
+ )
1339
+ : (
1340
+ node.influxDb
1341
+ ? (normalizePayloadFields(node.payloadFields)[0] || "full")
1342
+ : node.payloadField
1343
+ ),
1344
+ payload_fields: node.multiInverter
1345
+ ? (
1346
+ node.multiInverterUseSelectedFields
1347
+ ? (function () {
1348
+ const selected = normalizePayloadFields(node.payloadFields).filter(function (field) {
1349
+ return field !== "full";
1350
+ });
1351
+ return selected.length ? selected : ["summary.ac.power_w"];
1352
+ })()
1353
+ : ["summary.ac.power_w"]
1354
+ )
1355
+ : (
1356
+ node.influxDb
1357
+ ? normalizePayloadFields(node.payloadFields)
1358
+ : node.payloadFields
1359
+ ),
1360
+ error: e.message || String(e)
1361
+ });
1362
+
1363
+ if (node.outputs > 1) {
1364
+ const errorOutputs = new Array(node.outputs).fill(null);
1365
+ errorOutputs[0] = {
1366
+ topic: "fronius/error",
1367
+ payload: {
1368
+ status: "error",
1369
+ ip: node.host,
1370
+ unit_id: node.unitId,
1371
+ inverter_type: node.inverterType,
1372
+ error: e.message || String(e),
1373
+ ts: new Date().toISOString()
1374
+ }
1375
+ };
1376
+ node.send(errorOutputs);
1377
+ } else {
1378
+ node.send({
1379
+ topic: "fronius/error",
1380
+ payload: {
1381
+ status: "error",
1382
+ ip: node.host,
1383
+ unit_id: node.unitId,
1384
+ inverter_type: node.inverterType,
1385
+ error: e.message || String(e),
1386
+ ts: new Date().toISOString()
1387
+ }
1388
+ });
1389
+ }
1390
+
1391
+ await resetConnection();
1392
+
1393
+ } finally {
1394
+
1395
+ busy = false;
1396
+
1397
+ }
1398
+
1399
+ }
1400
+
1401
+ async function pollLoop() {
1402
+
1403
+ if (closing) {
1404
+ return;
1405
+ }
1406
+
1407
+ await pollOnce();
1408
+ scheduleNextPoll();
1409
+
1410
+ }
1411
+
1412
+ if (node.influxDb) {
1413
+ registeredInfluxIdentityKey = getInfluxIdentityKey();
1414
+ registerInfluxMeasurement(node.id, registeredInfluxIdentityKey);
1415
+
1416
+ if (hasInfluxMeasurementConflict()) {
1417
+ node.status({
1418
+ fill: "red",
1419
+ shape: "ring",
1420
+ text: "inverter_id+measurement duplicat"
1421
+ });
1422
+ node.error("Fronius InfluxDB: combinatie duplicata Inverter ID + Measurement ('" + getEffectiveInverterId() + "' @ '" + getInfluxMeasurement() + "'). Configureaza un Inverter ID unic pentru acelasi measurement.");
1423
+ }
1424
+ }
1425
+
1426
+ if (node.pollInterval > 0) {
1427
+ pollLoop();
1428
+ }
1429
+
1430
+ node.on("input", function () {
1431
+ pollOnce();
1432
+ });
1433
+
1434
+ node.on("close", async function (done) {
1435
+
1436
+ closing = true;
1437
+
1438
+ try {
1439
+
1440
+ if (timer) {
1441
+ clearTimeout(timer);
1442
+ }
1443
+
1444
+ if (registeredInfluxIdentityKey) {
1445
+ unregisterInfluxMeasurement(node.id, registeredInfluxIdentityKey);
1446
+ registeredInfluxIdentityKey = "";
1447
+ }
1448
+
1449
+ clearNodeRuntimeState(node.id);
1450
+ await closeClient(client);
1451
+
1452
+ } catch (e) {
1453
+ // ignore
1454
+ }
1455
+
1456
+ done();
1457
+
1458
+ });
1459
+
1460
+
1461
+ });
1462
+ }
1463
+
1464
+ RED.nodes.registerType("fronius-read", FroniusReadNode);
1465
+
1466
+ };