@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,2531 @@
|
|
|
1
|
+
module.exports = function (RED) {
|
|
2
|
+
|
|
3
|
+
const { runLicensedNode } = require("../lib/licensing/node-guard");
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const ModbusRTU = require("modbus-serial");
|
|
7
|
+
const {
|
|
8
|
+
atLeast,
|
|
9
|
+
clampNumber
|
|
10
|
+
} = require("../lib/numbers");
|
|
11
|
+
|
|
12
|
+
const MAX_OPERATION_RETRIES = 3;
|
|
13
|
+
|
|
14
|
+
const RETRY_DELAY_MS = [250, 750];
|
|
15
|
+
|
|
16
|
+
const MAX_CONFIG_POWER_W = 125000;
|
|
17
|
+
|
|
18
|
+
const nodeRuntimeState = new Map();
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
function setNodeRuntimeState(nodeId, state) {
|
|
22
|
+
|
|
23
|
+
nodeRuntimeState.set(nodeId, Object.assign({}, state, {
|
|
24
|
+
|
|
25
|
+
updated_at: new Date().toISOString()
|
|
26
|
+
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
function clearNodeRuntimeState(nodeId) {
|
|
33
|
+
|
|
34
|
+
nodeRuntimeState.delete(nodeId);
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
RED.httpAdmin.get("/fronius-write/state/:id", function (req, res) {
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
const nodeId = req.params.id;
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if (!nodeId) {
|
|
46
|
+
|
|
47
|
+
res.json({ ok: false, error: "Missing node id" });
|
|
48
|
+
|
|
49
|
+
return;
|
|
50
|
+
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
const state = nodeRuntimeState.get(nodeId);
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if (!state) {
|
|
58
|
+
|
|
59
|
+
res.json({ ok: false, error: "No cached state" });
|
|
60
|
+
|
|
61
|
+
return;
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
res.json({
|
|
67
|
+
|
|
68
|
+
ok: true,
|
|
69
|
+
|
|
70
|
+
state: state
|
|
71
|
+
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
function normalizeRampType(type) {
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if (!type) return "none";
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if (type === "gradient_watts") return "gradient_w";
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if (type === "gradient_vars") return "gradient_w";
|
|
89
|
+
|
|
90
|
+
if (type === "gradient_var") return "gradient_w";
|
|
91
|
+
|
|
92
|
+
if (type === "gradient_vars_per_sec") return "gradient_w";
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
return type;
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
function normalizePowerType(type) {
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
return type === "reactive" ? "reactive" : "active";
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
function normalizeModeByPowerType(powerType, mode) {
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
const normalizedPowerType = normalizePowerType(powerType);
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if (normalizedPowerType === "reactive") {
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if (mode === "vars") return "vars";
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if (mode === "percent") return "percent";
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
return "vars";
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if (mode === "watts") return "watts";
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if (mode === "percent") return "percent";
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
return "watts";
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
function toUnsigned16FromSigned(value) {
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
let v = Math.round(Number(value) || 0);
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if (v > 32767) v = 32767;
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if (v < -32768) v = -32768;
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
return v < 0 ? (65536 + v) : v;
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
function clampSignedPercent(powerType, percent) {
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if (powerType === "reactive") {
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if (percent < -100) return -100;
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if (percent > 100) return 100;
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
return percent;
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
if (percent < -100) return -100;
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if (percent > 100) return 100;
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
return percent;
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
function assertRange(value, min, max, label) {
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if (!Number.isFinite(value)) {
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
throw new Error(label + " trebuie sa fie numeric");
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if (value < min || value > max) {
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
throw new Error(label + " trebuie sa fie intre " + min + " si " + max);
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
function isRetryableModbusError(err) {
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if (!err) {
|
|
230
|
+
|
|
231
|
+
return false;
|
|
232
|
+
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
const msg = String(err.message || err || "").toLowerCase();
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
if (!msg) {
|
|
240
|
+
|
|
241
|
+
return false;
|
|
242
|
+
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
return [
|
|
247
|
+
|
|
248
|
+
"timed out",
|
|
249
|
+
|
|
250
|
+
"timeout",
|
|
251
|
+
|
|
252
|
+
"econnreset",
|
|
253
|
+
|
|
254
|
+
"econnrefused",
|
|
255
|
+
|
|
256
|
+
"ehostunreach",
|
|
257
|
+
|
|
258
|
+
"enetunreach",
|
|
259
|
+
|
|
260
|
+
"etimedout",
|
|
261
|
+
|
|
262
|
+
"econnaborted",
|
|
263
|
+
|
|
264
|
+
"socket hang up",
|
|
265
|
+
|
|
266
|
+
"port not open",
|
|
267
|
+
|
|
268
|
+
"not connected",
|
|
269
|
+
|
|
270
|
+
"broken pipe",
|
|
271
|
+
|
|
272
|
+
"eof",
|
|
273
|
+
|
|
274
|
+
"modbus exception 4",
|
|
275
|
+
|
|
276
|
+
"slave device failure"
|
|
277
|
+
|
|
278
|
+
].some(function (token) {
|
|
279
|
+
|
|
280
|
+
return msg.includes(token);
|
|
281
|
+
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function optionalPositiveTimestampMs(payload, key) {
|
|
288
|
+
if (!payload || typeof payload !== "object") return null;
|
|
289
|
+
if (payload[key] === undefined || payload[key] === null || payload[key] === "") return null;
|
|
290
|
+
|
|
291
|
+
const timestamp = Number(payload[key]);
|
|
292
|
+
return Number.isFinite(timestamp) && timestamp > 0 ? timestamp : null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
function FroniusWriteNode(config) {
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
RED.nodes.createNode(this, config);
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
const node = this;
|
|
304
|
+
|
|
305
|
+
runLicensedNode(node, RED, config, function startLicensedRuntime() {
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
node.host = config.host;
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
node.port = atLeast(config.port, 1, 502);
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
node.unitId = atLeast(config.unitId, 1, 1);
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
node.powerType = normalizePowerType(config.powerType || "active");
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
node.mode = normalizeModeByPowerType(node.powerType, config.mode || "watts");
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
node.nominalPower = clampNumber(config.nominalPower, 1, MAX_CONFIG_POWER_W, 25000);
|
|
327
|
+
|
|
328
|
+
node.maxPower = clampNumber(
|
|
329
|
+
|
|
330
|
+
config.maxPower !== undefined
|
|
331
|
+
|
|
332
|
+
? config.maxPower
|
|
333
|
+
|
|
334
|
+
: (config.nominalPower || 25000)
|
|
335
|
+
|
|
336
|
+
,
|
|
337
|
+
1,
|
|
338
|
+
MAX_CONFIG_POWER_W,
|
|
339
|
+
25000
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
if (!Number.isFinite(node.maxPower) || node.maxPower <= 0) {
|
|
344
|
+
|
|
345
|
+
node.maxPower = clampNumber(config.nominalPower, 1, MAX_CONFIG_POWER_W, 25000);
|
|
346
|
+
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
if (!Number.isFinite(node.nominalPower) || node.nominalPower <= 0) {
|
|
351
|
+
|
|
352
|
+
node.nominalPower = 25000;
|
|
353
|
+
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
if (node.nominalPower < node.maxPower) {
|
|
358
|
+
|
|
359
|
+
node.nominalPower = node.maxPower;
|
|
360
|
+
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
node.nominalReactivePower = clampNumber(
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
config.nominalReactivePower !== undefined
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
? config.nominalReactivePower
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
: (config.nominalPower || 25000)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
,
|
|
377
|
+
1,
|
|
378
|
+
MAX_CONFIG_POWER_W,
|
|
379
|
+
25000
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
node.rampType = normalizeRampType(config.rampType || "none");
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
node.rampTime = atLeast(config.rampTime, 0, 0);
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
node.rampGradientPct = atLeast(config.rampGradientPct, 0, 10);
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
node.rampGradientWatts = atLeast(config.rampGradientWatts, 0, 2500);
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
node.rampGradientVars = atLeast(
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
config.rampGradientVars !== undefined
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
? config.rampGradientVars
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
: (config.rampGradientWatts || 2500)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
,
|
|
409
|
+
0,
|
|
410
|
+
2500
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
node.rampUpValue = atLeast(config.rampUpValue, 0, 0);
|
|
414
|
+
|
|
415
|
+
node.rampDownValue = atLeast(config.rampDownValue, 0, 0);
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
node.multiInverter = !!config.multiInverter;
|
|
419
|
+
|
|
420
|
+
node.inverterId = String(config.inverterId || "").trim();
|
|
421
|
+
node.latencyTargetMs = atLeast(config.latencyTargetMs, 1, 200);
|
|
422
|
+
node.maxCommandAgeMs = atLeast(config.maxCommandAgeMs, 50, 1000);
|
|
423
|
+
node.disableReadbackVerification = !!config.disableReadbackVerification;
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
let client = new ModbusRTU();
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
let sunspecBase = null;
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
let cachedControlModel = null;
|
|
434
|
+
|
|
435
|
+
const lastAppliedPercentByPowerType = {
|
|
436
|
+
active: null,
|
|
437
|
+
reactive: null
|
|
438
|
+
};
|
|
439
|
+
let confirmedControlState = null;
|
|
440
|
+
let pendingOperation = null;
|
|
441
|
+
let operationWorker = Promise.resolve();
|
|
442
|
+
let operationWorkerRunning = false;
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
function toSigned16(v) {
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
return v > 32767 ? v - 65536 : v;
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
function invScale(value, sf) {
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
return Math.round(value / Math.pow(10, sf));
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
function toNumber(v) {
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
const n = Number(v);
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
return Number.isFinite(n) ? n : null;
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
function wait(ms) {
|
|
480
|
+
|
|
481
|
+
return new Promise(function (resolve) {
|
|
482
|
+
|
|
483
|
+
setTimeout(resolve, ms);
|
|
484
|
+
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
async function recreateClient() {
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
|
|
496
|
+
if (client && client.isOpen) {
|
|
497
|
+
|
|
498
|
+
await client.close();
|
|
499
|
+
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
} catch (e) {
|
|
503
|
+
|
|
504
|
+
// ignore
|
|
505
|
+
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
client = new ModbusRTU();
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
async function resetRuntimeAfterCommFailure() {
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
await recreateClient();
|
|
520
|
+
|
|
521
|
+
sunspecBase = null;
|
|
522
|
+
|
|
523
|
+
cachedControlModel = null;
|
|
524
|
+
confirmedControlState = null;
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
async function executeWithRetry(label, operation) {
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
let lastError = null;
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
for (let attempt = 1; attempt <= MAX_OPERATION_RETRIES; attempt++) {
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
|
|
542
|
+
return await operation();
|
|
543
|
+
|
|
544
|
+
} catch (err) {
|
|
545
|
+
|
|
546
|
+
lastError = err;
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
const retryable = isRetryableModbusError(err);
|
|
550
|
+
|
|
551
|
+
const shouldRetry = retryable && attempt < MAX_OPERATION_RETRIES;
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
if (!shouldRetry) {
|
|
555
|
+
|
|
556
|
+
throw err;
|
|
557
|
+
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
node.warn(
|
|
562
|
+
|
|
563
|
+
label +
|
|
564
|
+
|
|
565
|
+
" failed (" +
|
|
566
|
+
|
|
567
|
+
attempt +
|
|
568
|
+
|
|
569
|
+
"/" +
|
|
570
|
+
|
|
571
|
+
MAX_OPERATION_RETRIES +
|
|
572
|
+
|
|
573
|
+
"): " +
|
|
574
|
+
|
|
575
|
+
err.message +
|
|
576
|
+
|
|
577
|
+
". Retrying..."
|
|
578
|
+
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
await resetRuntimeAfterCommFailure();
|
|
583
|
+
|
|
584
|
+
await wait(RETRY_DELAY_MS[attempt - 1] || RETRY_DELAY_MS[RETRY_DELAY_MS.length - 1] || 500);
|
|
585
|
+
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
throw lastError || new Error(label + " failed");
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
async function connect() {
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
if (client.isOpen) {
|
|
602
|
+
|
|
603
|
+
client.setID(node.unitId);
|
|
604
|
+
|
|
605
|
+
client.setTimeout(2500);
|
|
606
|
+
|
|
607
|
+
return;
|
|
608
|
+
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
await client.connectTCP(node.host, { port: node.port });
|
|
613
|
+
|
|
614
|
+
client.setID(node.unitId);
|
|
615
|
+
|
|
616
|
+
client.setTimeout(2500);
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
async function read(addr, len) {
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
return client.readHoldingRegisters(addr, len);
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
async function writeSingle(addr, value) {
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
return client.writeRegister(addr, value);
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function writeSingleLabeled(addr, value, label) {
|
|
642
|
+
try {
|
|
643
|
+
return await writeSingle(addr, value);
|
|
644
|
+
} catch (err) {
|
|
645
|
+
const detail = (label || "Registru") + " write failed at register " + addr + " value " + value;
|
|
646
|
+
err.message = detail + ": " + (err.message || String(err));
|
|
647
|
+
throw err;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function readSingle(addr, label) {
|
|
652
|
+
let response = null;
|
|
653
|
+
try {
|
|
654
|
+
response = await read(addr, 1);
|
|
655
|
+
} catch (err) {
|
|
656
|
+
const detail = (label || "Registru") + " read failed at register " + addr;
|
|
657
|
+
err.message = detail + ": " + (err.message || String(err));
|
|
658
|
+
throw err;
|
|
659
|
+
}
|
|
660
|
+
if (!response || !Array.isArray(response.data) || response.data.length !== 1) {
|
|
661
|
+
throw new Error("Raspuns Modbus incomplet la verificarea " + (label || "registrului") + " register " + addr);
|
|
662
|
+
}
|
|
663
|
+
return response.data[0];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async function verifyRegister(addr, expected, label) {
|
|
667
|
+
if (node.disableReadbackVerification) {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
const actual = await readSingle(addr, label);
|
|
671
|
+
if (actual !== expected) {
|
|
672
|
+
throw new Error(
|
|
673
|
+
(label || "Registru") + " register " + addr + " nu a fost confirmat: asteptat " + expected + ", citit " + actual
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
return actual;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function isSlaveDeviceFailure(err) {
|
|
680
|
+
const msg = String(err && err.message ? err.message : err || "").toLowerCase();
|
|
681
|
+
return msg.includes("modbus exception 4") || msg.includes("slave device failure");
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async function verifyOppositeEnableRegister(addr, expected) {
|
|
685
|
+
try {
|
|
686
|
+
return await verifyRegister(addr, expected, "Enable control opus");
|
|
687
|
+
} catch (err) {
|
|
688
|
+
if (isSlaveDeviceFailure(err)) {
|
|
689
|
+
node.warn(
|
|
690
|
+
"Enable control opus read-back indisponibil la registrul " +
|
|
691
|
+
addr +
|
|
692
|
+
": " +
|
|
693
|
+
(err.message || String(err)) +
|
|
694
|
+
". Comanda continua deoarece scrierea de dezactivare a fost acceptata."
|
|
695
|
+
);
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
throw err;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
async function findBase() {
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
const bases = [39999, 40000, 40001, 40002, 40199, 40200, 40201, 40202];
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
for (const b of bases) {
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
const r = await read(b, 2);
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
const txt =
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
String.fromCharCode((r.data[0] >> 8) & 0xff) +
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
String.fromCharCode(r.data[0] & 0xff) +
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
String.fromCharCode((r.data[1] >> 8) & 0xff) +
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
String.fromCharCode(r.data[1] & 0xff);
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
if (txt.includes("SunS")) {
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
return b;
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
} catch (e) {
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
// ignore
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
throw new Error("SunSpec not found");
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
async function scanModels(base) {
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
const models = [];
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
let addr = base + 2;
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
while (true) {
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
const header = await read(addr, 2);
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
const modelId = header.data[0];
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
const len = header.data[1];
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
if (modelId === 0xffff) {
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
break;
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
const payload = await read(addr + 2, len);
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
models.push({
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
modelId,
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
addr,
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
len,
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
baseAddr: addr + 2,
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
raw: payload.data
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
addr += 2 + len;
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
return models;
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
async function findControlModel(base) {
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
const models = await scanModels(base);
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
const preferred = [123, 122, 121, 120];
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
for (const wantedId of preferred) {
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
const found = models.find((m) => m.modelId === wantedId);
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
if (found) {
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
return found;
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
const foundAnyControl = models.find((m) => [120, 121, 122, 123].includes(m.modelId));
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
if (foundAnyControl) {
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
return foundAnyControl;
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
throw new Error("Nu a fost găsit niciun model SunSpec de control (120/121/122/123)");
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
function extractRampFromConfig(powerType) {
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
if (node.rampType === "none") {
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
return { type: "none", value: 0 };
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
if (node.rampType === "time") {
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
return { type: "time", value: 0 };
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
if (node.rampType === "gradient_pct") {
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
return { type: "gradient_pct", value: 0 };
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
if (node.rampType === "gradient_w") {
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
return {
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
type: "gradient_w",
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
value: 0
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
return { type: "none", value: 0 };
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
function extractInput(msg) {
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
let powerType = node.powerType;
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
let mode = node.mode;
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
let value;
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
let ramp = extractRampFromConfig(powerType);
|
|
960
|
+
let rampFromMessage = false;
|
|
961
|
+
let rampDirectionHint = null;
|
|
962
|
+
let nominalPowerOverride = null;
|
|
963
|
+
let nominalReactivePowerOverride = null;
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
if (typeof msg.payload === "number") {
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
value = Number(msg.payload);
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
} else if (typeof msg.payload === "string" && msg.payload.trim() !== "") {
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
const parsed = Number(msg.payload.trim());
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
if (Number.isFinite(parsed)) {
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
value = parsed;
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
} else if (typeof msg.payload === "object" && msg.payload !== null) {
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
const p = msg.payload;
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
if (p.powerType) {
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
powerType = normalizePowerType(p.powerType);
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
} else if (p.power_type) {
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
powerType = normalizePowerType(p.power_type);
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
if (p.mode) {
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
mode = p.mode;
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
if (Number.isFinite(Number(p.nominal_power_w))) {
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
nominalPowerOverride = Number(p.nominal_power_w);
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
if (Number.isFinite(Number(p.nominal_reactive_power_var))) {
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
nominalReactivePowerOverride = Number(p.nominal_reactive_power_var);
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
if (p.value !== undefined) {
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
value = Number(p.value);
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
} else if (p.site_limit_w !== undefined) {
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
value = Number(p.site_limit_w);
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
powerType = "active";
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
mode = "watts";
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
} else if (p.limit_w !== undefined) {
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
value = Number(p.limit_w);
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
powerType = "active";
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
mode = "watts";
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
} else if (p.watts !== undefined) {
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
value = Number(p.watts);
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
powerType = "active";
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
mode = "watts";
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
} else if (p.limit_pct !== undefined) {
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
value = Number(p.limit_pct);
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
powerType = "active";
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
mode = "percent";
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
} else if (p.percent !== undefined) {
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
value = Number(p.percent);
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
mode = "percent";
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
} else if (p.limit_var !== undefined) {
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
value = Number(p.limit_var);
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
powerType = "reactive";
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
mode = "vars";
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
} else if (p.vars !== undefined) {
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
value = Number(p.vars);
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
powerType = "reactive";
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
mode = "vars";
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
} else if (p.var !== undefined) {
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
value = Number(p.var);
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
powerType = "reactive";
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
mode = "vars";
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
} else if (p.limit_var_pct !== undefined) {
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
value = Number(p.limit_var_pct);
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
powerType = "reactive";
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
mode = "percent";
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
if (p.ramp && typeof p.ramp === "object") {
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
const msgRampType = normalizeRampType(p.ramp.type || "none");
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
const msgRampValue = toNumber(p.ramp.value);
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
ramp = {
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
type: msgRampType,
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
value: msgRampType === "none" ? 0 : (msgRampValue ?? 0)
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
rampFromMessage = true;
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (p.ramp_direction === "up" || p.ramp_direction === "down" || p.ramp_direction === "hold") {
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
rampDirectionHint = p.ramp_direction;
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
powerType = normalizePowerType(powerType);
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
mode = normalizeModeByPowerType(powerType, mode);
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
if (!Number.isFinite(value)) {
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
throw new Error("msg.payload trebuie să conțină valoarea limitei");
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
if (powerType === "active" && mode !== "watts" && mode !== "percent") {
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
throw new Error("Pentru putere activă, mode trebuie să fie 'watts' sau 'percent'");
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
if (powerType === "reactive" && mode !== "vars" && mode !== "percent") {
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
throw new Error("Pentru putere reactivă, mode trebuie să fie 'vars' sau 'percent'");
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
if (mode === "percent") {
|
|
1231
|
+
|
|
1232
|
+
|
|
1233
|
+
assertRange(value, -100, 100, "Valoarea procentuală");
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
} else if (powerType === "active" && mode === "watts") {
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
if (!Number.isFinite(node.maxPower) || node.maxPower <= 0) {
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
throw new Error("Max Power (W) trebuie sa fie > 0");
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
assertRange(value, 0, node.maxPower, "Valoarea in W fata de Max Power");
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
} else if (powerType === "reactive" && mode === "vars") {
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
const nominalReactivePower = effectiveNominalReactivePower({
|
|
1255
|
+
nominalReactivePowerOverride: nominalReactivePowerOverride
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
if (!Number.isFinite(nominalReactivePower) || nominalReactivePower <= 0) {
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
throw new Error("Nominal Reactive Power (VAr) trebuie să fie > 0");
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
assertRange(value, -nominalReactivePower, nominalReactivePower, "Valoarea în VAr");
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
if (mode === "watts" || mode === "vars") {
|
|
1275
|
+
|
|
1276
|
+
value = Math.round(value);
|
|
1277
|
+
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
return {
|
|
1283
|
+
powerType,
|
|
1284
|
+
mode,
|
|
1285
|
+
value,
|
|
1286
|
+
ramp,
|
|
1287
|
+
rampFromMessage,
|
|
1288
|
+
rampDirectionHint,
|
|
1289
|
+
nominalPowerOverride,
|
|
1290
|
+
nominalReactivePowerOverride
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
function effectiveNominalPower(input) {
|
|
1298
|
+
|
|
1299
|
+
|
|
1300
|
+
const n = Number(input && input.nominalPowerOverride);
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
return Number.isFinite(n) && n > 0 ? n : node.nominalPower;
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function effectiveNominalReactivePower(input) {
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
const n = Number(input && input.nominalReactivePowerOverride);
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
return Number.isFinite(n) && n > 0 ? n : node.nominalReactivePower;
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
function computePercent(powerType, mode, value, input) {
|
|
1322
|
+
|
|
1323
|
+
|
|
1324
|
+
if (powerType === "active") {
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
if (mode === "watts") {
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
const nominalPower = effectiveNominalPower(input);
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
if (!Number.isFinite(nominalPower) || nominalPower <= 0) {
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
|
+
throw new Error("Nominal Power (W) trebuie sa fie > 0");
|
|
1337
|
+
|
|
1338
|
+
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
if (Number.isFinite(node.maxPower) && node.maxPower > 0 && nominalPower < node.maxPower) {
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
throw new Error("Nominal Power (W) trebuie sa fie >= Max Power (W)");
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
return (value / nominalPower) * 100;
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
assertRange(value, -100, 100, "Valoarea procentuală");
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
return value;
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
|
|
1368
|
+
if (mode === "vars") {
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
const nominalReactivePower = effectiveNominalReactivePower(input);
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
if (!Number.isFinite(nominalReactivePower) || nominalReactivePower <= 0) {
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
throw new Error("Nominal Reactive Power (VAr) trebuie să fie > 0");
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
assertRange(value, -nominalReactivePower, nominalReactivePower, "Valoarea în VAr");
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
return (value / nominalReactivePower) * 100;
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
|
|
1393
|
+
|
|
1394
|
+
assertRange(value, -100, 100, "Valoarea procentuală");
|
|
1395
|
+
|
|
1396
|
+
|
|
1397
|
+
return value;
|
|
1398
|
+
|
|
1399
|
+
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
function computeRampTimeSeconds(powerType, percent, ramp, input) {
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
const rampType = normalizeRampType(ramp?.type || "none");
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
const rampValue = toNumber(ramp?.value);
|
|
1411
|
+
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
if (rampType === "none") {
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
return null;
|
|
1418
|
+
|
|
1419
|
+
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
const safePercent = Math.abs(clampSignedPercent(powerType, percent));
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
if (rampType === "time") {
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
if (!Number.isFinite(rampValue) || rampValue < 0) {
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
throw new Error("Ramp Time (s) invalid");
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
return Math.round(rampValue);
|
|
1441
|
+
|
|
1442
|
+
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
if (rampType === "gradient_pct") {
|
|
1448
|
+
|
|
1449
|
+
|
|
1450
|
+
if (!Number.isFinite(rampValue) || rampValue <= 0) {
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
throw new Error("Gradient (%/s) trebuie să fie > 0");
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
const t = safePercent / rampValue;
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
return Math.round(t);
|
|
1464
|
+
|
|
1465
|
+
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
if (rampType === "gradient_w") {
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
if (!Number.isFinite(rampValue) || rampValue <= 0) {
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
throw new Error(powerType === "reactive"
|
|
1477
|
+
|
|
1478
|
+
|
|
1479
|
+
? "Gradient (VAr/s) trebuie să fie > 0"
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
: "Gradient (W/s) trebuie să fie > 0");
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
if (powerType === "reactive") {
|
|
1490
|
+
|
|
1491
|
+
|
|
1492
|
+
const nominalReactivePower = effectiveNominalReactivePower(input);
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
if (!Number.isFinite(nominalReactivePower) || nominalReactivePower <= 0) {
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
throw new Error("Nominal Reactive Power (VAr) trebuie să fie > 0");
|
|
1499
|
+
|
|
1500
|
+
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
|
|
1505
|
+
const targetVars = (safePercent / 100) * nominalReactivePower;
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
const t = targetVars / rampValue;
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
return Math.round(t);
|
|
1512
|
+
|
|
1513
|
+
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
|
|
1517
|
+
|
|
1518
|
+
const nominalPower = effectiveNominalPower(input);
|
|
1519
|
+
|
|
1520
|
+
|
|
1521
|
+
if (!Number.isFinite(nominalPower) || nominalPower <= 0) {
|
|
1522
|
+
|
|
1523
|
+
|
|
1524
|
+
throw new Error("Nominal Power (W) trebuie sa fie > 0");
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
if (Number.isFinite(node.maxPower) && node.maxPower > 0 && nominalPower < node.maxPower) {
|
|
1531
|
+
|
|
1532
|
+
|
|
1533
|
+
throw new Error("Nominal Power (W) trebuie sa fie >= Max Power (W)");
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
const targetWatts = (safePercent / 100) * nominalPower;
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
const t = targetWatts / rampValue;
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
return Math.round(t);
|
|
1546
|
+
|
|
1547
|
+
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
|
|
1552
|
+
throw new Error(`Ramp type invalid: ${rampType}`);
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
|
|
1558
|
+
function selectDirectionalRamp(input, percent) {
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
const powerType = normalizePowerType(input.powerType);
|
|
1562
|
+
|
|
1563
|
+
|
|
1564
|
+
const previousPercent = lastAppliedPercentByPowerType[powerType];
|
|
1565
|
+
|
|
1566
|
+
|
|
1567
|
+
const measuredDirection = Number.isFinite(previousPercent)
|
|
1568
|
+
|
|
1569
|
+
|
|
1570
|
+
? (percent > previousPercent ? "up" : (percent < previousPercent ? "down" : "hold"))
|
|
1571
|
+
|
|
1572
|
+
|
|
1573
|
+
: "hold";
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
const direction = input.rampDirectionHint || measuredDirection;
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
const baseRamp = input.ramp || { type: "none", value: 0 };
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
if (input.rampFromMessage || !baseRamp || baseRamp.type === "none") {
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
return { ramp: baseRamp, direction: direction };
|
|
1586
|
+
|
|
1587
|
+
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
|
|
1591
|
+
if (direction === "up" && node.rampUpValue > 0) {
|
|
1592
|
+
|
|
1593
|
+
|
|
1594
|
+
return { ramp: { type: baseRamp.type, value: node.rampUpValue }, direction: direction };
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
|
|
1600
|
+
if (direction === "down" && node.rampDownValue > 0) {
|
|
1601
|
+
|
|
1602
|
+
|
|
1603
|
+
return { ramp: { type: baseRamp.type, value: node.rampDownValue }, direction: direction };
|
|
1604
|
+
|
|
1605
|
+
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
|
|
1609
|
+
return { ramp: baseRamp, direction: direction };
|
|
1610
|
+
|
|
1611
|
+
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
|
|
1615
|
+
|
|
1616
|
+
function buildStatusText(powerType, inputMode, inputValue, appliedPercent, rampTimeSeconds, ramp) {
|
|
1617
|
+
|
|
1618
|
+
|
|
1619
|
+
const rampType = normalizeRampType(ramp?.type || "none");
|
|
1620
|
+
|
|
1621
|
+
|
|
1622
|
+
|
|
1623
|
+
let baseText = "";
|
|
1624
|
+
|
|
1625
|
+
|
|
1626
|
+
if (powerType === "reactive") {
|
|
1627
|
+
|
|
1628
|
+
|
|
1629
|
+
if (inputMode === "vars") {
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
baseText = `Q ${inputValue} VAr -> ${appliedPercent.toFixed(1)}%`;
|
|
1633
|
+
|
|
1634
|
+
|
|
1635
|
+
} else {
|
|
1636
|
+
|
|
1637
|
+
|
|
1638
|
+
baseText = `Q ${appliedPercent.toFixed(1)}%`;
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
} else {
|
|
1645
|
+
|
|
1646
|
+
|
|
1647
|
+
if (inputMode === "watts") {
|
|
1648
|
+
|
|
1649
|
+
|
|
1650
|
+
baseText = `P ${inputValue} W -> ${appliedPercent.toFixed(1)}%`;
|
|
1651
|
+
|
|
1652
|
+
|
|
1653
|
+
} else {
|
|
1654
|
+
|
|
1655
|
+
|
|
1656
|
+
baseText = `P ${appliedPercent.toFixed(1)}%`;
|
|
1657
|
+
|
|
1658
|
+
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
|
|
1665
|
+
|
|
1666
|
+
if (rampTimeSeconds === null || rampType === "none") {
|
|
1667
|
+
|
|
1668
|
+
|
|
1669
|
+
return baseText;
|
|
1670
|
+
|
|
1671
|
+
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
|
|
1676
|
+
if (rampType === "time") {
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
return `${baseText} | ramp ${ramp.value}s`;
|
|
1680
|
+
|
|
1681
|
+
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
|
|
1685
|
+
|
|
1686
|
+
if (rampType === "gradient_pct") {
|
|
1687
|
+
|
|
1688
|
+
|
|
1689
|
+
return `${baseText} | ramp ${ramp.value}%/s`;
|
|
1690
|
+
|
|
1691
|
+
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
|
|
1695
|
+
|
|
1696
|
+
if (rampType === "gradient_w") {
|
|
1697
|
+
|
|
1698
|
+
|
|
1699
|
+
return powerType === "reactive"
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
? `${baseText} | ramp ${ramp.value}VAr/s`
|
|
1703
|
+
|
|
1704
|
+
|
|
1705
|
+
: `${baseText} | ramp ${ramp.value}W/s`;
|
|
1706
|
+
|
|
1707
|
+
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
|
|
1711
|
+
|
|
1712
|
+
return baseText;
|
|
1713
|
+
|
|
1714
|
+
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
|
|
1718
|
+
function normalizeMultiItem(item) {
|
|
1719
|
+
|
|
1720
|
+
if (!item || typeof item !== "object") {
|
|
1721
|
+
|
|
1722
|
+
return null;
|
|
1723
|
+
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
|
|
1727
|
+
const id = String(item.id || item.inverter_id || item.topic || "").trim();
|
|
1728
|
+
|
|
1729
|
+
if (!id) {
|
|
1730
|
+
|
|
1731
|
+
return null;
|
|
1732
|
+
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
|
|
1736
|
+
if (item.payload && typeof item.payload === "object") {
|
|
1737
|
+
|
|
1738
|
+
const payload = Object.assign({}, item.payload);
|
|
1739
|
+
|
|
1740
|
+
if (!Number.isFinite(Number(payload.limit_w)) && Number.isFinite(Number(item.limit_w))) {
|
|
1741
|
+
|
|
1742
|
+
payload.limit_w = Number(item.limit_w);
|
|
1743
|
+
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
if (!Number.isFinite(Number(payload.limit_var)) && Number.isFinite(Number(item.limit_var))) {
|
|
1747
|
+
|
|
1748
|
+
payload.limit_var = Number(item.limit_var);
|
|
1749
|
+
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
if (!Number.isFinite(Number(payload.limit_var_pct)) && Number.isFinite(Number(item.limit_var_pct))) {
|
|
1753
|
+
|
|
1754
|
+
payload.limit_var_pct = Number(item.limit_var_pct);
|
|
1755
|
+
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if (!payload.ramp && item.ramp && typeof item.ramp === "object") {
|
|
1759
|
+
|
|
1760
|
+
payload.ramp = item.ramp;
|
|
1761
|
+
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
if (!Number.isFinite(Number(payload.nominal_power_w)) && Number.isFinite(Number(item.nominal_power_w))) {
|
|
1765
|
+
|
|
1766
|
+
payload.nominal_power_w = Number(item.nominal_power_w);
|
|
1767
|
+
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
if (!Number.isFinite(Number(payload.nominal_reactive_power_var)) && Number.isFinite(Number(item.nominal_reactive_power_var))) {
|
|
1771
|
+
|
|
1772
|
+
payload.nominal_reactive_power_var = Number(item.nominal_reactive_power_var);
|
|
1773
|
+
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
return { id: id, payload: payload };
|
|
1777
|
+
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
const payload = {};
|
|
1782
|
+
|
|
1783
|
+
if (Number.isFinite(Number(item.limit_w))) {
|
|
1784
|
+
|
|
1785
|
+
payload.limit_w = Number(item.limit_w);
|
|
1786
|
+
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
if (Number.isFinite(Number(item.limit_var))) {
|
|
1790
|
+
|
|
1791
|
+
payload.limit_var = Number(item.limit_var);
|
|
1792
|
+
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
if (Number.isFinite(Number(item.limit_var_pct))) {
|
|
1796
|
+
|
|
1797
|
+
payload.limit_var_pct = Number(item.limit_var_pct);
|
|
1798
|
+
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
if (item.ramp && typeof item.ramp === "object") {
|
|
1802
|
+
|
|
1803
|
+
payload.ramp = item.ramp;
|
|
1804
|
+
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
if (Number.isFinite(Number(item.nominal_power_w))) {
|
|
1808
|
+
|
|
1809
|
+
payload.nominal_power_w = Number(item.nominal_power_w);
|
|
1810
|
+
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
if (Number.isFinite(Number(item.nominal_reactive_power_var))) {
|
|
1814
|
+
|
|
1815
|
+
payload.nominal_reactive_power_var = Number(item.nominal_reactive_power_var);
|
|
1816
|
+
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
[
|
|
1820
|
+
"command_id",
|
|
1821
|
+
"command_requested_at_ms",
|
|
1822
|
+
"control_received_at_ms",
|
|
1823
|
+
"control_calculated_at_ms",
|
|
1824
|
+
"allocated_at_ms"
|
|
1825
|
+
].forEach(function (key) {
|
|
1826
|
+
if (item[key] !== undefined) payload[key] = item[key];
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
return { id: id, payload: payload };
|
|
1830
|
+
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
|
|
1834
|
+
function rewriteMessageForMultiInverter(msg) {
|
|
1835
|
+
|
|
1836
|
+
if (!node.multiInverter) {
|
|
1837
|
+
|
|
1838
|
+
return msg;
|
|
1839
|
+
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
|
|
1843
|
+
if (!node.inverterId) {
|
|
1844
|
+
|
|
1845
|
+
throw new Error("Inverter ID este obligatoriu cand Multi-inverter este activ");
|
|
1846
|
+
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
|
|
1850
|
+
const payload = msg && msg.payload;
|
|
1851
|
+
|
|
1852
|
+
let items = null;
|
|
1853
|
+
|
|
1854
|
+
|
|
1855
|
+
if (Array.isArray(payload)) {
|
|
1856
|
+
|
|
1857
|
+
items = payload;
|
|
1858
|
+
|
|
1859
|
+
} else if (payload && typeof payload === "object") {
|
|
1860
|
+
|
|
1861
|
+
if (Array.isArray(payload.limits)) {
|
|
1862
|
+
|
|
1863
|
+
items = payload.limits;
|
|
1864
|
+
|
|
1865
|
+
} else if (Array.isArray(payload.inverters)) {
|
|
1866
|
+
|
|
1867
|
+
items = payload.inverters;
|
|
1868
|
+
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
|
|
1874
|
+
if (!Array.isArray(items)) {
|
|
1875
|
+
|
|
1876
|
+
throw new Error("Payload multi-inverter invalid: lipseste lista de limite");
|
|
1877
|
+
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
|
|
1881
|
+
const normalizedItems = items
|
|
1882
|
+
|
|
1883
|
+
.map(normalizeMultiItem)
|
|
1884
|
+
|
|
1885
|
+
.filter(Boolean);
|
|
1886
|
+
|
|
1887
|
+
|
|
1888
|
+
const target = normalizedItems.find(function (entry) {
|
|
1889
|
+
|
|
1890
|
+
return entry.id === node.inverterId;
|
|
1891
|
+
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
|
|
1895
|
+
if (!target) {
|
|
1896
|
+
|
|
1897
|
+
throw new Error("Inverter ID '" + node.inverterId + "' nu exista in payload-ul multi-inverter");
|
|
1898
|
+
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
|
|
1902
|
+
return Object.assign({}, msg, {
|
|
1903
|
+
|
|
1904
|
+
topic: node.inverterId,
|
|
1905
|
+
|
|
1906
|
+
payload: target.payload
|
|
1907
|
+
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
|
|
1913
|
+
|
|
1914
|
+
function getControlOffsets() {
|
|
1915
|
+
|
|
1916
|
+
|
|
1917
|
+
return {
|
|
1918
|
+
|
|
1919
|
+
|
|
1920
|
+
active: {
|
|
1921
|
+
|
|
1922
|
+
|
|
1923
|
+
pctOffset: 3,
|
|
1924
|
+
|
|
1925
|
+
|
|
1926
|
+
winTmsOffset: 4,
|
|
1927
|
+
|
|
1928
|
+
|
|
1929
|
+
rvrtTmsOffset: 5,
|
|
1930
|
+
|
|
1931
|
+
|
|
1932
|
+
rmpTmsOffset: 6,
|
|
1933
|
+
|
|
1934
|
+
|
|
1935
|
+
enaOffset: 7,
|
|
1936
|
+
|
|
1937
|
+
|
|
1938
|
+
sfIndex: 21
|
|
1939
|
+
|
|
1940
|
+
|
|
1941
|
+
},
|
|
1942
|
+
|
|
1943
|
+
|
|
1944
|
+
reactive: {
|
|
1945
|
+
|
|
1946
|
+
|
|
1947
|
+
pctOffset: 14,
|
|
1948
|
+
|
|
1949
|
+
|
|
1950
|
+
winTmsOffset: 16,
|
|
1951
|
+
|
|
1952
|
+
|
|
1953
|
+
rvrtTmsOffset: 17,
|
|
1954
|
+
|
|
1955
|
+
|
|
1956
|
+
rmpTmsOffset: 18,
|
|
1957
|
+
|
|
1958
|
+
|
|
1959
|
+
enaOffset: 20,
|
|
1960
|
+
|
|
1961
|
+
|
|
1962
|
+
sfIndex: 23
|
|
1963
|
+
|
|
1964
|
+
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
|
|
1968
|
+
};
|
|
1969
|
+
|
|
1970
|
+
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
|
|
1974
|
+
|
|
1975
|
+
async function handleInput(msg) {
|
|
1976
|
+
|
|
1977
|
+
|
|
1978
|
+
try {
|
|
1979
|
+
const effectiveMsg = rewriteMessageForMultiInverter(msg);
|
|
1980
|
+
const commandRequestedAtMs = optionalPositiveTimestampMs(effectiveMsg.payload, "command_requested_at_ms");
|
|
1981
|
+
if (commandRequestedAtMs !== null && (Date.now() - commandRequestedAtMs) > node.maxCommandAgeMs) {
|
|
1982
|
+
throw new Error("Comanda Fronius a expirat in coada");
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
const writeStartedAtMs = Date.now();
|
|
1986
|
+
const result = await executeWithRetry("fronius-write operation", async function () {
|
|
1987
|
+
|
|
1988
|
+
|
|
1989
|
+
await connect();
|
|
1990
|
+
|
|
1991
|
+
|
|
1992
|
+
if (!sunspecBase) {
|
|
1993
|
+
|
|
1994
|
+
sunspecBase = await findBase();
|
|
1995
|
+
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
|
|
1999
|
+
if (!cachedControlModel) {
|
|
2000
|
+
|
|
2001
|
+
cachedControlModel = await findControlModel(sunspecBase);
|
|
2002
|
+
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
|
|
2006
|
+
const controlModel = cachedControlModel;
|
|
2007
|
+
|
|
2008
|
+
const controlOffsets = getControlOffsets();
|
|
2009
|
+
|
|
2010
|
+
const input = extractInput(effectiveMsg);
|
|
2011
|
+
|
|
2012
|
+
const selectedOffsets = input.powerType === "reactive"
|
|
2013
|
+
|
|
2014
|
+
? controlOffsets.reactive
|
|
2015
|
+
|
|
2016
|
+
: controlOffsets.active;
|
|
2017
|
+
|
|
2018
|
+
const otherOffsets = input.powerType === "reactive"
|
|
2019
|
+
|
|
2020
|
+
? controlOffsets.active
|
|
2021
|
+
|
|
2022
|
+
: controlOffsets.reactive;
|
|
2023
|
+
|
|
2024
|
+
|
|
2025
|
+
let percent = computePercent(input.powerType, input.mode, input.value, input);
|
|
2026
|
+
|
|
2027
|
+
percent = clampSignedPercent(input.powerType, percent);
|
|
2028
|
+
|
|
2029
|
+
const directionalRamp = selectDirectionalRamp(input, percent);
|
|
2030
|
+
|
|
2031
|
+
input.ramp = directionalRamp.ramp;
|
|
2032
|
+
|
|
2033
|
+
|
|
2034
|
+
const rampTimeSeconds = computeRampTimeSeconds(input.powerType, percent, input.ramp, input);
|
|
2035
|
+
|
|
2036
|
+
const sfRaw = controlModel.raw[selectedOffsets.sfIndex];
|
|
2037
|
+
|
|
2038
|
+
|
|
2039
|
+
if (sfRaw === undefined) {
|
|
2040
|
+
|
|
2041
|
+
throw new Error("Scale factor index inexistent pentru modelul de control " + controlModel.modelId);
|
|
2042
|
+
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
|
|
2046
|
+
const sf = toSigned16(sfRaw);
|
|
2047
|
+
|
|
2048
|
+
let scaledValue = invScale(percent, sf);
|
|
2049
|
+
|
|
2050
|
+
|
|
2051
|
+
if (input.powerType === "reactive") {
|
|
2052
|
+
|
|
2053
|
+
if (scaledValue > 32767) scaledValue = 32767;
|
|
2054
|
+
|
|
2055
|
+
if (scaledValue < -32768) scaledValue = -32768;
|
|
2056
|
+
|
|
2057
|
+
} else {
|
|
2058
|
+
|
|
2059
|
+
if (scaledValue < 0) scaledValue = 0;
|
|
2060
|
+
|
|
2061
|
+
if (scaledValue > 32767) scaledValue = 32767;
|
|
2062
|
+
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
|
|
2066
|
+
const rawRegisterValue = input.powerType === "reactive"
|
|
2067
|
+
|
|
2068
|
+
? toUnsigned16FromSigned(scaledValue)
|
|
2069
|
+
|
|
2070
|
+
: scaledValue;
|
|
2071
|
+
|
|
2072
|
+
|
|
2073
|
+
const rampRegisterValue = rampTimeSeconds !== null ? rampTimeSeconds : 0;
|
|
2074
|
+
assertRange(rampRegisterValue, 0, 65535, "Ramp Time (s)");
|
|
2075
|
+
|
|
2076
|
+
const selectedEnableAddr = controlModel.baseAddr + selectedOffsets.enaOffset;
|
|
2077
|
+
const otherEnableAddr = controlModel.baseAddr + otherOffsets.enaOffset;
|
|
2078
|
+
const rampAddr = controlModel.baseAddr + selectedOffsets.rmpTmsOffset;
|
|
2079
|
+
const windowAddr = controlModel.baseAddr + selectedOffsets.winTmsOffset;
|
|
2080
|
+
const revertAddr = controlModel.baseAddr + selectedOffsets.rvrtTmsOffset;
|
|
2081
|
+
const setpointAddr = controlModel.baseAddr + selectedOffsets.pctOffset;
|
|
2082
|
+
let setpointReadback = null;
|
|
2083
|
+
let selectedEnableReadback = null;
|
|
2084
|
+
let otherEnableReadback = null;
|
|
2085
|
+
let appliedSetpointReadback = null;
|
|
2086
|
+
async function runFullWriteSequence() {
|
|
2087
|
+
// Keep the opposite control loop disabled; do not drop the selected loop to 0
|
|
2088
|
+
// because some Fronius firmwares only latch the limit after an enabled write.
|
|
2089
|
+
await writeSingleLabeled(otherEnableAddr, 0, "Enable control opus");
|
|
2090
|
+
otherEnableReadback = await verifyOppositeEnableRegister(otherEnableAddr, 0);
|
|
2091
|
+
|
|
2092
|
+
await writeSingleLabeled(rampAddr, rampRegisterValue, "Ramp Time");
|
|
2093
|
+
await writeSingleLabeled(windowAddr, 0, "Window Time");
|
|
2094
|
+
await writeSingleLabeled(revertAddr, 0, "Revert Time");
|
|
2095
|
+
await writeSingleLabeled(setpointAddr, rawRegisterValue, "Setpoint");
|
|
2096
|
+
|
|
2097
|
+
await verifyRegister(rampAddr, rampRegisterValue, "Ramp Time");
|
|
2098
|
+
await verifyRegister(windowAddr, 0, "Window Time");
|
|
2099
|
+
await verifyRegister(revertAddr, 0, "Revert Time");
|
|
2100
|
+
setpointReadback = await verifyRegister(setpointAddr, rawRegisterValue, "Setpoint");
|
|
2101
|
+
|
|
2102
|
+
await writeSingleLabeled(selectedEnableAddr, 1, "Enable control selectat");
|
|
2103
|
+
selectedEnableReadback = await verifyRegister(selectedEnableAddr, 1, "Enable control selectat");
|
|
2104
|
+
otherEnableReadback = await verifyOppositeEnableRegister(otherEnableAddr, 0);
|
|
2105
|
+
|
|
2106
|
+
await writeSingleLabeled(setpointAddr, rawRegisterValue, "Setpoint activ");
|
|
2107
|
+
appliedSetpointReadback = await verifyRegister(setpointAddr, rawRegisterValue, "Setpoint activ");
|
|
2108
|
+
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
try {
|
|
2112
|
+
await runFullWriteSequence();
|
|
2113
|
+
confirmedControlState = {
|
|
2114
|
+
powerType: input.powerType,
|
|
2115
|
+
controlModelBaseAddr: controlModel.baseAddr,
|
|
2116
|
+
rampRegisterValue: rampRegisterValue,
|
|
2117
|
+
selectedEnableAddr: selectedEnableAddr,
|
|
2118
|
+
otherEnableAddr: otherEnableAddr,
|
|
2119
|
+
rawRegisterValue: rawRegisterValue
|
|
2120
|
+
};
|
|
2121
|
+
} catch (writeError) {
|
|
2122
|
+
confirmedControlState = null;
|
|
2123
|
+
// Best effort: a partial command must not remain enabled.
|
|
2124
|
+
await writeSingleLabeled(selectedEnableAddr, 0, "Rollback enable control selectat").catch(function () {});
|
|
2125
|
+
await writeSingleLabeled(otherEnableAddr, 0, "Rollback enable control opus").catch(function () {});
|
|
2126
|
+
throw writeError;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
|
|
2130
|
+
return {
|
|
2131
|
+
|
|
2132
|
+
input: input,
|
|
2133
|
+
|
|
2134
|
+
percent: percent,
|
|
2135
|
+
|
|
2136
|
+
rampDirection: directionalRamp.direction,
|
|
2137
|
+
|
|
2138
|
+
rampTimeSeconds: rampTimeSeconds,
|
|
2139
|
+
|
|
2140
|
+
sf: sf,
|
|
2141
|
+
|
|
2142
|
+
scaledValue: scaledValue,
|
|
2143
|
+
|
|
2144
|
+
rawRegisterValue: rawRegisterValue,
|
|
2145
|
+
|
|
2146
|
+
controlModel: controlModel,
|
|
2147
|
+
|
|
2148
|
+
selectedOffsets: selectedOffsets,
|
|
2149
|
+
setpointAddr: setpointAddr,
|
|
2150
|
+
selectedEnableAddr: selectedEnableAddr,
|
|
2151
|
+
otherEnableAddr: otherEnableAddr,
|
|
2152
|
+
rampAddr: rampAddr,
|
|
2153
|
+
windowAddr: windowAddr,
|
|
2154
|
+
revertAddr: revertAddr,
|
|
2155
|
+
setpointReadback: setpointReadback,
|
|
2156
|
+
appliedSetpointReadback: appliedSetpointReadback,
|
|
2157
|
+
selectedEnableReadback: selectedEnableReadback,
|
|
2158
|
+
otherEnableReadback: otherEnableReadback,
|
|
2159
|
+
readbackVerificationDisabled: node.disableReadbackVerification,
|
|
2160
|
+
commandMeta: effectiveMsg.payload && typeof effectiveMsg.payload === "object" ? {
|
|
2161
|
+
commandId: effectiveMsg.payload.command_id || null,
|
|
2162
|
+
requestedAtMs: optionalPositiveTimestampMs(effectiveMsg.payload, "command_requested_at_ms"),
|
|
2163
|
+
controlReceivedAtMs: optionalPositiveTimestampMs(effectiveMsg.payload, "control_received_at_ms"),
|
|
2164
|
+
controlCalculatedAtMs: optionalPositiveTimestampMs(effectiveMsg.payload, "control_calculated_at_ms"),
|
|
2165
|
+
allocatedAtMs: optionalPositiveTimestampMs(effectiveMsg.payload, "allocated_at_ms")
|
|
2166
|
+
} : {}
|
|
2167
|
+
|
|
2168
|
+
};
|
|
2169
|
+
|
|
2170
|
+
});
|
|
2171
|
+
|
|
2172
|
+
|
|
2173
|
+
const verifiedAtMs = Date.now();
|
|
2174
|
+
lastAppliedPercentByPowerType[result.input.powerType] = result.percent;
|
|
2175
|
+
const endToEndLatencyMs = Number.isFinite(result.commandMeta.requestedAtMs)
|
|
2176
|
+
? Math.max(0, verifiedAtMs - result.commandMeta.requestedAtMs)
|
|
2177
|
+
: null;
|
|
2178
|
+
const statusText = buildStatusText(
|
|
2179
|
+
|
|
2180
|
+
result.input.powerType,
|
|
2181
|
+
|
|
2182
|
+
result.input.mode,
|
|
2183
|
+
|
|
2184
|
+
result.input.value,
|
|
2185
|
+
|
|
2186
|
+
result.percent,
|
|
2187
|
+
|
|
2188
|
+
result.rampTimeSeconds,
|
|
2189
|
+
|
|
2190
|
+
result.input.ramp
|
|
2191
|
+
|
|
2192
|
+
);
|
|
2193
|
+
|
|
2194
|
+
|
|
2195
|
+
node.status({
|
|
2196
|
+
|
|
2197
|
+
fill: "green",
|
|
2198
|
+
|
|
2199
|
+
shape: "dot",
|
|
2200
|
+
|
|
2201
|
+
text: statusText
|
|
2202
|
+
|
|
2203
|
+
});
|
|
2204
|
+
|
|
2205
|
+
if (Number.isFinite(endToEndLatencyMs) && endToEndLatencyMs > node.latencyTargetMs) {
|
|
2206
|
+
node.status({ fill: "yellow", shape: "ring", text: statusText + " | latency " + endToEndLatencyMs + "ms" });
|
|
2207
|
+
node.warn("Fronius Write latency target exceeded: " + endToEndLatencyMs + "ms > " + node.latencyTargetMs + "ms");
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
|
|
2211
|
+
node.send({
|
|
2212
|
+
|
|
2213
|
+
topic: "fronius/write",
|
|
2214
|
+
|
|
2215
|
+
payload: {
|
|
2216
|
+
|
|
2217
|
+
status: "ok",
|
|
2218
|
+
|
|
2219
|
+
detection_mode: "auto",
|
|
2220
|
+
|
|
2221
|
+
power_type: result.input.powerType,
|
|
2222
|
+
|
|
2223
|
+
mode: result.input.mode,
|
|
2224
|
+
|
|
2225
|
+
input_value: result.input.value,
|
|
2226
|
+
|
|
2227
|
+
max_power_w:
|
|
2228
|
+
|
|
2229
|
+
result.input.powerType === "active"
|
|
2230
|
+
|
|
2231
|
+
? node.maxPower
|
|
2232
|
+
|
|
2233
|
+
: null,
|
|
2234
|
+
|
|
2235
|
+
nominal_power_w:
|
|
2236
|
+
|
|
2237
|
+
result.input.powerType === "active" &&
|
|
2238
|
+
|
|
2239
|
+
(result.input.mode === "watts" || result.input.ramp.type === "gradient_w")
|
|
2240
|
+
|
|
2241
|
+
? effectiveNominalPower(result.input)
|
|
2242
|
+
|
|
2243
|
+
: null,
|
|
2244
|
+
|
|
2245
|
+
nominal_reactive_power_var:
|
|
2246
|
+
|
|
2247
|
+
result.input.powerType === "reactive" &&
|
|
2248
|
+
|
|
2249
|
+
(result.input.mode === "vars" || result.input.ramp.type === "gradient_w")
|
|
2250
|
+
|
|
2251
|
+
? effectiveNominalReactivePower(result.input)
|
|
2252
|
+
|
|
2253
|
+
: null,
|
|
2254
|
+
|
|
2255
|
+
applied_percent: result.percent,
|
|
2256
|
+
|
|
2257
|
+
scaled_signed_value: result.scaledValue,
|
|
2258
|
+
|
|
2259
|
+
raw_written: result.rawRegisterValue,
|
|
2260
|
+
|
|
2261
|
+
scale_factor: result.sf,
|
|
2262
|
+
|
|
2263
|
+
ramp_type: result.input.ramp.type,
|
|
2264
|
+
|
|
2265
|
+
ramp_direction: result.rampDirection,
|
|
2266
|
+
|
|
2267
|
+
ramp_time_seconds: result.rampTimeSeconds,
|
|
2268
|
+
|
|
2269
|
+
ramp_input: result.input.ramp.type === "none" ? null : result.input.ramp.value,
|
|
2270
|
+
|
|
2271
|
+
setpoint_addr: result.setpointAddr,
|
|
2272
|
+
|
|
2273
|
+
enable_addr: result.selectedEnableAddr,
|
|
2274
|
+
|
|
2275
|
+
other_enable_addr: result.otherEnableAddr,
|
|
2276
|
+
|
|
2277
|
+
ramp_addr: result.rampAddr,
|
|
2278
|
+
|
|
2279
|
+
window_addr: result.windowAddr,
|
|
2280
|
+
|
|
2281
|
+
revert_addr: result.revertAddr,
|
|
2282
|
+
|
|
2283
|
+
enable_written: 1,
|
|
2284
|
+
|
|
2285
|
+
other_enable_written: 0,
|
|
2286
|
+
|
|
2287
|
+
setpoint_readback: result.setpointReadback,
|
|
2288
|
+
|
|
2289
|
+
applied_setpoint_readback: result.appliedSetpointReadback,
|
|
2290
|
+
|
|
2291
|
+
selected_enable_readback: result.selectedEnableReadback,
|
|
2292
|
+
|
|
2293
|
+
other_enable_readback: result.otherEnableReadback,
|
|
2294
|
+
|
|
2295
|
+
readback_verification_disabled: result.readbackVerificationDisabled,
|
|
2296
|
+
|
|
2297
|
+
command_id: result.commandMeta.commandId,
|
|
2298
|
+
|
|
2299
|
+
command_requested_at_ms: result.commandMeta.requestedAtMs,
|
|
2300
|
+
|
|
2301
|
+
control_received_at_ms: result.commandMeta.controlReceivedAtMs,
|
|
2302
|
+
|
|
2303
|
+
control_calculated_at_ms: result.commandMeta.controlCalculatedAtMs,
|
|
2304
|
+
|
|
2305
|
+
allocated_at_ms: result.commandMeta.allocatedAtMs,
|
|
2306
|
+
|
|
2307
|
+
write_started_at_ms: writeStartedAtMs,
|
|
2308
|
+
|
|
2309
|
+
verified_at_ms: verifiedAtMs,
|
|
2310
|
+
|
|
2311
|
+
end_to_end_latency_ms: endToEndLatencyMs,
|
|
2312
|
+
|
|
2313
|
+
latency_target_ms: node.latencyTargetMs,
|
|
2314
|
+
|
|
2315
|
+
latency_target_exceeded: Number.isFinite(endToEndLatencyMs) && endToEndLatencyMs > node.latencyTargetMs,
|
|
2316
|
+
|
|
2317
|
+
sunspec_base: sunspecBase,
|
|
2318
|
+
|
|
2319
|
+
control_model_id: result.controlModel.modelId,
|
|
2320
|
+
|
|
2321
|
+
control_model_addr: result.controlModel.addr,
|
|
2322
|
+
|
|
2323
|
+
control_model_base_addr: result.controlModel.baseAddr,
|
|
2324
|
+
|
|
2325
|
+
written_register_offset: result.selectedOffsets.pctOffset,
|
|
2326
|
+
|
|
2327
|
+
ts: new Date().toISOString()
|
|
2328
|
+
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
});
|
|
2332
|
+
|
|
2333
|
+
|
|
2334
|
+
setNodeRuntimeState(node.id, {
|
|
2335
|
+
|
|
2336
|
+
status: "ok",
|
|
2337
|
+
|
|
2338
|
+
node_id: node.id,
|
|
2339
|
+
|
|
2340
|
+
host: node.host,
|
|
2341
|
+
|
|
2342
|
+
port: node.port,
|
|
2343
|
+
|
|
2344
|
+
unit_id: node.unitId,
|
|
2345
|
+
|
|
2346
|
+
multi_inverter: !!node.multiInverter,
|
|
2347
|
+
|
|
2348
|
+
inverter_id: node.inverterId || null,
|
|
2349
|
+
|
|
2350
|
+
power_type: result.input.powerType,
|
|
2351
|
+
|
|
2352
|
+
mode: result.input.mode,
|
|
2353
|
+
|
|
2354
|
+
input_value: result.input.value,
|
|
2355
|
+
|
|
2356
|
+
applied_percent: result.percent,
|
|
2357
|
+
|
|
2358
|
+
ramp_type: result.input.ramp.type,
|
|
2359
|
+
|
|
2360
|
+
ramp_time_seconds: result.rampTimeSeconds,
|
|
2361
|
+
|
|
2362
|
+
control_model_id: result.controlModel.modelId,
|
|
2363
|
+
|
|
2364
|
+
written_register_offset: result.selectedOffsets.pctOffset,
|
|
2365
|
+
|
|
2366
|
+
readback_verification_disabled: result.readbackVerificationDisabled,
|
|
2367
|
+
|
|
2368
|
+
ts: new Date().toISOString()
|
|
2369
|
+
|
|
2370
|
+
});
|
|
2371
|
+
|
|
2372
|
+
|
|
2373
|
+
} catch (e) {
|
|
2374
|
+
|
|
2375
|
+
|
|
2376
|
+
if (isRetryableModbusError(e)) {
|
|
2377
|
+
|
|
2378
|
+
await resetRuntimeAfterCommFailure();
|
|
2379
|
+
|
|
2380
|
+
} else {
|
|
2381
|
+
|
|
2382
|
+
cachedControlModel = null;
|
|
2383
|
+
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
|
|
2387
|
+
|
|
2388
|
+
node.status({
|
|
2389
|
+
|
|
2390
|
+
|
|
2391
|
+
fill: "red",
|
|
2392
|
+
|
|
2393
|
+
|
|
2394
|
+
shape: "ring",
|
|
2395
|
+
|
|
2396
|
+
|
|
2397
|
+
text: "error"
|
|
2398
|
+
|
|
2399
|
+
|
|
2400
|
+
});
|
|
2401
|
+
|
|
2402
|
+
|
|
2403
|
+
|
|
2404
|
+
node.error(e.message, msg);
|
|
2405
|
+
|
|
2406
|
+
|
|
2407
|
+
|
|
2408
|
+
node.send({
|
|
2409
|
+
|
|
2410
|
+
|
|
2411
|
+
topic: "fronius/error",
|
|
2412
|
+
|
|
2413
|
+
|
|
2414
|
+
payload: {
|
|
2415
|
+
|
|
2416
|
+
|
|
2417
|
+
status: "error",
|
|
2418
|
+
|
|
2419
|
+
|
|
2420
|
+
detection_mode: "auto",
|
|
2421
|
+
|
|
2422
|
+
|
|
2423
|
+
error: e.message,
|
|
2424
|
+
|
|
2425
|
+
|
|
2426
|
+
ts: new Date().toISOString()
|
|
2427
|
+
|
|
2428
|
+
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
|
|
2432
|
+
});
|
|
2433
|
+
|
|
2434
|
+
|
|
2435
|
+
setNodeRuntimeState(node.id, {
|
|
2436
|
+
|
|
2437
|
+
status: "error",
|
|
2438
|
+
|
|
2439
|
+
node_id: node.id,
|
|
2440
|
+
|
|
2441
|
+
host: node.host,
|
|
2442
|
+
|
|
2443
|
+
port: node.port,
|
|
2444
|
+
|
|
2445
|
+
unit_id: node.unitId,
|
|
2446
|
+
|
|
2447
|
+
multi_inverter: !!node.multiInverter,
|
|
2448
|
+
|
|
2449
|
+
inverter_id: node.inverterId || null,
|
|
2450
|
+
|
|
2451
|
+
error: e.message,
|
|
2452
|
+
|
|
2453
|
+
ts: new Date().toISOString()
|
|
2454
|
+
|
|
2455
|
+
});
|
|
2456
|
+
|
|
2457
|
+
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
node.on("input", function (msg, send, done) {
|
|
2464
|
+
if (pendingOperation && pendingOperation.done) pendingOperation.done();
|
|
2465
|
+
pendingOperation = { msg: msg, done: done };
|
|
2466
|
+
if (operationWorkerRunning) return;
|
|
2467
|
+
|
|
2468
|
+
operationWorkerRunning = true;
|
|
2469
|
+
operationWorker = (async function () {
|
|
2470
|
+
while (pendingOperation) {
|
|
2471
|
+
const current = pendingOperation;
|
|
2472
|
+
pendingOperation = null;
|
|
2473
|
+
try {
|
|
2474
|
+
await handleInput(current.msg);
|
|
2475
|
+
if (current.done) current.done();
|
|
2476
|
+
} catch (error) {
|
|
2477
|
+
if (current.done) current.done(error);
|
|
2478
|
+
else node.error(error, current.msg);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
})().finally(function () {
|
|
2482
|
+
operationWorkerRunning = false;
|
|
2483
|
+
});
|
|
2484
|
+
});
|
|
2485
|
+
|
|
2486
|
+
|
|
2487
|
+
|
|
2488
|
+
node.on("close", async function (done) {
|
|
2489
|
+
await operationWorker.catch(function () {});
|
|
2490
|
+
|
|
2491
|
+
|
|
2492
|
+
try {
|
|
2493
|
+
|
|
2494
|
+
|
|
2495
|
+
if (client && client.isOpen) {
|
|
2496
|
+
|
|
2497
|
+
|
|
2498
|
+
await client.close();
|
|
2499
|
+
|
|
2500
|
+
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
|
|
2504
|
+
clearNodeRuntimeState(node.id);
|
|
2505
|
+
|
|
2506
|
+
|
|
2507
|
+
} catch (e) {
|
|
2508
|
+
|
|
2509
|
+
|
|
2510
|
+
// ignore
|
|
2511
|
+
|
|
2512
|
+
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
|
|
2516
|
+
done();
|
|
2517
|
+
|
|
2518
|
+
|
|
2519
|
+
});
|
|
2520
|
+
|
|
2521
|
+
|
|
2522
|
+
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
|
|
2527
|
+
|
|
2528
|
+
RED.nodes.registerType("fronius-write", FroniusWriteNode);
|
|
2529
|
+
|
|
2530
|
+
|
|
2531
|
+
};
|