@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,520 @@
1
+ "use strict";
2
+
3
+ const { runLicensedNode } = require("../lib/licensing/node-guard");
4
+
5
+ const { createModbusTcpServer } = require("./lib/modbus-tcp-server");
6
+ const { createFroniusRegisterMap, DEFAULT_MEASUREMENTS } = require("./lib/fronius-register-map");
7
+ const {
8
+ integerInRange,
9
+ cleanText,
10
+ canonicalKey,
11
+ finiteMeasurement,
12
+ getByPath,
13
+ normalizePayload
14
+ } = require("./lib/validation");
15
+ const {
16
+ normalizeMeterRole,
17
+ gridReferenceFromSnapshot,
18
+ deriveSecondaryMeasurements,
19
+ normalizedEnergyState,
20
+ integrateDirectionalEnergy
21
+ } = require("./lib/virtual-meter");
22
+
23
+ module.exports = function (RED) {
24
+ // Nodes on the same bind/port share one listener and are selected by Unit ID.
25
+ // Grid providers are also registered separately, allowing PV/battery nodes
26
+ // bound to other local IPs to reuse the freshest Grid voltage reference.
27
+ const sharedServers = new Map();
28
+ const gridProviders = new Map();
29
+
30
+ function serverKey(bindAddress, port) {
31
+ return bindAddress + ":" + port;
32
+ }
33
+
34
+ function forEachMeter(group, callback) {
35
+ group.meters.forEach(function (meter) {
36
+ try { callback(meter); } catch (error) { meter.onInternalError(error); }
37
+ });
38
+ }
39
+
40
+ function createSharedServerGroup(settings) {
41
+ const group = {
42
+ key: serverKey(settings.bindAddress, settings.port),
43
+ bindAddress: settings.bindAddress,
44
+ port: settings.port,
45
+ maxClients: settings.maxClients,
46
+ clientIdleTimeout: settings.clientIdleTimeout,
47
+ meters: new Map(),
48
+ listening: false,
49
+ server: null,
50
+ startPromise: null
51
+ };
52
+
53
+ group.server = createModbusTcpServer({
54
+ bindAddress: group.bindAddress,
55
+ port: group.port,
56
+ maxClients: group.maxClients,
57
+ clientIdleTimeout: group.clientIdleTimeout,
58
+ allowWrites: true,
59
+ isUnitIdAllowed: function (unitId) {
60
+ return group.meters.has(unitId);
61
+ },
62
+ readRegisters: function (startAddress, quantity, functionCode, unitId) {
63
+ const meter = group.meters.get(unitId);
64
+ return meter ? meter.readRegisters(startAddress, quantity, functionCode) : { exceptionCode: 2 };
65
+ },
66
+ writeRegisters: function (startAddress, values, unitId) {
67
+ const meter = group.meters.get(unitId);
68
+ return meter ? meter.writeRegisters(startAddress, values) : { exceptionCode: 2 };
69
+ },
70
+ onClientConnected: function (info) {
71
+ forEachMeter(group, function (meter) { meter.onClientConnected(info); });
72
+ },
73
+ onClientDisconnected: function (info) {
74
+ forEachMeter(group, function (meter) { meter.onClientDisconnected(info); });
75
+ },
76
+ onClientRejected: function (info) {
77
+ forEachMeter(group, function (meter) { meter.onClientRejected(info); });
78
+ },
79
+ onRequest: function (info) {
80
+ const meter = group.meters.get(info.unitId);
81
+ if (meter) meter.onRequest(info);
82
+ },
83
+ onProtocolError: function (info) {
84
+ forEachMeter(group, function (meter) { meter.onProtocolError(info); });
85
+ },
86
+ onSocketError: function (error) {
87
+ forEachMeter(group, function (meter) { meter.onSocketError(error); });
88
+ },
89
+ onError: function (error) {
90
+ forEachMeter(group, function (meter) { meter.onServerError(error); });
91
+ }
92
+ });
93
+
94
+ group.startPromise = group.server.start().then(function (address) {
95
+ group.listening = true;
96
+ if (address && address.port) group.port = address.port;
97
+ return address;
98
+ }).catch(function (error) {
99
+ group.listening = false;
100
+ if (sharedServers.get(group.key) === group) sharedServers.delete(group.key);
101
+ throw error;
102
+ });
103
+ return group;
104
+ }
105
+
106
+ function acquireSharedServer(node, meter) {
107
+ const key = serverKey(node.bindAddress, node.port);
108
+ let group = sharedServers.get(key);
109
+ if (!group) {
110
+ group = createSharedServerGroup({
111
+ bindAddress: node.bindAddress,
112
+ port: node.port,
113
+ maxClients: node.maxClients,
114
+ clientIdleTimeout: node.clientIdleTimeout
115
+ });
116
+ sharedServers.set(key, group);
117
+ } else if (group.maxClients !== node.maxClients || group.clientIdleTimeout !== node.clientIdleTimeout) {
118
+ node.warn("fronius-emulator: serverul comun " + key + " foloseste setarile de clienti ale primei instante");
119
+ }
120
+ if (group.meters.has(node.unitId)) {
121
+ throw new Error("Unit ID " + node.unitId + " este deja folosit pe " + key);
122
+ }
123
+ group.meters.set(node.unitId, meter);
124
+ return group;
125
+ }
126
+
127
+ function releaseSharedServer(group, unitId) {
128
+ if (!group) return Promise.resolve();
129
+ group.meters.delete(unitId);
130
+ if (group.meters.size) return Promise.resolve();
131
+ if (sharedServers.get(group.key) === group) sharedServers.delete(group.key);
132
+ return group.startPromise.catch(function () { return null; }).then(function () {
133
+ group.listening = false;
134
+ return group.server.stop();
135
+ });
136
+ }
137
+
138
+ function freshestGridReference(preferredGroup) {
139
+ const preferred = preferredGroup && preferredGroup.meters.get(1);
140
+ const preferredReference = preferred && typeof preferred.getGridReference === "function" ? preferred.getGridReference() : null;
141
+ if (preferredReference) return preferredReference;
142
+
143
+ let freshest = null;
144
+ gridProviders.forEach(function (provider) {
145
+ const reference = provider.getGridReference();
146
+ if (!reference) return;
147
+ if (!freshest || Date.parse(reference.timestamp) > Date.parse(freshest.timestamp)) freshest = reference;
148
+ });
149
+ return freshest;
150
+ }
151
+
152
+ function FroniusEmulatorNode(config) {
153
+ RED.nodes.createNode(this, config);
154
+ const node = this;
155
+
156
+ runLicensedNode(node, RED, config, function startLicensedRuntime() {
157
+ node.name = "Fronius Emulator";
158
+ node.meterRole = normalizeMeterRole(config.meterRole);
159
+ node.bindAddress = cleanText(config.bindAddress, "0.0.0.0", 255);
160
+ node.port = integerInRange(Number(config.port), 1502, 1, 65535);
161
+ node.unitId = node.meterRole === "grid"
162
+ ? 1
163
+ : integerInRange(Number(config.unitId), node.meterRole === "battery" ? 3 : 2, 2, 14);
164
+ node.manufacturer = cleanText(config.manufacturer, "Fronius", 64);
165
+ node.model = cleanText(config.model, "Smart Meter 63A", 64);
166
+ node.serialNumber = cleanText(config.serialNumber, "00000001", 32);
167
+ node.updateTimeout = integerInRange(Number(config.updateTimeout), 10000, 100, 86400000);
168
+ node.staleBehaviour = ["hold-last", "zero-power", "modbus-exception"].includes(config.staleBehaviour) ? config.staleBehaviour : "zero-power";
169
+ node.powerSignConvention = config.powerSignConvention === "export-positive" ? "export-positive" : "import-positive";
170
+ node.allowWrites = !!config.allowWrites;
171
+ node.logRequests = !!config.logRequests;
172
+ node.maxClients = integerInRange(Number(config.maxClients), 5, 1, 50);
173
+ node.clientIdleTimeout = integerInRange(Number(config.clientIdleTimeout), 30000, 500, 3600000);
174
+ node.inputMode = config.inputMode === "topic-values" ? "topic-values" : "payload-object";
175
+ node.contextType = config.contextType === "flow" ? "flow" : "global";
176
+ node.powerVariable = cleanText(
177
+ config.powerVariable || config.globalVariable,
178
+ node.meterRole === "battery" ? "batteryPower" : "pvPower",
179
+ 255
180
+ );
181
+ node.legacyPath = String(config.globalPath || "").trim();
182
+ node.sourcePollInterval = integerInRange(Number(config.sourcePollInterval || config.globalPollInterval), 1000, 100, 3600000);
183
+
184
+ let measurements = Object.assign({}, DEFAULT_MEASUREMENTS);
185
+ let lastUpdate = null;
186
+ let stale = node.meterRole !== "grid";
187
+ let listening = false;
188
+ let closing = false;
189
+ let staleTimer = null;
190
+ let sourceTimer = null;
191
+ let sharedGroup = null;
192
+ let registered = false;
193
+ let inputGridReference = null;
194
+ let energyState = normalizedEnergyState(node.context().get("secondaryEnergy"));
195
+ const warningTimes = new Map();
196
+ const registerMap = createFroniusRegisterMap({
197
+ manufacturer: node.manufacturer,
198
+ model: node.model,
199
+ serialNumber: node.serialNumber,
200
+ unitId: node.unitId,
201
+ // PV/battery keep the source sign; the selectable convention applies to Grid.
202
+ powerSignConvention: node.meterRole === "grid" ? node.powerSignConvention : "import-positive"
203
+ });
204
+
205
+ function effectivePower() {
206
+ if (stale && node.staleBehaviour === "zero-power") return 0;
207
+ if (node.meterRole === "grid" && node.powerSignConvention === "export-positive") return -measurements.power;
208
+ return measurements.power;
209
+ }
210
+
211
+ function roleLabel() {
212
+ if (node.meterRole === "pv") return "PV";
213
+ if (node.meterRole === "battery") return "BAT";
214
+ return "GRID";
215
+ }
216
+
217
+ function clientCount() {
218
+ return sharedGroup ? sharedGroup.server.clientCount() : 0;
219
+ }
220
+
221
+ function refreshStatus(customText) {
222
+ if (customText) {
223
+ node.status({ fill: "yellow", shape: "ring", text: customText });
224
+ return;
225
+ }
226
+ if (!listening) return;
227
+ const clients = clientCount();
228
+ if (stale) {
229
+ node.status({ fill: "yellow", shape: "ring", text: roleLabel() + " U" + node.unitId + " stale | " + Math.round(effectivePower()) + " W" });
230
+ } else if (clients) {
231
+ node.status({ fill: "green", shape: "dot", text: roleLabel() + " U" + node.unitId + " | " + Math.round(effectivePower()) + " W | " + clients + " client" + (clients === 1 ? "" : "i") });
232
+ } else {
233
+ node.status({ fill: "green", shape: "dot", text: roleLabel() + " U" + node.unitId + " | listening " + node.port });
234
+ }
235
+ }
236
+
237
+ function emitEvent(event, details, send) {
238
+ const message = {
239
+ topic: "fronius-emulator/" + (event === "modbus-request" ? "request" : event),
240
+ payload: Object.assign({
241
+ event,
242
+ timestamp: new Date().toISOString(),
243
+ meterRole: node.meterRole,
244
+ unitId: node.unitId
245
+ }, details || {})
246
+ };
247
+ (send || node.send).call(node, message);
248
+ }
249
+
250
+ function throttledWarning(key, text) {
251
+ const now = Date.now();
252
+ if (!warningTimes.has(key) || now - warningTimes.get(key) >= 5000) {
253
+ warningTimes.set(key, now);
254
+ node.warn(text);
255
+ }
256
+ }
257
+
258
+ function scheduleStale() {
259
+ if (staleTimer) clearTimeout(staleTimer);
260
+ staleTimer = setTimeout(function () {
261
+ staleTimer = null;
262
+ stale = true;
263
+ refreshStatus();
264
+ }, node.updateTimeout);
265
+ if (typeof staleTimer.unref === "function") staleTimer.unref();
266
+ }
267
+
268
+ function markUpdated(timestamp) {
269
+ lastUpdate = timestamp || new Date().toISOString();
270
+ stale = false;
271
+ scheduleStale();
272
+ refreshStatus();
273
+ }
274
+
275
+ function snapshot() {
276
+ return {
277
+ listening,
278
+ bindAddress: node.bindAddress,
279
+ port: node.port,
280
+ unitId: node.unitId,
281
+ meterRole: node.meterRole,
282
+ clients: clientCount(),
283
+ stale,
284
+ lastUpdate,
285
+ measurements: Object.assign({}, measurements)
286
+ };
287
+ }
288
+
289
+ function meterRecord() {
290
+ return {
291
+ readRegisters: function (startAddress, quantity) {
292
+ return registerMap.read(startAddress, quantity, { measurements, stale, staleBehaviour: node.staleBehaviour });
293
+ },
294
+ writeRegisters: function () {
295
+ return { exceptionCode: 2 };
296
+ },
297
+ getGridReference: function () {
298
+ return node.meterRole === "grid" ? gridReferenceFromSnapshot(snapshot()) : null;
299
+ },
300
+ onClientConnected: function (info) {
301
+ emitEvent("client-connected", { remoteAddress: info.remoteAddress, clients: info.clients });
302
+ refreshStatus();
303
+ },
304
+ onClientDisconnected: function (info) {
305
+ emitEvent("client-disconnected", { remoteAddress: info.remoteAddress, clients: info.clients });
306
+ refreshStatus();
307
+ },
308
+ onClientRejected: function (info) {
309
+ throttledWarning("max-clients", "fronius-emulator: conexiune refuzata; limita de clienti a fost atinsa (" + (info.remoteAddress || "necunoscut") + ")");
310
+ },
311
+ onRequest: function (info) {
312
+ if (node.logRequests) emitEvent("modbus-request", info);
313
+ },
314
+ onProtocolError: function (info) {
315
+ throttledWarning(info.code, "fronius-emulator: cadru Modbus invalid de la " + info.remoteAddress + " (" + info.code + ")");
316
+ },
317
+ onSocketError: function (error) {
318
+ throttledWarning("socket-" + error.code, "fronius-emulator: eroare client: " + error.message);
319
+ },
320
+ onServerError: function (error) {
321
+ node.status({ fill: "red", shape: "ring", text: "server error" });
322
+ const key = "server-" + (error.code || error.message);
323
+ const now = Date.now();
324
+ if (!warningTimes.has(key) || now - warningTimes.get(key) >= 5000) {
325
+ warningTimes.set(key, now);
326
+ emitEvent("server-error", { code: error.code || null, message: error.message });
327
+ node.error("fronius-emulator: " + error.message);
328
+ }
329
+ },
330
+ onInternalError: function (error) {
331
+ node.error("fronius-emulator: " + error.message);
332
+ }
333
+ };
334
+ }
335
+
336
+ const record = meterRecord();
337
+ if (node.meterRole === "grid") gridProviders.set(node.id, record);
338
+
339
+ function readContextPower() {
340
+ const context = node.context();
341
+ const store = node.contextType === "flow" ? context.flow : context.global;
342
+ if (!store || typeof store.get !== "function") throw new Error("contextul " + node.contextType + " nu este disponibil");
343
+
344
+ let root = store.get(node.powerVariable);
345
+ let selected = node.legacyPath ? getByPath(root, node.legacyPath) : root;
346
+ if (selected === undefined && !node.legacyPath && node.powerVariable.includes(".")) {
347
+ const parts = node.powerVariable.split(".");
348
+ root = store.get(parts.shift());
349
+ selected = getByPath(root, parts.join("."));
350
+ }
351
+ if (selected && typeof selected === "object" && Object.prototype.hasOwnProperty.call(selected, "value")) {
352
+ selected = selected.value;
353
+ }
354
+ return finiteMeasurement("power", selected);
355
+ }
356
+
357
+ function updateInputGridReference(msg) {
358
+ const update = normalizePayload(msg && msg.payload, true, msg);
359
+ const timestamp = update.timestamp || new Date().toISOString();
360
+ delete update.timestamp;
361
+ const reference = gridReferenceFromSnapshot({
362
+ stale: false,
363
+ lastUpdate: timestamp,
364
+ measurements: update
365
+ });
366
+ if (!reference) throw new Error("intrarea Grid nu contine tensiuni L1/L2/L3 si frecventa valide");
367
+ inputGridReference = { reference, receivedAt: Date.now() };
368
+ }
369
+
370
+ function localGridReference() {
371
+ if (!inputGridReference) return null;
372
+ if (Date.now() - inputGridReference.receivedAt > node.updateTimeout) {
373
+ inputGridReference = null;
374
+ return null;
375
+ }
376
+ return inputGridReference.reference;
377
+ }
378
+
379
+ function pollSource() {
380
+ if (closing || node.meterRole === "grid" || !sharedGroup) return;
381
+ try {
382
+ const reference = localGridReference() || freshestGridReference(sharedGroup);
383
+ if (!reference) {
384
+ stale = true;
385
+ refreshStatus("U" + node.unitId + " asteapta Grid U1");
386
+ return;
387
+ }
388
+ const sourcePower = readContextPower();
389
+ const derived = deriveSecondaryMeasurements(node.meterRole, sourcePower, reference, false);
390
+ const now = Date.now();
391
+ const maximumGap = Math.max(node.updateTimeout, node.sourcePollInterval * 5);
392
+ energyState = integrateDirectionalEnergy(energyState, derived.power, now, maximumGap);
393
+ node.context().set("secondaryEnergy", energyState);
394
+ derived.importEnergy = energyState.importEnergy;
395
+ derived.exportEnergy = energyState.exportEnergy;
396
+ measurements = Object.assign({}, measurements, derived);
397
+ markUpdated(new Date(now).toISOString());
398
+ } catch (error) {
399
+ stale = true;
400
+ refreshStatus("U" + node.unitId + " sursa " + node.contextType + " invalida");
401
+ throttledWarning("context-source-" + error.message, "fronius-emulator: " + error.message);
402
+ }
403
+ }
404
+
405
+ function startSourcePolling() {
406
+ if (node.meterRole === "grid" || sourceTimer) return;
407
+ pollSource();
408
+ sourceTimer = setInterval(pollSource, node.sourcePollInterval);
409
+ if (typeof sourceTimer.unref === "function") sourceTimer.unref();
410
+ }
411
+
412
+ node.on("input", function (msg, send, done) {
413
+ const output = typeof send === "function" ? send : node.send.bind(node);
414
+ const finish = typeof done === "function" ? done : function () {};
415
+ try {
416
+ if (msg && msg.command === "status") {
417
+ emitEvent("status", snapshot(), output);
418
+ finish();
419
+ return;
420
+ }
421
+ if (msg && msg.reset === true) {
422
+ measurements = Object.keys(DEFAULT_MEASUREMENTS).reduce(function (result, key) {
423
+ result[key] = 0;
424
+ return result;
425
+ }, {});
426
+ if (node.meterRole === "grid") {
427
+ markUpdated(new Date().toISOString());
428
+ } else {
429
+ energyState = normalizedEnergyState(null);
430
+ node.context().set("secondaryEnergy", energyState);
431
+ inputGridReference = null;
432
+ stale = true;
433
+ lastUpdate = null;
434
+ pollSource();
435
+ }
436
+ } else if (node.meterRole !== "grid") {
437
+ // Secondary power still comes only from context. The wired meter
438
+ // supplies the Grid voltage/frequency reference used for derivation.
439
+ updateInputGridReference(msg);
440
+ pollSource();
441
+ } else if (node.inputMode === "topic-values") {
442
+ const key = canonicalKey(msg && msg.topic);
443
+ if (!key) throw new Error("msg.topic is not a supported measurement");
444
+ measurements = Object.assign({}, measurements, { [key]: finiteMeasurement(key, msg.payload) });
445
+ markUpdated(new Date().toISOString());
446
+ } else {
447
+ // Preserve direct compatibility with Carlo Gavazzi, Phoenix and Janitza.
448
+ const update = normalizePayload(msg && msg.payload, true, msg);
449
+ const updateTimestamp = update.timestamp;
450
+ delete update.timestamp;
451
+ measurements = Object.assign({}, measurements, update);
452
+ markUpdated(updateTimestamp || new Date().toISOString());
453
+ }
454
+ if (msg && msg.ack === true) emitEvent("status", Object.assign({ reason: "ack" }, snapshot()), output);
455
+ finish();
456
+ } catch (error) {
457
+ emitEvent("validation-error", { message: error.message }, output);
458
+ throttledWarning("validation-" + error.message, "fronius-emulator: " + error.message);
459
+ finish(error);
460
+ }
461
+ });
462
+
463
+ node.status({ fill: "grey", shape: "ring", text: "starting" });
464
+ try {
465
+ sharedGroup = acquireSharedServer(node, record);
466
+ registered = true;
467
+ sharedGroup.startPromise.then(function (address) {
468
+ if (closing) return null;
469
+ listening = true;
470
+ if (address && address.port) node.port = address.port;
471
+ if (node.meterRole === "grid") scheduleStale();
472
+ startSourcePolling();
473
+ refreshStatus();
474
+ emitEvent("server-started", {
475
+ bindAddress: node.bindAddress,
476
+ port: node.port,
477
+ unitId: node.unitId,
478
+ sharedMeters: sharedGroup.meters.size
479
+ });
480
+ return null;
481
+ }).catch(function (error) {
482
+ listening = false;
483
+ const portInUse = error && error.code === "EADDRINUSE";
484
+ node.status({ fill: "red", shape: "ring", text: portInUse ? "port in use" : "server error" });
485
+ let message = error.message;
486
+ if (error.code === "EACCES") message += "; foloseste portul 1502 sau configureaza un proxy/capabilitati OS";
487
+ emitEvent("server-error", { code: error.code || null, message });
488
+ node.error("fronius-emulator: " + message);
489
+ });
490
+ } catch (error) {
491
+ node.status({ fill: "red", shape: "ring", text: "duplicate Unit ID" });
492
+ node.error("fronius-emulator: " + error.message);
493
+ }
494
+
495
+ node.on("close", function (removed, done) {
496
+ const callback = typeof removed === "function" ? removed : (typeof done === "function" ? done : function () {});
497
+ closing = true;
498
+ listening = false;
499
+ if (node.meterRole === "grid") gridProviders.delete(node.id);
500
+ if (staleTimer) clearTimeout(staleTimer);
501
+ staleTimer = null;
502
+ if (sourceTimer) clearInterval(sourceTimer);
503
+ sourceTimer = null;
504
+ const release = registered ? releaseSharedServer(sharedGroup, node.unitId) : Promise.resolve();
505
+ registered = false;
506
+ release.then(function () {
507
+ node.status({ fill: "grey", shape: "ring", text: "stopped" });
508
+ emitEvent("server-stopped", { bindAddress: node.bindAddress, port: node.port, unitId: node.unitId });
509
+ callback();
510
+ }).catch(function (error) {
511
+ node.error("fronius-emulator: eroare la oprire: " + error.message);
512
+ callback();
513
+ });
514
+ });
515
+
516
+ });
517
+ }
518
+
519
+ RED.nodes.registerType("fronius-emulator", FroniusEmulatorNode);
520
+ };
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+
3
+ /** Encode an ASCII SunSpec string as two characters per 16-bit register. */
4
+ function stringToRegisters(value, registerCount) {
5
+ const count = Number(registerCount);
6
+ if (!Number.isInteger(count) || count < 0 || count > 32768) {
7
+ throw new RangeError("registerCount must be an integer between 0 and 32768");
8
+ }
9
+ const source = String(value == null ? "" : value);
10
+ let ascii = "";
11
+ for (const character of source) {
12
+ const code = character.codePointAt(0);
13
+ ascii += code >= 0x20 && code <= 0x7e ? character : "?";
14
+ }
15
+ const bytes = Buffer.alloc(count * 2);
16
+ Buffer.from(ascii, "ascii").copy(bytes, 0, 0, bytes.length);
17
+ const words = [];
18
+ for (let offset = 0; offset < bytes.length; offset += 2) {
19
+ words.push(bytes.readUInt16BE(offset));
20
+ }
21
+ return words;
22
+ }
23
+
24
+ /** Decode an array of SunSpec string registers, stripping trailing NUL bytes. */
25
+ function registersToString(registers) {
26
+ if (!Array.isArray(registers)) throw new TypeError("registers must be an array");
27
+ const buffer = Buffer.alloc(registers.length * 2);
28
+ registers.forEach(function (word, index) {
29
+ if (!Number.isInteger(word) || word < 0 || word > 0xffff) {
30
+ throw new RangeError("register value is outside uint16");
31
+ }
32
+ buffer.writeUInt16BE(word, index * 2);
33
+ });
34
+ return buffer.toString("ascii").replace(/\0+$/, "");
35
+ }
36
+
37
+ /** Encode a finite number as IEEE-754 float32, big-endian, high word first (ABCD). */
38
+ function float32ToRegisters(value) {
39
+ if (typeof value !== "number" || !Number.isFinite(value)) {
40
+ throw new TypeError("float32 value must be finite");
41
+ }
42
+ const buffer = Buffer.allocUnsafe(4);
43
+ buffer.writeFloatBE(value, 0);
44
+ return [buffer.readUInt16BE(0), buffer.readUInt16BE(2)];
45
+ }
46
+
47
+ /** Decode two high-word-first registers as an IEEE-754 float32. */
48
+ function registersToFloat32(highWord, lowWord) {
49
+ if (![highWord, lowWord].every(function (word) {
50
+ return Number.isInteger(word) && word >= 0 && word <= 0xffff;
51
+ })) {
52
+ throw new RangeError("float32 registers must be uint16 values");
53
+ }
54
+ const buffer = Buffer.allocUnsafe(4);
55
+ buffer.writeUInt16BE(highWord, 0);
56
+ buffer.writeUInt16BE(lowWord, 2);
57
+ return buffer.readFloatBE(0);
58
+ }
59
+
60
+ module.exports = {
61
+ stringToRegisters,
62
+ registersToString,
63
+ float32ToRegisters,
64
+ registersToFloat32
65
+ };