@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.
- package/NOTICE +56 -0
- package/PROPRIETARY-LICENSE.txt +157 -0
- package/README.md +105 -0
- package/TERMS-AND-CONDITIONS.md +311 -0
- package/advantech-read/advantech-read.html +775 -0
- package/advantech-read/advantech-read.js +449 -0
- package/advantech-write/advantech-write.html +413 -0
- package/advantech-write/advantech-write.js +338 -0
- package/bess-control/bess-control.html +569 -0
- package/bess-control/bess-control.js +541 -0
- package/bess-control/lib/allocation.js +149 -0
- package/bess-control/lib/batteries.js +86 -0
- package/bess-control/lib/helpers.js +157 -0
- package/bess-control/lib/messages.js +131 -0
- package/bess-control/lib/runtime.js +156 -0
- package/carlo-gavazzi/carlo-gavazzi.html +4327 -0
- package/carlo-gavazzi/carlo-gavazzi.js +3081 -0
- package/control-export/control-export.html +3644 -0
- package/control-export/control-export.js +2192 -0
- package/control-reactive/control-reactive.html +3136 -0
- package/control-reactive/control-reactive.js +2124 -0
- package/fronius-emulator/README.md +258 -0
- package/fronius-emulator/fronius-emulator.html +170 -0
- package/fronius-emulator/fronius-emulator.js +520 -0
- package/fronius-emulator/lib/binary-codec.js +65 -0
- package/fronius-emulator/lib/fronius-register-map.js +185 -0
- package/fronius-emulator/lib/modbus-parser.js +61 -0
- package/fronius-emulator/lib/modbus-tcp-server.js +224 -0
- package/fronius-emulator/lib/validation.js +206 -0
- package/fronius-emulator/lib/virtual-meter.js +132 -0
- package/fronius-read/fronius-read.html +3944 -0
- package/fronius-read/fronius-read.js +1466 -0
- package/fronius-read/lib/inverter-profiles.js +711 -0
- package/fronius-read/lib/sunspec-core.js +303 -0
- package/fronius-read/lib/sunspec-model-parsers.js +845 -0
- package/fronius-write/fronius-write.html +1789 -0
- package/fronius-write/fronius-write.js +2531 -0
- package/influxdb/influxdb.html +1077 -0
- package/influxdb/influxdb.js +1487 -0
- package/janitza/janitza.html +2562 -0
- package/janitza/janitza.js +1500 -0
- package/janitza/lib/janitza-profiles.js +371 -0
- package/lib/a3-test-server.js +199 -0
- package/lib/adam-6000-profiles.js +173 -0
- package/lib/licensing/constants.js +103 -0
- package/lib/licensing/device-id.js +251 -0
- package/lib/licensing/errors.js +21 -0
- package/lib/licensing/license-client.js +377 -0
- package/lib/licensing/license-state.js +50 -0
- package/lib/licensing/node-guard.js +150 -0
- package/lib/licensing/package-integrity.js +167 -0
- package/lib/licensing/package-integrity.json +513 -0
- package/lib/licensing/public-key.pem +3 -0
- package/lib/licensing/public-key.pem.example +2 -0
- package/lib/licensing/public-keys.js +18 -0
- package/lib/licensing/storage.js +63 -0
- package/lib/licensing/token-verifier.js +84 -0
- package/lib/numbers.js +26 -0
- package/license/atelier-license.html +88 -0
- package/license/atelier-license.js +170 -0
- package/modbus-tcp-discover/modbus-tcp-discover.html +190 -0
- package/modbus-tcp-discover/modbus-tcp-discover.js +706 -0
- package/multi-battery/multi-battery.html +649 -0
- package/multi-battery/multi-battery.js +668 -0
- package/multi-grid/multi-grid.html +745 -0
- package/multi-grid/multi-grid.js +1392 -0
- package/multi-inverter/lib/engine-core.js +237 -0
- package/multi-inverter/multi-inverter.html +2674 -0
- package/multi-inverter/multi-inverter.js +1393 -0
- package/package.json +109 -0
- package/phoenix-contact/lib/eem-ma370-advanced.js +175 -0
- package/phoenix-contact/lib/eem-ma370.js +138 -0
- package/phoenix-contact/lib/integrations.js +149 -0
- package/phoenix-contact/lib/phoenix-contact-profile.js +35030 -0
- package/phoenix-contact/phoenix-contact.html +840 -0
- package/phoenix-contact/phoenix-contact.js +679 -0
- package/pv-generator/pv-generator.html +284 -0
- package/pv-generator/pv-generator.js +387 -0
- package/pylontech-read/pylontech-read.html +763 -0
- package/pylontech-read/pylontech-read.js +518 -0
- package/pylontech-write/pylontech-write.html +405 -0
- package/pylontech-write/pylontech-write.js +546 -0
- package/scada-aggregator/lib/aggregate.js +72 -0
- package/scada-aggregator/lib/groups.js +39 -0
- package/scada-aggregator/scada-aggregator.html +108 -0
- package/scada-aggregator/scada-aggregator.js +117 -0
- package/scada-server/lib/aggregator-map.js +58 -0
- package/scada-server/lib/encoding.js +235 -0
- package/scada-server/lib/modbus-server.js +492 -0
- package/scada-server/lib/object-path.js +18 -0
- package/scada-server/lib/register-map.js +288 -0
- package/scada-server/scada-server.html +1419 -0
- package/scada-server/scada-server.js +704 -0
- package/scripts/check-syntax.js +66 -0
- package/scripts/package-integrity.js +46 -0
- package/scripts/package-release.js +116 -0
- package/scripts/run-tests.js +2178 -0
- package/scripts/tgz-integrity.js +74 -0
- package/sma-read/lib/sunspec-core.js +83 -0
- package/sma-read/lib/sunspec-model-parsers.js +853 -0
- package/sma-read/sma-read.html +659 -0
- package/sma-read/sma-read.js +458 -0
- package/sma-write/sma-write.html +105 -0
- package/sma-write/sma-write.js +270 -0
- package/victron-read/lib/mqtt-state.js +176 -0
- package/victron-read/victron-read.html +1074 -0
- 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
|
+
};
|