@atelieruldetehnologii/node-red-custome-solutions 0.62.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/NOTICE +56 -0
  2. package/PROPRIETARY-LICENSE.txt +157 -0
  3. package/README.md +105 -0
  4. package/TERMS-AND-CONDITIONS.md +311 -0
  5. package/advantech-read/advantech-read.html +775 -0
  6. package/advantech-read/advantech-read.js +449 -0
  7. package/advantech-write/advantech-write.html +413 -0
  8. package/advantech-write/advantech-write.js +338 -0
  9. package/bess-control/bess-control.html +569 -0
  10. package/bess-control/bess-control.js +541 -0
  11. package/bess-control/lib/allocation.js +149 -0
  12. package/bess-control/lib/batteries.js +86 -0
  13. package/bess-control/lib/helpers.js +157 -0
  14. package/bess-control/lib/messages.js +131 -0
  15. package/bess-control/lib/runtime.js +156 -0
  16. package/carlo-gavazzi/carlo-gavazzi.html +4327 -0
  17. package/carlo-gavazzi/carlo-gavazzi.js +3081 -0
  18. package/control-export/control-export.html +3644 -0
  19. package/control-export/control-export.js +2192 -0
  20. package/control-reactive/control-reactive.html +3136 -0
  21. package/control-reactive/control-reactive.js +2124 -0
  22. package/fronius-emulator/README.md +258 -0
  23. package/fronius-emulator/fronius-emulator.html +170 -0
  24. package/fronius-emulator/fronius-emulator.js +520 -0
  25. package/fronius-emulator/lib/binary-codec.js +65 -0
  26. package/fronius-emulator/lib/fronius-register-map.js +185 -0
  27. package/fronius-emulator/lib/modbus-parser.js +61 -0
  28. package/fronius-emulator/lib/modbus-tcp-server.js +224 -0
  29. package/fronius-emulator/lib/validation.js +206 -0
  30. package/fronius-emulator/lib/virtual-meter.js +132 -0
  31. package/fronius-read/fronius-read.html +3944 -0
  32. package/fronius-read/fronius-read.js +1466 -0
  33. package/fronius-read/lib/inverter-profiles.js +711 -0
  34. package/fronius-read/lib/sunspec-core.js +303 -0
  35. package/fronius-read/lib/sunspec-model-parsers.js +845 -0
  36. package/fronius-write/fronius-write.html +1789 -0
  37. package/fronius-write/fronius-write.js +2531 -0
  38. package/influxdb/influxdb.html +1077 -0
  39. package/influxdb/influxdb.js +1487 -0
  40. package/janitza/janitza.html +2562 -0
  41. package/janitza/janitza.js +1500 -0
  42. package/janitza/lib/janitza-profiles.js +371 -0
  43. package/lib/a3-test-server.js +199 -0
  44. package/lib/adam-6000-profiles.js +173 -0
  45. package/lib/licensing/constants.js +103 -0
  46. package/lib/licensing/device-id.js +251 -0
  47. package/lib/licensing/errors.js +21 -0
  48. package/lib/licensing/license-client.js +377 -0
  49. package/lib/licensing/license-state.js +50 -0
  50. package/lib/licensing/node-guard.js +150 -0
  51. package/lib/licensing/package-integrity.js +167 -0
  52. package/lib/licensing/package-integrity.json +513 -0
  53. package/lib/licensing/public-key.pem +3 -0
  54. package/lib/licensing/public-key.pem.example +2 -0
  55. package/lib/licensing/public-keys.js +18 -0
  56. package/lib/licensing/storage.js +63 -0
  57. package/lib/licensing/token-verifier.js +84 -0
  58. package/lib/numbers.js +26 -0
  59. package/license/atelier-license.html +88 -0
  60. package/license/atelier-license.js +170 -0
  61. package/modbus-tcp-discover/modbus-tcp-discover.html +190 -0
  62. package/modbus-tcp-discover/modbus-tcp-discover.js +706 -0
  63. package/multi-battery/multi-battery.html +649 -0
  64. package/multi-battery/multi-battery.js +668 -0
  65. package/multi-grid/multi-grid.html +745 -0
  66. package/multi-grid/multi-grid.js +1392 -0
  67. package/multi-inverter/lib/engine-core.js +237 -0
  68. package/multi-inverter/multi-inverter.html +2674 -0
  69. package/multi-inverter/multi-inverter.js +1393 -0
  70. package/package.json +109 -0
  71. package/phoenix-contact/lib/eem-ma370-advanced.js +175 -0
  72. package/phoenix-contact/lib/eem-ma370.js +138 -0
  73. package/phoenix-contact/lib/integrations.js +149 -0
  74. package/phoenix-contact/lib/phoenix-contact-profile.js +35030 -0
  75. package/phoenix-contact/phoenix-contact.html +840 -0
  76. package/phoenix-contact/phoenix-contact.js +679 -0
  77. package/pv-generator/pv-generator.html +284 -0
  78. package/pv-generator/pv-generator.js +387 -0
  79. package/pylontech-read/pylontech-read.html +763 -0
  80. package/pylontech-read/pylontech-read.js +518 -0
  81. package/pylontech-write/pylontech-write.html +405 -0
  82. package/pylontech-write/pylontech-write.js +546 -0
  83. package/scada-aggregator/lib/aggregate.js +72 -0
  84. package/scada-aggregator/lib/groups.js +39 -0
  85. package/scada-aggregator/scada-aggregator.html +108 -0
  86. package/scada-aggregator/scada-aggregator.js +117 -0
  87. package/scada-server/lib/aggregator-map.js +58 -0
  88. package/scada-server/lib/encoding.js +235 -0
  89. package/scada-server/lib/modbus-server.js +492 -0
  90. package/scada-server/lib/object-path.js +18 -0
  91. package/scada-server/lib/register-map.js +288 -0
  92. package/scada-server/scada-server.html +1419 -0
  93. package/scada-server/scada-server.js +704 -0
  94. package/scripts/check-syntax.js +66 -0
  95. package/scripts/package-integrity.js +46 -0
  96. package/scripts/package-release.js +116 -0
  97. package/scripts/run-tests.js +2178 -0
  98. package/scripts/tgz-integrity.js +74 -0
  99. package/sma-read/lib/sunspec-core.js +83 -0
  100. package/sma-read/lib/sunspec-model-parsers.js +853 -0
  101. package/sma-read/sma-read.html +659 -0
  102. package/sma-read/sma-read.js +458 -0
  103. package/sma-write/sma-write.html +105 -0
  104. package/sma-write/sma-write.js +270 -0
  105. package/victron-read/lib/mqtt-state.js +176 -0
  106. package/victron-read/victron-read.html +1074 -0
  107. package/victron-read/victron-read.js +1222 -0
