@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,541 @@
1
+ module.exports = function (RED) {
2
+
3
+ "use strict";
4
+
5
+ const { runLicensedNode } = require("../lib/licensing/node-guard");
6
+
7
+
8
+ const {
9
+ denormalizeByConvention,
10
+ extractTimestampMs,
11
+ getByPath,
12
+ normalizeByConvention,
13
+ parseBooleanLike
14
+ } = require("./lib/helpers");
15
+
16
+
17
+ function toFiniteNumber(value, fallback) {
18
+
19
+ const numeric = Number(value);
20
+ return Number.isFinite(numeric) ? numeric : fallback;
21
+
22
+ }
23
+
24
+
25
+ function toOptionalFiniteNumber(value) {
26
+
27
+ if (value === "" || value === null || value === undefined) {
28
+ return null;
29
+ }
30
+
31
+ const numeric = Number(value);
32
+ return Number.isFinite(numeric) ? numeric : null;
33
+
34
+ }
35
+
36
+
37
+ function firstFinite() {
38
+
39
+ for (let index = 0; index < arguments.length; index += 1) {
40
+
41
+ const numeric = Number(arguments[index]);
42
+
43
+ if (Number.isFinite(numeric)) {
44
+ return numeric;
45
+ }
46
+
47
+ }
48
+
49
+ return null;
50
+
51
+ }
52
+
53
+
54
+ function extractGridReference(msg, path) {
55
+
56
+ if (typeof msg.payload === "number") {
57
+ return Number(msg.payload);
58
+ }
59
+
60
+ if (typeof msg.payload === "string" && msg.payload.trim() !== "") {
61
+
62
+ const direct = Number(msg.payload);
63
+
64
+ if (Number.isFinite(direct)) {
65
+ return direct;
66
+ }
67
+
68
+ }
69
+
70
+ const payload = msg.payload;
71
+
72
+ if (!payload || typeof payload !== "object") {
73
+ return null;
74
+ }
75
+
76
+ const byPath = Number(getByPath(msg, path));
77
+
78
+ if (Number.isFinite(byPath)) {
79
+ return byPath;
80
+ }
81
+
82
+ return firstFinite(
83
+ payload.grid_reference_w,
84
+ payload.power_w,
85
+ payload.pcc_power,
86
+ payload.grid_power,
87
+ payload.power,
88
+ payload.value
89
+ );
90
+
91
+ }
92
+
93
+
94
+ function extractPhasePower(msg, path, fallbackPaths) {
95
+
96
+ const candidates = [];
97
+
98
+ if (path) {
99
+ candidates.push(getByPath(msg, path));
100
+ }
101
+
102
+ (Array.isArray(fallbackPaths) ? fallbackPaths : []).forEach(function (fallbackPath) {
103
+ candidates.push(getByPath(msg, fallbackPath));
104
+ });
105
+
106
+ for (let index = 0; index < candidates.length; index += 1) {
107
+ const value = Number(candidates[index]);
108
+ if (Number.isFinite(value)) {
109
+ return value;
110
+ }
111
+ }
112
+
113
+ return null;
114
+
115
+ }
116
+
117
+
118
+ function extractBessOff(msg) {
119
+
120
+ const payloadOverride = getByPath(msg, "payload.bess_off");
121
+ const parsed = parseBooleanLike(payloadOverride !== undefined ? payloadOverride : msg.bessOff);
122
+
123
+ return parsed;
124
+
125
+ }
126
+
127
+
128
+ function buildPrimaryPayload(powerW, externalControl, overrides) {
129
+
130
+ const payload = {
131
+ power_w: Math.round(powerW || 0),
132
+ external_control: !!externalControl
133
+ };
134
+
135
+ if (overrides && Number.isFinite(overrides.socMinPct)) {
136
+ payload.soc_min_pct = overrides.socMinPct;
137
+ }
138
+
139
+ if (overrides && Number.isFinite(overrides.socHystPct)) {
140
+ payload.soc_hyst_pct = overrides.socHystPct;
141
+ }
142
+
143
+ return payload;
144
+
145
+ }
146
+
147
+
148
+ function computeAbsoluteZeroFeedInTarget(phaseValues, options) {
149
+
150
+ const values = phaseValues.filter(function (value) {
151
+ return Number.isFinite(value);
152
+ });
153
+
154
+ const kRel = Math.max(0, Number(options && options.kRel));
155
+ const deadbandW = Math.max(0, Number(options && options.deadbandW));
156
+ const currentCommandW = Number(options && options.currentCommandW) || 0;
157
+
158
+ if (values.length !== 3) {
159
+ return {
160
+ totalPowerW: 0,
161
+ reason: "missing_phase_power",
162
+ valid: false
163
+ };
164
+ }
165
+
166
+ const allImport = values.every(function (value) { return value > 0; });
167
+ const allExport = values.every(function (value) { return value < 0; });
168
+ const minPhaseW = Math.min.apply(null, values);
169
+ const maxPhaseW = Math.max.apply(null, values);
170
+ const discharging = currentCommandW > 0;
171
+ const charging = currentCommandW < 0;
172
+
173
+ if (discharging || (!charging && allImport)) {
174
+
175
+ if (Math.abs(minPhaseW) <= deadbandW) {
176
+ return {
177
+ totalPowerW: Math.max(0, currentCommandW),
178
+ reason: "zero_feed_in_absolute_deadband",
179
+ valid: true
180
+ };
181
+ }
182
+
183
+ return {
184
+ totalPowerW: currentCommandW + (3 * kRel * minPhaseW),
185
+ reason: minPhaseW < 0
186
+ ? "zero_feed_in_absolute_reduce_discharge"
187
+ : "zero_feed_in_absolute_discharge",
188
+ valid: true
189
+ };
190
+ }
191
+
192
+ if (charging || (!discharging && allExport)) {
193
+
194
+ if (Math.abs(maxPhaseW) <= deadbandW) {
195
+ return {
196
+ totalPowerW: Math.min(0, currentCommandW),
197
+ reason: "zero_feed_in_absolute_deadband",
198
+ valid: true
199
+ };
200
+ }
201
+
202
+ return {
203
+ totalPowerW: currentCommandW + (3 * kRel * maxPhaseW),
204
+ reason: maxPhaseW > 0
205
+ ? "zero_feed_in_absolute_reduce_charge"
206
+ : "zero_feed_in_absolute_charge",
207
+ valid: true
208
+ };
209
+ }
210
+
211
+ if (allImport) {
212
+
213
+ const limitingPhaseW = minPhaseW;
214
+
215
+ if (Math.abs(limitingPhaseW) <= deadbandW) {
216
+ return {
217
+ totalPowerW: 0,
218
+ reason: "zero_feed_in_absolute_deadband",
219
+ valid: true
220
+ };
221
+ }
222
+
223
+ return {
224
+ totalPowerW: 3 * kRel * limitingPhaseW,
225
+ reason: "zero_feed_in_absolute_discharge",
226
+ valid: true
227
+ };
228
+ }
229
+
230
+ if (allExport) {
231
+
232
+ const limitingPhaseW = maxPhaseW;
233
+
234
+ if (Math.abs(limitingPhaseW) <= deadbandW) {
235
+ return {
236
+ totalPowerW: 0,
237
+ reason: "zero_feed_in_absolute_deadband",
238
+ valid: true
239
+ };
240
+ }
241
+
242
+ return {
243
+ totalPowerW: 3 * kRel * limitingPhaseW,
244
+ reason: "zero_feed_in_absolute_charge",
245
+ valid: true
246
+ };
247
+ }
248
+
249
+ return {
250
+ totalPowerW: 0,
251
+ reason: "zero_feed_in_absolute_mixed_phases",
252
+ valid: true
253
+ };
254
+
255
+ }
256
+
257
+
258
+ function BessControlNode(config) {
259
+
260
+ RED.nodes.createNode(this, config);
261
+
262
+ const node = this;
263
+
264
+ runLicensedNode(node, RED, config, function startLicensedRuntime() {
265
+
266
+
267
+ node.name = String(config.name || "").trim();
268
+ node.controlMode = config.controlMode || "grid_setpoint";
269
+ node.zeroFeedInMode = config.zeroFeedInMode || "relative";
270
+ node.signConvention = config.signConvention || "positive_import";
271
+ node.gridPowerPath = String(config.gridPowerPath || "payload.power_w").trim();
272
+ node.gridSetpointW = toFiniteNumber(config.gridSetpointW, 0);
273
+ node.nominalPowerW = toFiniteNumber(config.nominalPowerW, 0);
274
+ node.kRel = Math.max(0, toFiniteNumber(config.kRel, 0.3));
275
+ node.deadbandW = Math.max(0, toFiniteNumber(config.deadbandW, 1000));
276
+ node.staleTimeoutMs = Math.max(0, toFiniteNumber(config.staleTimeoutMs, 5000));
277
+ node.socMinPct = toOptionalFiniteNumber(config.socMinPct);
278
+ node.socHystPct = toOptionalFiniteNumber(config.socHystPct);
279
+ node.phasePowerPathL1 = String(config.phasePowerPathL1 || "payload.l1_power_w").trim();
280
+ node.phasePowerPathL2 = String(config.phasePowerPathL2 || "payload.l2_power_w").trim();
281
+ node.phasePowerPathL3 = String(config.phasePowerPathL3 || "payload.l3_power_w").trim();
282
+ node.bessOff = !!config.bessOff;
283
+ node.outputTopic = String(config.outputTopic || "bess/setpoint").trim();
284
+ node.enableDebugOutput = !!config.enableDebugOutput;
285
+
286
+
287
+ let lastGridNormalizedW = null;
288
+ let lastGridTs = null;
289
+ let lastPhaseRawW = { l1: null, l2: null, l3: null };
290
+ let lastPhaseNormalizedW = { l1: null, l2: null, l3: null };
291
+ let lastTotalCommandW = 0;
292
+ let bessOff = node.bessOff;
293
+ let lastMeasurementReceivedAtMs = null;
294
+ let watchdogSafeEmitted = false;
295
+ let watchdogTimer = null;
296
+
297
+
298
+ function refreshStatus(reason, totalPowerW) {
299
+
300
+ node.status({
301
+ fill: bessOff ? "blue" : "green",
302
+ shape: bessOff ? "ring" : "dot",
303
+ text: reason + " | total " + Math.round(totalPowerW || 0) + "W"
304
+ });
305
+
306
+ }
307
+
308
+
309
+ node.on("input", function (msg, send, done) {
310
+
311
+ try {
312
+
313
+ const msgTs = extractTimestampMs(msg);
314
+ const parsedBessOff = extractBessOff(msg);
315
+
316
+ if (parsedBessOff !== null) {
317
+ bessOff = parsedBessOff;
318
+ }
319
+
320
+ const gridReference = extractGridReference(msg, node.gridPowerPath);
321
+ const updatedGrid = Number.isFinite(gridReference);
322
+ const phaseRawW = {
323
+ l1: extractPhasePower(msg, node.phasePowerPathL1, ["payload.power_l1_w", "payload.l1_power_w"]),
324
+ l2: extractPhasePower(msg, node.phasePowerPathL2, ["payload.power_l2_w", "payload.l2_power_w"]),
325
+ l3: extractPhasePower(msg, node.phasePowerPathL3, ["payload.power_l3_w", "payload.l3_power_w"])
326
+ };
327
+
328
+ const phaseNormalizedW = {
329
+ l1: Number.isFinite(phaseRawW.l1) ? normalizeByConvention(phaseRawW.l1, node.signConvention) : null,
330
+ l2: Number.isFinite(phaseRawW.l2) ? normalizeByConvention(phaseRawW.l2, node.signConvention) : null,
331
+ l3: Number.isFinite(phaseRawW.l3) ? normalizeByConvention(phaseRawW.l3, node.signConvention) : null
332
+ };
333
+
334
+ if (updatedGrid) {
335
+ lastGridNormalizedW = normalizeByConvention(gridReference, node.signConvention);
336
+ lastGridTs = msgTs;
337
+ lastMeasurementReceivedAtMs = Date.now();
338
+ watchdogSafeEmitted = false;
339
+ }
340
+
341
+ ["l1", "l2", "l3"].forEach(function (phaseKey) {
342
+ if (Number.isFinite(phaseRawW[phaseKey])) {
343
+ lastPhaseRawW[phaseKey] = phaseRawW[phaseKey];
344
+ lastPhaseNormalizedW[phaseKey] = phaseNormalizedW[phaseKey];
345
+ lastMeasurementReceivedAtMs = Date.now();
346
+ watchdogSafeEmitted = false;
347
+ }
348
+ });
349
+
350
+ let commandNormalizedW = lastTotalCommandW;
351
+ let reason = "hold_last_command";
352
+ let validMeasurement = true;
353
+
354
+ if (bessOff) {
355
+ commandNormalizedW = 0;
356
+ reason = "bess_off";
357
+ } else if (node.controlMode === "nominal_power") {
358
+ commandNormalizedW = normalizeByConvention(node.nominalPowerW, node.signConvention);
359
+ reason = "nominal_power";
360
+ } else if (node.controlMode === "zero_feed_in" && node.zeroFeedInMode === "absolute") {
361
+
362
+ const absoluteResult = computeAbsoluteZeroFeedInTarget(
363
+ [lastPhaseNormalizedW.l1, lastPhaseNormalizedW.l2, lastPhaseNormalizedW.l3],
364
+ {
365
+ kRel: node.kRel,
366
+ deadbandW: node.deadbandW,
367
+ currentCommandW: lastTotalCommandW
368
+ }
369
+ );
370
+
371
+ commandNormalizedW = absoluteResult.totalPowerW;
372
+ reason = absoluteResult.reason;
373
+ validMeasurement = absoluteResult.valid;
374
+
375
+ } else {
376
+
377
+ if (!Number.isFinite(lastGridNormalizedW)) {
378
+ validMeasurement = false;
379
+ reason = "missing_grid_power";
380
+ } else {
381
+
382
+ let targetNormalizedW = 0;
383
+
384
+ if (node.controlMode === "grid_setpoint") {
385
+ targetNormalizedW = normalizeByConvention(node.gridSetpointW, node.signConvention);
386
+ } else {
387
+ targetNormalizedW = 0;
388
+ }
389
+
390
+ const errorW = lastGridNormalizedW - targetNormalizedW;
391
+
392
+ if (Math.abs(errorW) <= node.deadbandW) {
393
+ commandNormalizedW = lastTotalCommandW;
394
+ reason = "deadband_hold";
395
+ } else {
396
+ commandNormalizedW = lastTotalCommandW + (node.kRel * errorW);
397
+ reason = node.controlMode === "grid_setpoint"
398
+ ? "grid_setpoint_tracking"
399
+ : "zero_feed_in_relative";
400
+ }
401
+
402
+ }
403
+
404
+ }
405
+
406
+ const overrides = {
407
+ socMinPct: node.socMinPct,
408
+ socHystPct: node.socHystPct
409
+ };
410
+
411
+ const primaryPayload = buildPrimaryPayload(
412
+ denormalizeByConvention(commandNormalizedW, node.signConvention),
413
+ !bessOff,
414
+ overrides
415
+ );
416
+
417
+ const primaryMsg = {
418
+ topic: node.outputTopic,
419
+ payload: primaryPayload
420
+ };
421
+
422
+ if (msg._msgid) {
423
+ primaryMsg._msgid = msg._msgid;
424
+ }
425
+
426
+ const debugMsg = {
427
+ topic: node.outputTopic + "/debug",
428
+ payload: {
429
+ control_mode: node.controlMode,
430
+ zero_feed_in_mode: node.zeroFeedInMode,
431
+ sign_convention: node.signConvention,
432
+ grid_reference_raw_w: updatedGrid ? gridReference : null,
433
+ grid_reference_normalized_w: updatedGrid ? normalizeByConvention(gridReference, node.signConvention) : lastGridNormalizedW,
434
+ grid_reference_timestamp_ms: lastGridTs,
435
+ phase_power_raw_w: {
436
+ l1: lastPhaseRawW.l1,
437
+ l2: lastPhaseRawW.l2,
438
+ l3: lastPhaseRawW.l3
439
+ },
440
+ phase_power_normalized_w: {
441
+ l1: lastPhaseNormalizedW.l1,
442
+ l2: lastPhaseNormalizedW.l2,
443
+ l3: lastPhaseNormalizedW.l3
444
+ },
445
+ k_rel: node.kRel,
446
+ deadband_w: node.deadbandW,
447
+ total_command_normalized_w: Math.round(commandNormalizedW || 0),
448
+ total_command_raw_w: Math.round(primaryPayload.power_w || 0),
449
+ external_control: primaryPayload.external_control,
450
+ bess_off: bessOff,
451
+ reason: reason,
452
+ valid_measurement: validMeasurement
453
+ }
454
+ };
455
+
456
+ if (msg._msgid) {
457
+ debugMsg._msgid = msg._msgid;
458
+ }
459
+
460
+ lastTotalCommandW = commandNormalizedW;
461
+ refreshStatus(reason, primaryPayload.power_w);
462
+
463
+ if (node.enableDebugOutput) {
464
+ send([primaryMsg, debugMsg]);
465
+ } else {
466
+ send(primaryMsg);
467
+ }
468
+
469
+ if (done) {
470
+ done();
471
+ }
472
+
473
+ } catch (error) {
474
+
475
+ node.status({
476
+ fill: "red",
477
+ shape: "ring",
478
+ text: "error"
479
+ });
480
+
481
+ if (done) {
482
+ done(error);
483
+ } else {
484
+ node.error(error, msg);
485
+ }
486
+
487
+ }
488
+
489
+ });
490
+
491
+ if (node.staleTimeoutMs > 0 && node.controlMode !== "nominal_power") {
492
+ watchdogTimer = setInterval(function () {
493
+ const nowMs = Date.now();
494
+ if (bessOff || watchdogSafeEmitted || !Number.isFinite(lastMeasurementReceivedAtMs) || (nowMs - lastMeasurementReceivedAtMs) <= node.staleTimeoutMs) {
495
+ return;
496
+ }
497
+
498
+ watchdogSafeEmitted = true;
499
+ lastTotalCommandW = 0;
500
+ const primaryMsg = {
501
+ topic: node.outputTopic,
502
+ payload: buildPrimaryPayload(0, false, {
503
+ socMinPct: node.socMinPct,
504
+ socHystPct: node.socHystPct
505
+ })
506
+ };
507
+ node.status({ fill: "yellow", shape: "ring", text: "watchdog stale | total 0W" });
508
+
509
+ if (node.enableDebugOutput) {
510
+ node.send([primaryMsg, {
511
+ topic: node.outputTopic + "/debug",
512
+ payload: {
513
+ reason: "watchdog_stale",
514
+ valid_measurement: false,
515
+ measurement_age_ms: nowMs - lastMeasurementReceivedAtMs,
516
+ total_command_raw_w: 0,
517
+ external_control: false
518
+ }
519
+ }]);
520
+ } else {
521
+ node.send(primaryMsg);
522
+ }
523
+ }, Math.max(250, Math.min(node.staleTimeoutMs, 1000)));
524
+ }
525
+
526
+ node.on("close", function (done) {
527
+ if (watchdogTimer) {
528
+ clearInterval(watchdogTimer);
529
+ watchdogTimer = null;
530
+ }
531
+ if (done) done();
532
+ });
533
+
534
+
535
+ });
536
+ }
537
+
538
+
539
+ RED.nodes.registerType("bess-control", BessControlNode);
540
+
541
+ };
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+
3
+
4
+ function positiveOrZero(value) {
5
+
6
+ const n = Number(value);
7
+
8
+ return Number.isFinite(n) && n > 0 ? n : 0;
9
+
10
+ }
11
+
12
+
13
+ function proportionalAllocation(total, entries) {
14
+
15
+ const result = {};
16
+
17
+
18
+ (entries || []).forEach(function (entry) {
19
+
20
+ result[entry.id] = 0;
21
+
22
+ });
23
+
24
+
25
+ if (!entries || !entries.length || !Number.isFinite(total) || total === 0) {
26
+
27
+ return result;
28
+
29
+ }
30
+
31
+
32
+ const positive = total > 0;
33
+
34
+ const requested = Math.abs(total);
35
+
36
+ let remaining = requested;
37
+
38
+ let active = entries.map(function (entry) {
39
+
40
+ return {
41
+
42
+ id: entry.id,
43
+
44
+ weight: positiveOrZero(entry.weight),
45
+
46
+ cap: Math.max(0, Number(entry.capW || 0))
47
+
48
+ };
49
+
50
+ }).filter(function (entry) {
51
+
52
+ return entry.cap > 0;
53
+
54
+ });
55
+
56
+
57
+ let pass = 0;
58
+
59
+ while (remaining > 0.5 && active.length > 0 && pass < 20) {
60
+
61
+ let totalWeight = active.reduce(function (sum, entry) {
62
+
63
+ return sum + (entry.weight > 0 ? entry.weight : 1);
64
+
65
+ }, 0);
66
+
67
+
68
+ if (totalWeight <= 0) {
69
+
70
+ totalWeight = active.length;
71
+
72
+ }
73
+
74
+
75
+ let allocatedThisPass = 0;
76
+
77
+ active.forEach(function (entry) {
78
+
79
+ const wanted = remaining * ((entry.weight > 0 ? entry.weight : 1) / totalWeight);
80
+
81
+ const current = Math.abs(result[entry.id] || 0);
82
+
83
+ const room = Math.max(0, entry.cap - current);
84
+
85
+ const delta = Math.min(room, wanted);
86
+
87
+
88
+ result[entry.id] = positive
89
+
90
+ ? (result[entry.id] || 0) + delta
91
+
92
+ : (result[entry.id] || 0) - delta;
93
+
94
+
95
+ allocatedThisPass += delta;
96
+
97
+ });
98
+
99
+
100
+ if (allocatedThisPass <= 0.001) {
101
+
102
+ break;
103
+
104
+ }
105
+
106
+
107
+ remaining -= allocatedThisPass;
108
+
109
+ active = active.filter(function (entry) {
110
+
111
+ return Math.abs(result[entry.id] || 0) < entry.cap - 0.001;
112
+
113
+ });
114
+
115
+ pass += 1;
116
+
117
+ }
118
+
119
+
120
+ Object.keys(result).forEach(function (id) {
121
+
122
+ result[id] = Math.round(result[id]);
123
+
124
+ });
125
+
126
+
127
+ return result;
128
+
129
+ }
130
+
131
+
132
+ function sumAllocation(map) {
133
+
134
+ return Object.keys(map || {}).reduce(function (sum, key) {
135
+
136
+ return sum + Number(map[key] || 0);
137
+
138
+ }, 0);
139
+
140
+ }
141
+
142
+
143
+ module.exports = {
144
+
145
+ proportionalAllocation,
146
+
147
+ sumAllocation
148
+
149
+ };