@@ -0,0 +1,668 @@
1
+ module.exports = function (RED) {
2
+
3
+ "use strict";
4
+
5
+ const { runLicensedNode } = require("../lib/licensing/node-guard");
6
+
7
+
8
+ const {
9
+ clamp,
10
+ extractTimestampMs,
11
+ getByPath,
12
+ parseBooleanLike
13
+ } = require("../bess-control/lib/helpers");
14
+
15
+ const {
16
+ proportionalAllocation
17
+ } = require("../bess-control/lib/allocation");
18
+
19
+ const {
20
+ distributionIsValid,
21
+ normalizeBatteries
22
+ } = require("../bess-control/lib/batteries");
23
+
24
+ const {
25
+ extractBatteryId,
26
+ extractBatteryTelemetry,
27
+ isFeedbackMessage
28
+ } = require("../bess-control/lib/messages");
29
+
30
+ const {
31
+ evaluateRuntimeBatteries
32
+ } = require("../bess-control/lib/runtime");
33
+
34
+ const {
35
+ atLeast
36
+ } = require("../lib/numbers");
37
+
38
+
39
+ function toCommandValue(msg, fieldNames) {
40
+
41
+ const payload = msg && msg.payload && typeof msg.payload === "object" ? msg.payload : null;
42
+
43
+ if (!payload) {
44
+ return undefined;
45
+ }
46
+
47
+ for (let index = 0; index < fieldNames.length; index += 1) {
48
+
49
+ const field = fieldNames[index];
50
+
51
+ if (payload[field] !== undefined) {
52
+ return payload[field];
53
+ }
54
+
55
+ }
56
+
57
+ return undefined;
58
+
59
+ }
60
+
61
+
62
+ function toFiniteNumber(value) {
63
+
64
+ const numeric = Number(value);
65
+ return Number.isFinite(numeric) ? numeric : null;
66
+
67
+ }
68
+
69
+
70
+ function extractIncomingCommand(msg) {
71
+
72
+ const payload = msg && msg.payload && typeof msg.payload === "object" ? msg.payload : null;
73
+ let powerW = null;
74
+ let externalControl = null;
75
+ let socMinPct = null;
76
+ let socHystPct = null;
77
+
78
+ if (payload) {
79
+
80
+ const powerWRaw = toCommandValue(msg, ["power_w", "limit_w", "value_w", "watts"]);
81
+ const powerKwRaw = toCommandValue(msg, ["power_kw", "limit_kw", "value_kw", "kw", "power"]);
82
+ const externalRaw = toCommandValue(msg, [
83
+ "external_control",
84
+ "externalControl",
85
+ "enable_external_control",
86
+ "enableExternalControl",
87
+ "control_extern",
88
+ "enabled",
89
+ "enable"
90
+ ]);
91
+ const socMinRaw = toCommandValue(msg, [
92
+ "soc_min_pct",
93
+ "soc_min",
94
+ "min_soc_pct",
95
+ "socMinPct"
96
+ ]);
97
+ const socHystRaw = toCommandValue(msg, [
98
+ "soc_hyst_pct",
99
+ "soc_hysteresis_pct",
100
+ "soc_hyst",
101
+ "socHystPct"
102
+ ]);
103
+
104
+ if (powerWRaw !== undefined) {
105
+ powerW = toFiniteNumber(powerWRaw);
106
+ } else if (powerKwRaw !== undefined) {
107
+ const powerKw = toFiniteNumber(powerKwRaw);
108
+ powerW = Number.isFinite(powerKw) ? Math.round(powerKw * 1000) : null;
109
+ }
110
+
111
+ if (externalRaw !== undefined) {
112
+ externalControl = parseBooleanLike(externalRaw);
113
+ }
114
+
115
+ if (socMinRaw !== undefined) {
116
+ socMinPct = toFiniteNumber(socMinRaw);
117
+ }
118
+
119
+ if (socHystRaw !== undefined) {
120
+ socHystPct = toFiniteNumber(socHystRaw);
121
+ }
122
+
123
+ }
124
+
125
+ return {
126
+ hasCommand: powerW !== null || externalControl !== null || socMinPct !== null || socHystPct !== null,
127
+ powerW: powerW,
128
+ externalControl: externalControl,
129
+ socMinPct: socMinPct,
130
+ socHystPct: socHystPct
131
+ };
132
+
133
+ }
134
+
135
+
136
+ function MultiBatteryNode(config) {
137
+
138
+ RED.nodes.createNode(this, config);
139
+
140
+ const node = this;
141
+
142
+ runLicensedNode(node, RED, config, function startLicensedRuntime() {
143
+
144
+
145
+ node.name = String(config.name || "").trim();
146
+ node.batterySocPath = String(config.batterySocPath || "payload.battery_soc").trim();
147
+ node.externalControlStatusPath = String(config.externalControlStatusPath || "payload.external_control_status").trim();
148
+ node.alarmLevelPath = String(config.alarmLevelPath || "payload.battery_alarm_level").trim();
149
+ node.operatingStatusPath = String(config.operatingStatusPath || "payload.battery_operating_status").trim();
150
+ node.deratingReasonPath = String(config.deratingReasonPath || "payload.battery_derating_reason").trim();
151
+ node.availableChargePath = String(config.availableChargePath || "").trim();
152
+ node.availableDischargePath = String(config.availableDischargePath || "").trim();
153
+ node.feedbackTimeoutMs = atLeast(config.feedbackTimeoutMs, 0, 5000);
154
+ node.staleTimeoutMs = atLeast(config.staleTimeoutMs, 0, 0);
155
+ node.strategy = config.strategy || "fixed_share";
156
+ node.outputTopic = String(config.outputTopic || "bess/setpoint").trim();
157
+ node.enableDebugOutput = !!config.enableDebugOutput;
158
+ node.batteryCount = atLeast(config.batteryCount, 1, 1);
159
+ node.batteries = normalizeBatteries(config.batteries, node.batteryCount);
160
+
161
+
162
+ let lastExternalControl = null;
163
+ let lastRequestedPowerW = 0;
164
+ let lastCommandTs = null;
165
+ let watchdogTimer = null;
166
+ let lastWatchdogSignature = "";
167
+ const batteryState = {};
168
+
169
+ function applyGlobalSocOverrides(batteries, overrides) {
170
+
171
+ return batteries.map(function (battery) {
172
+ return Object.assign({}, battery, {
173
+ socMin: Number.isFinite(overrides.socMinPct) ? overrides.socMinPct : battery.socMin,
174
+ socHyst: Number.isFinite(overrides.socHystPct) ? overrides.socHystPct : battery.socHyst
175
+ });
176
+ });
177
+
178
+ }
179
+
180
+
181
+ function configuredBatteryIds() {
182
+
183
+ return node.batteries.map(function (battery) {
184
+ return battery.id;
185
+ }).filter(Boolean);
186
+
187
+ }
188
+
189
+ function buildRuntimeBatteries(batteries, now) {
190
+ return evaluateRuntimeBatteries(
191
+ batteries,
192
+ batteryState,
193
+ {
194
+ staleTimeoutMs: node.staleTimeoutMs,
195
+ feedbackTimeoutMs: node.feedbackTimeoutMs
196
+ },
197
+ now
198
+ ).map(function (runtime) {
199
+ const config = runtime.config;
200
+ const state = runtime.state;
201
+ return Object.assign({}, config, {
202
+ enabled: !runtime.excluded,
203
+ reason: runtime.excludedReasons.length ? runtime.excludedReasons.join(",") : "ready",
204
+ currentSocPct: state.soc,
205
+ externalControlStatus: state.externalControlStatus,
206
+ alarmLevel: state.alarmLevel,
207
+ operatingStatus: state.operatingStatus,
208
+ deratingReason: state.deratingReason,
209
+ availableChargeW: runtime.availableChargeW,
210
+ availableDischargeW: runtime.availableDischargeW,
211
+ feedback: state.feedback,
212
+ telemetryTs: state.ts,
213
+ feedbackTs: state.feedbackTs,
214
+ telemetryStale: runtime.telemetryStale,
215
+ feedbackStale: runtime.feedbackStale,
216
+ dischargeAllowed: runtime.dischargeAllowed
217
+ });
218
+ });
219
+ }
220
+
221
+ function validateDistribution(batteries) {
222
+ const valid = distributionIsValid(batteries);
223
+ return {
224
+ valid: valid,
225
+ reasons: valid ? [] : ["charge/discharge shares must total 100%"]
226
+ };
227
+ }
228
+
229
+
230
+ function updateTelemetry(msg, now, msgTs) {
231
+
232
+ const telemetry = extractBatteryTelemetry(msg, {
233
+ getByPath: getByPath,
234
+ parseBooleanLike: parseBooleanLike,
235
+ batterySocPath: node.batterySocPath,
236
+ externalControlStatusPath: node.externalControlStatusPath,
237
+ alarmLevelPath: node.alarmLevelPath,
238
+ operatingStatusPath: node.operatingStatusPath,
239
+ deratingReasonPath: node.deratingReasonPath,
240
+ availableChargePath: node.availableChargePath,
241
+ availableDischargePath: node.availableDischargePath
242
+ });
243
+
244
+ const incomingBatteryId = extractBatteryId(msg);
245
+ const batteryIds = configuredBatteryIds();
246
+
247
+
248
+ const hasTelemetry =
249
+ telemetry.soc !== null ||
250
+ telemetry.externalControlStatus !== null ||
251
+ telemetry.alarmLevel !== undefined ||
252
+ telemetry.operatingStatus !== undefined ||
253
+ telemetry.deratingReason !== undefined ||
254
+ telemetry.availableChargeW !== null ||
255
+ telemetry.availableDischargeW !== null;
256
+
257
+ if (!hasTelemetry) {
258
+ return false;
259
+ }
260
+
261
+
262
+ let targetIds = [];
263
+
264
+ if (incomingBatteryId) {
265
+
266
+ if (batteryIds.indexOf(incomingBatteryId) !== -1) {
267
+ targetIds = [incomingBatteryId];
268
+ } else {
269
+ node.warn("multi-battery: batteryId necunoscut: " + incomingBatteryId);
270
+ return false;
271
+ }
272
+
273
+ } else if (batteryIds.length === 1) {
274
+
275
+ targetIds = [batteryIds[0]];
276
+
277
+ } else {
278
+
279
+ node.warn("multi-battery: mesaj de baterie fara batteryId pentru configuratie multi-baterie");
280
+ return false;
281
+
282
+ }
283
+
284
+
285
+ targetIds.forEach(function (batteryId) {
286
+
287
+ const current = batteryState[batteryId] || {};
288
+
289
+ batteryState[batteryId] = Object.assign({}, current, {
290
+ soc: telemetry.soc !== null ? telemetry.soc : current.soc,
291
+ externalControlStatus: telemetry.externalControlStatus !== null ? telemetry.externalControlStatus : current.externalControlStatus,
292
+ alarmLevel: telemetry.alarmLevel !== undefined ? telemetry.alarmLevel : current.alarmLevel,
293
+ operatingStatus: telemetry.operatingStatus !== undefined ? telemetry.operatingStatus : current.operatingStatus,
294
+ deratingReason: telemetry.deratingReason !== undefined ? telemetry.deratingReason : current.deratingReason,
295
+ availableChargeW: telemetry.availableChargeW !== null ? telemetry.availableChargeW : current.availableChargeW,
296
+ availableDischargeW: telemetry.availableDischargeW !== null ? telemetry.availableDischargeW : current.availableDischargeW,
297
+ ts: msgTs || now
298
+ });
299
+
300
+ });
301
+
302
+
303
+ return true;
304
+
305
+ }
306
+
307
+
308
+ function updateFeedback(msg, now, msgTs) {
309
+
310
+ if (!isFeedbackMessage(msg)) {
311
+ return false;
312
+ }
313
+
314
+ const batteryId = extractBatteryId(msg);
315
+ const configuredIds = configuredBatteryIds();
316
+
317
+ if (batteryId) {
318
+
319
+ if (configuredIds.indexOf(batteryId) === -1) {
320
+ node.warn("multi-battery: feedback pentru batteryId necunoscut: " + batteryId);
321
+ return false;
322
+ }
323
+
324
+ const current = batteryState[batteryId] || {};
325
+ batteryState[batteryId] = Object.assign({}, current, {
326
+ feedback: msg.payload,
327
+ feedbackTs: msgTs || now,
328
+ writeFeedback: {
329
+ status: msg.payload && msg.payload.status,
330
+ ts: msgTs || now
331
+ }
332
+ });
333
+ return true;
334
+
335
+ }
336
+
337
+ if (configuredIds.length === 1) {
338
+ const onlyId = configuredIds[0];
339
+ const current = batteryState[onlyId] || {};
340
+ batteryState[onlyId] = Object.assign({}, current, {
341
+ feedback: msg.payload,
342
+ feedbackTs: msgTs || now,
343
+ writeFeedback: {
344
+ status: msg.payload && msg.payload.status,
345
+ ts: msgTs || now
346
+ }
347
+ });
348
+ return true;
349
+ }
350
+
351
+ node.warn("multi-battery: feedback fara batteryId pentru configuratie multi-baterie");
352
+ return false;
353
+
354
+ }
355
+
356
+
357
+ function buildDiagnostic(command, runtimeBatteries, validation) {
358
+
359
+ return {
360
+ topic: node.outputTopic,
361
+ payload: {
362
+ command: {
363
+ requested_power_w: lastRequestedPowerW,
364
+ external_control: lastExternalControl,
365
+ received_power_w: command.powerW,
366
+ received_external_control: command.externalControl,
367
+ soc_min_pct: command.socMinPct,
368
+ soc_hyst_pct: command.socHystPct,
369
+ last_command_ts: lastCommandTs
370
+ },
371
+ strategy: node.strategy,
372
+ valid: validation.valid,
373
+ reasons: validation.reasons,
374
+ batteries: runtimeBatteries.map(function (battery) {
375
+ return {
376
+ id: battery.id,
377
+ enabled: battery.enabled,
378
+ reason: battery.reason,
379
+ requested_power_w: battery.requestedPowerW,
380
+ clamped_power_w: battery.clampedPowerW,
381
+ current_soc_pct: battery.currentSocPct,
382
+ soc_min_pct: battery.socMin,
383
+ soc_hyst_pct: battery.socHyst,
384
+ external_control_status: battery.externalControlStatus,
385
+ alarm_level: battery.alarmLevel,
386
+ operating_status: battery.operatingStatus,
387
+ derating_reason: battery.deratingReason,
388
+ available_charge_w: battery.availableChargeW,
389
+ available_discharge_w: battery.availableDischargeW,
390
+ feedback: battery.feedback,
391
+ telemetry_ts: battery.telemetryTs,
392
+ feedback_ts: battery.feedbackTs
393
+ };
394
+ })
395
+ }
396
+ };
397
+
398
+ }
399
+
400
+
401
+ function sendOutputs(command, runtimeBatteries, validation) {
402
+
403
+ const outputs = [];
404
+
405
+ runtimeBatteries.forEach(function (battery) {
406
+
407
+ const msg = {
408
+ topic: node.outputTopic,
409
+ batteryId: battery.id,
410
+ payload: {
411
+ battery_id: battery.id,
412
+ external_control: !!lastExternalControl,
413
+ power_w: battery.clampedPowerW,
414
+ requested_power_w: battery.requestedPowerW,
415
+ soc_min_pct: battery.socMin,
416
+ soc_hyst_pct: battery.socHyst,
417
+ strategy: node.strategy
418
+ }
419
+ };
420
+
421
+ outputs.push(msg);
422
+
423
+ });
424
+
425
+ if (node.enableDebugOutput) {
426
+ outputs.push(buildDiagnostic(command, runtimeBatteries, validation));
427
+ }
428
+
429
+ while (outputs.length < node.batteryCount + (node.enableDebugOutput ? 1 : 0)) {
430
+ outputs.push(null);
431
+ }
432
+
433
+ node.send(outputs);
434
+
435
+ }
436
+
437
+
438
+ function applyDistribution(totalPowerW, runtimeBatteries) {
439
+
440
+ const chargeTargets = runtimeBatteries.map(function (battery) {
441
+ return battery.shareChargePct;
442
+ });
443
+
444
+ const dischargeTargets = runtimeBatteries.map(function (battery) {
445
+ return battery.shareDischargePct;
446
+ });
447
+
448
+ const bySocWeights = runtimeBatteries.map(function (battery) {
449
+ const soc = Number.isFinite(battery.currentSocPct) ? battery.currentSocPct : 0;
450
+ return totalPowerW >= 0 ? soc : Math.max(0, 100 - soc);
451
+ });
452
+
453
+ const byAvailablePowerWeights = runtimeBatteries.map(function (battery) {
454
+ return totalPowerW >= 0
455
+ ? Math.max(0, battery.availableChargeW || battery.maxChargeW || 0)
456
+ : Math.max(0, battery.availableDischargeW || battery.maxDischargeW || 0);
457
+ });
458
+
459
+ let weights;
460
+
461
+ switch (node.strategy) {
462
+ case "by_soc":
463
+ weights = bySocWeights;
464
+ break;
465
+ case "by_available_power":
466
+ weights = byAvailablePowerWeights;
467
+ break;
468
+ case "hybrid":
469
+ weights = chargeTargets.map(function (target, index) {
470
+ const base = totalPowerW >= 0 ? target : dischargeTargets[index];
471
+ const socWeight = bySocWeights[index];
472
+ const availableWeight = byAvailablePowerWeights[index];
473
+ return (base * 0.5) + (socWeight * 0.25) + (availableWeight * 0.25);
474
+ });
475
+ break;
476
+ case "fixed_share":
477
+ default:
478
+ weights = totalPowerW >= 0 ? chargeTargets : dischargeTargets;
479
+ break;
480
+ }
481
+
482
+ const entries = runtimeBatteries.map(function (battery, index) {
483
+ const capW = totalPowerW >= 0
484
+ ? Math.min(battery.maxChargeW, battery.availableChargeW)
485
+ : (battery.dischargeAllowed ? Math.min(battery.maxDischargeW, battery.availableDischargeW) : 0);
486
+ return {
487
+ id: battery.id,
488
+ weight: weights[index],
489
+ capW: battery.enabled ? capW : 0
490
+ };
491
+ });
492
+ const allocated = proportionalAllocation(totalPowerW, entries);
493
+ const signed = runtimeBatteries.map(function (battery) {
494
+ return Number(allocated[battery.id] || 0);
495
+ });
496
+
497
+ return signed;
498
+
499
+ }
500
+
501
+
502
+ function updateStatus(runtimeBatteries, validation) {
503
+
504
+ const enabled = runtimeBatteries.filter(function (battery) { return battery.enabled; }).length;
505
+ const total = runtimeBatteries.length;
506
+
507
+ if (!validation.valid) {
508
+ node.status({
509
+ fill: "red",
510
+ shape: "ring",
511
+ text: "invalid: " + validation.reasons.join(", ")
512
+ });
513
+ return;
514
+ }
515
+
516
+ if (!lastExternalControl) {
517
+ node.status({
518
+ fill: "yellow",
519
+ shape: "ring",
520
+ text: "external control off"
521
+ });
522
+ return;
523
+ }
524
+
525
+ node.status({
526
+ fill: enabled === total ? "green" : "yellow",
527
+ shape: enabled === total ? "dot" : "ring",
528
+ text: enabled + "/" + total + " active | " + lastRequestedPowerW + "W"
529
+ });
530
+
531
+ }
532
+
533
+
534
+ node.on("input", function (msg, send, done) {
535
+
536
+ const now = Date.now();
537
+ const msgTs = extractTimestampMs(msg, now);
538
+
539
+ let handled = false;
540
+
541
+ if (updateTelemetry(msg, now, msgTs)) {
542
+ handled = true;
543
+ }
544
+
545
+ if (updateFeedback(msg, now, msgTs)) {
546
+ handled = true;
547
+ }
548
+
549
+ const command = extractIncomingCommand(msg);
550
+
551
+ if (command.hasCommand) {
552
+
553
+ handled = true;
554
+
555
+ if (command.externalControl !== null) {
556
+ lastExternalControl = command.externalControl;
557
+ }
558
+
559
+ if (command.powerW !== null) {
560
+ lastRequestedPowerW = Math.round(command.powerW);
561
+ }
562
+
563
+ lastCommandTs = msgTs || now;
564
+
565
+ const batteriesWithOverrides = applyGlobalSocOverrides(node.batteries, command);
566
+
567
+ const runtimeBatteries = buildRuntimeBatteries(batteriesWithOverrides, now);
568
+
569
+ const validation = validateDistribution(batteriesWithOverrides);
570
+
571
+ if (validation.valid) {
572
+
573
+ const allocations = applyDistribution(lastRequestedPowerW, runtimeBatteries);
574
+
575
+ runtimeBatteries.forEach(function (battery, index) {
576
+ const requested = allocations[index] || 0;
577
+ const limit = requested >= 0
578
+ ? Math.min(
579
+ Math.max(0, Number(battery.maxChargeW || 0)),
580
+ Math.max(0, Number(battery.availableChargeW || 0))
581
+ )
582
+ : (battery.dischargeAllowed
583
+ ? Math.min(
584
+ Math.max(0, Number(battery.maxDischargeW || 0)),
585
+ Math.max(0, Number(battery.availableDischargeW || 0))
586
+ )
587
+ : 0);
588
+
589
+ battery.requestedPowerW = requested;
590
+ battery.clampedPowerW = battery.enabled ? clamp(requested, -limit, limit) : 0;
591
+ });
592
+
593
+ } else {
594
+
595
+ runtimeBatteries.forEach(function (battery) {
596
+ battery.requestedPowerW = 0;
597
+ battery.clampedPowerW = 0;
598
+ });
599
+
600
+ }
601
+
602
+ updateStatus(runtimeBatteries, validation);
603
+ sendOutputs(command, runtimeBatteries, validation);
604
+
605
+ } else if (!handled) {
606
+
607
+ node.debug("multi-battery: mesaj ignorat fara command, telemetry sau feedback utile");
608
+
609
+ }
610
+
611
+ if (done) {
612
+ done();
613
+ }
614
+
615
+ });
616
+
617
+ if (node.staleTimeoutMs > 0) {
618
+ watchdogTimer = setInterval(function () {
619
+ if (!lastCommandTs) {
620
+ return;
621
+ }
622
+
623
+ const now = Date.now();
624
+ const runtimeBatteries = buildRuntimeBatteries(node.batteries, now);
625
+ const validation = validateDistribution(node.batteries);
626
+ if (!runtimeBatteries.some(function (battery) { return battery.telemetryStale; })) {
627
+ lastWatchdogSignature = "";
628
+ return;
629
+ }
630
+ const signature = JSON.stringify(runtimeBatteries.map(function (battery) {
631
+ return [battery.id, battery.enabled, battery.reason];
632
+ }));
633
+
634
+ if (signature === lastWatchdogSignature) {
635
+ return;
636
+ }
637
+ lastWatchdogSignature = signature;
638
+
639
+ runtimeBatteries.forEach(function (battery) {
640
+ battery.requestedPowerW = 0;
641
+ battery.clampedPowerW = 0;
642
+ });
643
+ updateStatus(runtimeBatteries, validation);
644
+ sendOutputs({
645
+ hasCommand: false,
646
+ powerW: null,
647
+ externalControl: null,
648
+ socMinPct: null,
649
+ socHystPct: null
650
+ }, runtimeBatteries, validation);
651
+ }, Math.max(250, Math.min(node.staleTimeoutMs, 1000)));
652
+ }
653
+
654
+ node.on("close", function (done) {
655
+ if (watchdogTimer) {
656
+ clearInterval(watchdogTimer);
657
+ watchdogTimer = null;
658
+ }
659
+ if (done) done();
660
+ });
661
+
662
+
663
+ });
664
+ }
665
+
666
+ RED.nodes.registerType("multi-battery", MultiBatteryNode);
667
+
668
+ };