@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,371 @@
1
+
2
+ "use strict";
3
+
4
+
5
+ const DEFAULT_PROFILE_ID = "umg604pro";
6
+
7
+
8
+ const UMG604_PRO = {
9
+
10
+ id: "umg604pro",
11
+
12
+ label: "UMG 604 Pro",
13
+
14
+ deviceName: "Janitza UMG 604 Pro",
15
+
16
+ startAddress: 19000,
17
+
18
+ registerCount: 122,
19
+
20
+ extendedRegisterFields: {
21
+
22
+ "summary.power_quality.k_factor.l1": { address: 1309, type: "float" },
23
+
24
+ "summary.power_quality.k_factor.l2": { address: 1311, type: "float" },
25
+
26
+ "summary.power_quality.k_factor.l3": { address: 1313, type: "float" },
27
+
28
+ "summary.power_quality.k_factor.l4": { address: 1315, type: "float" },
29
+
30
+ "summary.extended.voltage.l4_n_v": { address: 1323, type: "float" },
31
+
32
+ "summary.extended.current.l4_a": { address: 1331, type: "float" },
33
+
34
+ "summary.extended.power.l4_w": { address: 1339, type: "float" },
35
+
36
+ "summary.extended.reactive.l4_var": { address: 1347, type: "float" },
37
+
38
+ "summary.extended.apparent.l4_va": { address: 1355, type: "float" },
39
+
40
+ "summary.extended.current.sum_l1_l4_a": { address: 1365, type: "float" },
41
+
42
+ "summary.extended.apparent.total_l1_l4_va": { address: 1375, type: "float" },
43
+
44
+ "summary.extended.power.total_l1_l4_w": { address: 1377, type: "float" },
45
+
46
+ "summary.extended.reactive.total_l1_l4_var": { address: 1379, type: "float" },
47
+
48
+ "summary.extended.power_factor.total_l1_l4": { address: 1381, type: "float" },
49
+
50
+ "summary.power_quality.voltage.avg_l_n_v": { address: 19630, type: "float" },
51
+
52
+ "summary.power_quality.voltage.avg_l_l_v": { address: 19632, type: "float" },
53
+
54
+ "summary.power_quality.current.avg_a": { address: 19634, type: "float" },
55
+
56
+ "summary.power_quality.power_factor.avg": { address: 19636, type: "float" },
57
+
58
+ "summary.power_quality.current.tdd_l1_pct": { address: 19638, type: "float" },
59
+
60
+ "summary.power_quality.current.tdd_l2_pct": { address: 19640, type: "float" },
61
+
62
+ "summary.power_quality.current.tdd_l3_pct": { address: 19642, type: "float" },
63
+
64
+ "summary.power_quality.current.tdd_l4_pct": { address: 19644, type: "float" },
65
+
66
+ "summary.power_quality.current.unsymmetry_pct": { address: 19646, type: "float" },
67
+
68
+ "summary.power_quality.crest_factor.voltage_l1": { address: 19648, type: "float" },
69
+
70
+ "summary.power_quality.crest_factor.voltage_l2": { address: 19650, type: "float" },
71
+
72
+ "summary.power_quality.crest_factor.voltage_l3": { address: 19652, type: "float" },
73
+
74
+ "summary.power_quality.crest_factor.current_l1": { address: 19660, type: "float" },
75
+
76
+ "summary.power_quality.crest_factor.current_l2": { address: 19662, type: "float" },
77
+
78
+ "summary.power_quality.crest_factor.current_l3": { address: 19664, type: "float" },
79
+
80
+ "summary.power_quality.distortion_reactive.l1_var": { address: 19668, type: "float" },
81
+
82
+ "summary.power_quality.distortion_reactive.l2_var": { address: 19670, type: "float" },
83
+
84
+ "summary.power_quality.distortion_reactive.l3_var": { address: 19672, type: "float" },
85
+
86
+ "summary.max.voltage.l1_n_v": { address: 4509, type: "float" },
87
+
88
+ "summary.max.voltage.l2_n_v": { address: 4511, type: "float" },
89
+
90
+ "summary.max.voltage.l3_n_v": { address: 4513, type: "float" },
91
+
92
+ "summary.max.current.l1_a": { address: 4517, type: "float" },
93
+
94
+ "summary.max.current.l2_a": { address: 4519, type: "float" },
95
+
96
+ "summary.max.current.l3_a": { address: 4521, type: "float" },
97
+
98
+ "summary.max.power.total_w": { address: 4561, type: "float" },
99
+
100
+ "summary.max.reactive.total_var": { address: 4563, type: "float" },
101
+
102
+ "summary.max.frequency_hz": { address: 4571, type: "float" },
103
+
104
+ "summary.energy.tariff.active": { address: 9805, type: "short" },
105
+
106
+ "summary.energy.tariff.reactive": { address: 9806, type: "short" },
107
+
108
+ "summary.energy.detail.apparent_l1_kvah": { address: 9807, type: "float", scale: 0.001 },
109
+
110
+ "summary.energy.detail.apparent_l2_kvah": { address: 9809, type: "float", scale: 0.001 },
111
+
112
+ "summary.energy.detail.apparent_l3_kvah": { address: 9811, type: "float", scale: 0.001 },
113
+
114
+ "summary.energy.detail.active_l1_kwh": { address: 9819, type: "float", scale: 0.001 },
115
+
116
+ "summary.energy.detail.active_l2_kwh": { address: 9821, type: "float", scale: 0.001 },
117
+
118
+ "summary.energy.detail.active_l3_kwh": { address: 9823, type: "float", scale: 0.001 },
119
+
120
+ "summary.energy.detail.active_total_kwh": { address: 9827, type: "float", scale: 0.001 },
121
+
122
+ "summary.energy.detail.consumed_total_kwh": { address: 9851, type: "float", scale: 0.001 },
123
+
124
+ "summary.energy.detail.supplied_total_kwh": { address: 9863, type: "float", scale: 0.001 },
125
+
126
+ "summary.energy.detail.reactive_inductive_total_kvarh": { address: 9923, type: "float", scale: 0.001 },
127
+
128
+ "summary.energy.detail.reactive_capacitive_total_kvarh": { address: 9935, type: "float", scale: 0.001 },
129
+
130
+ "summary.digital_inputs.pulse_counter_1": { address: 9963, type: "float" },
131
+
132
+ "summary.digital_inputs.pulse_counter_2": { address: 9965, type: "float" },
133
+
134
+ "summary.device.current_transformer.primary_l1_a": { address: 10032, type: "float" },
135
+
136
+ "summary.device.current_transformer.primary_l2_a": { address: 10034, type: "float" },
137
+
138
+ "summary.device.current_transformer.primary_l3_a": { address: 10036, type: "float" },
139
+
140
+ "summary.device.current_transformer.secondary_l1_a": { address: 10040, type: "float" },
141
+
142
+ "summary.device.voltage_transformer.primary_l1_v": { address: 10048, type: "float" },
143
+
144
+ "summary.device.voltage_transformer.secondary_l1_v": { address: 10056, type: "float" },
145
+
146
+ "summary.device.serial_number": { address: 10176, type: "uint" },
147
+
148
+ "summary.device.production_number": { address: 10178, type: "uint" },
149
+
150
+ "summary.device.modbus_address": { address: 10180, type: "int" },
151
+
152
+ "summary.device.ip_address": { address: 10190, type: "ip" },
153
+
154
+ "summary.device.ip_mask": { address: 10192, type: "ip" },
155
+
156
+ "summary.device.ip_gateway": { address: 10194, type: "ip" },
157
+
158
+ "summary.device.temperature_sensor_type": { address: 10200, type: "int" },
159
+
160
+ "summary.events.current_threshold_l1_pct": { address: 10268, type: "float" },
161
+
162
+ "summary.events.voltage_max_l1_pct": { address: 10276, type: "float" },
163
+
164
+ "summary.events.voltage_min_l1_pct": { address: 10284, type: "float" },
165
+
166
+ "summary.digital_outputs.event_mask_1": { address: 10426, type: "int" },
167
+
168
+ "summary.digital_outputs.event_mask_2": { address: 10428, type: "int" },
169
+
170
+ "summary.temperature.offset_c": { address: 12411, type: "float" },
171
+
172
+ "summary.quick.frequency_hz": { address: 14131, type: "float" },
173
+
174
+ "summary.quick.voltage.l1_n_v": { address: 14133, type: "float" },
175
+
176
+ "summary.quick.voltage.l2_n_v": { address: 14135, type: "float" },
177
+
178
+ "summary.quick.voltage.l3_n_v": { address: 14137, type: "float" },
179
+
180
+ "summary.quick.current.l1_a": { address: 14141, type: "float" },
181
+
182
+ "summary.quick.current.l2_a": { address: 14143, type: "float" },
183
+
184
+ "summary.quick.current.l3_a": { address: 14145, type: "float" },
185
+
186
+ "summary.quick.power.l1_w": { address: 14149, type: "float" },
187
+
188
+ "summary.quick.power.l2_w": { address: 14151, type: "float" },
189
+
190
+ "summary.quick.power.l3_w": { address: 14153, type: "float" },
191
+
192
+ "summary.quick.reactive.l1_var": { address: 14157, type: "float" },
193
+
194
+ "summary.quick.reactive.l2_var": { address: 14159, type: "float" },
195
+
196
+ "summary.quick.reactive.l3_var": { address: 14161, type: "float" }
197
+
198
+ }
199
+
200
+ };
201
+
202
+
203
+ const PROFILES = {
204
+
205
+ [UMG604_PRO.id]: UMG604_PRO
206
+
207
+ };
208
+
209
+
210
+ function getProfile(model) {
211
+
212
+ return PROFILES[model] || PROFILES[DEFAULT_PROFILE_ID];
213
+
214
+ }
215
+
216
+
217
+ function getRegisterSize(type) {
218
+
219
+ if (type === "float" || type === "int" || type === "uint" || type === "ip") {
220
+
221
+ return 2;
222
+
223
+ }
224
+
225
+ return 1;
226
+
227
+ }
228
+
229
+
230
+ function readValueFromRegisterData(data, blockStartAddress, definition) {
231
+
232
+ const offset = (definition.address - blockStartAddress) * 2;
233
+
234
+ let value;
235
+
236
+
237
+ if (definition.type === "float") {
238
+
239
+ value = data.readFloatBE(offset);
240
+
241
+ } else if (definition.type === "uint" || definition.type === "ip") {
242
+
243
+ value = data.readUInt32BE(offset);
244
+
245
+ } else if (definition.type === "int") {
246
+
247
+ value = data.readInt32BE(offset);
248
+
249
+ } else if (definition.type === "short") {
250
+
251
+ value = data.readInt16BE(offset);
252
+
253
+ } else {
254
+
255
+ value = data.readUInt16BE(offset);
256
+
257
+ }
258
+
259
+
260
+ if (definition.type === "ip") {
261
+
262
+ return [
263
+
264
+ (value >>> 24) & 255,
265
+
266
+ (value >>> 16) & 255,
267
+
268
+ (value >>> 8) & 255,
269
+
270
+ value & 255
271
+
272
+ ].join(".");
273
+
274
+ }
275
+
276
+
277
+ if (definition.scale) {
278
+
279
+ return value * definition.scale;
280
+
281
+ }
282
+
283
+
284
+ return value;
285
+
286
+ }
287
+
288
+
289
+ function buildExtendedReadRanges(fields, profile, normalizeFields) {
290
+
291
+ const extendedFields = profile.extendedRegisterFields || {};
292
+
293
+ const definitions = normalizeFields(fields, true)
294
+
295
+ .filter(function (field) { return !!extendedFields[field]; })
296
+
297
+ .map(function (field) {
298
+
299
+ const definition = extendedFields[field];
300
+
301
+ return {
302
+
303
+ field: field,
304
+
305
+ address: definition.address,
306
+
307
+ end: definition.address + getRegisterSize(definition.type),
308
+
309
+ definition: definition
310
+
311
+ };
312
+
313
+ })
314
+
315
+ .sort(function (a, b) { return a.address - b.address; });
316
+
317
+
318
+ const ranges = [];
319
+
320
+
321
+ definitions.forEach(function (item) {
322
+
323
+ let range = ranges.length ? ranges[ranges.length - 1] : null;
324
+
325
+ if (!range || item.end - range.start > 120) {
326
+
327
+ range = {
328
+
329
+ start: item.address,
330
+
331
+ end: item.end,
332
+
333
+ items: []
334
+
335
+ };
336
+
337
+ ranges.push(range);
338
+
339
+ }
340
+
341
+ range.end = Math.max(range.end, item.end);
342
+
343
+ range.items.push(item);
344
+
345
+ });
346
+
347
+
348
+ return ranges.map(function (range) {
349
+
350
+ range.count = range.end - range.start;
351
+
352
+ return range;
353
+
354
+ });
355
+
356
+ }
357
+
358
+
359
+ module.exports = {
360
+
361
+ DEFAULT_PROFILE_ID,
362
+
363
+ PROFILES,
364
+
365
+ getProfile,
366
+
367
+ buildExtendedReadRanges,
368
+
369
+ readValueFromRegisterData
370
+
371
+ };
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+
3
+ const RAMP_TYPE_BY_CODE = {
4
+ 0: "none",
5
+ 1: "time",
6
+ 2: "gradient_pct",
7
+ 3: "gradient_w"
8
+ };
9
+
10
+ const RAMP_CODE_BY_TYPE = {
11
+ none: 0,
12
+ time: 1,
13
+ gradient_pct: 2,
14
+ gradient_w: 3
15
+ };
16
+
17
+ function clamp(value, min, max) {
18
+ if (!Number.isFinite(value)) return min;
19
+ return Math.min(Math.max(value, min), max);
20
+ }
21
+
22
+ function toSigned16(word) {
23
+ const n = Number(word) & 0xFFFF;
24
+ return n > 0x7FFF ? n - 0x10000 : n;
25
+ }
26
+
27
+ function toSigned32(high, low) {
28
+ const unsigned = (((Number(high) & 0xFFFF) * 0x10000) + (Number(low) & 0xFFFF)) >>> 0;
29
+ return unsigned > 0x7FFFFFFF ? unsigned - 0x100000000 : unsigned;
30
+ }
31
+
32
+ function fromSigned32(value) {
33
+ const n = Math.round(Number(value) || 0);
34
+ const unsigned = n < 0 ? (0x100000000 + n) : n;
35
+ return [
36
+ (unsigned >>> 16) & 0xFFFF,
37
+ unsigned & 0xFFFF
38
+ ];
39
+ }
40
+
41
+ function normalizeAddress(address) {
42
+ const n = Number(address);
43
+ if (!Number.isFinite(n)) return -1;
44
+ if (n >= 40001) return n - 40001;
45
+ return n;
46
+ }
47
+
48
+ function normalizeRampType(value) {
49
+ if (typeof value === "number") {
50
+ return RAMP_TYPE_BY_CODE[value] || "none";
51
+ }
52
+
53
+ const key = String(value || "").trim().toLowerCase();
54
+ if (key === "gradient_watts" || key === "gradient_vars" || key === "gradient_var") {
55
+ return "gradient_w";
56
+ }
57
+
58
+ return RAMP_CODE_BY_TYPE[key] !== undefined ? key : "none";
59
+ }
60
+
61
+ function createA3TestServer(options) {
62
+ const { createModbusServer } = require("../scada-server/lib/modbus-server");
63
+ const opts = options || {};
64
+ const minNominal = Number.isFinite(opts.minNominal) ? opts.minNominal : 0;
65
+ const maxNominal = Number.isFinite(opts.maxNominal) ? opts.maxNominal : 125000;
66
+ const host = String(opts.host || "0.0.0.0").trim() || "0.0.0.0";
67
+ const port = Math.max(1, Math.round(Number(opts.port) || 1502));
68
+ const unitId = Math.max(0, Math.round(Number(opts.unitId) || 1));
69
+ const state = {
70
+ nominalPower: clamp(Math.round(Number(opts.nominalPower) || 0), minNominal, maxNominal),
71
+ rampType: normalizeRampType(opts.rampType || "none"),
72
+ rampValue: Math.max(0, Math.round(Number(opts.rampValue) || 0)),
73
+ sequence: 0,
74
+ lastWriteTimestampMs: 0
75
+ };
76
+
77
+ function getRegister(address) {
78
+ const nominalWords = fromSigned32(state.nominalPower);
79
+ const rampWords = fromSigned32(state.rampValue);
80
+
81
+ switch (address) {
82
+ case 0:
83
+ return 1;
84
+ case 1:
85
+ return 1;
86
+ case 2:
87
+ return nominalWords[0];
88
+ case 3:
89
+ return nominalWords[1];
90
+ case 4:
91
+ return RAMP_CODE_BY_TYPE[state.rampType] || 0;
92
+ case 5:
93
+ return rampWords[0];
94
+ case 6:
95
+ return rampWords[1];
96
+ case 7:
97
+ return state.sequence & 0xFFFF;
98
+ case 8:
99
+ return Math.floor((state.lastWriteTimestampMs || 0) / 1000) & 0xFFFF;
100
+ default:
101
+ return null;
102
+ }
103
+ }
104
+
105
+ function applyWritableRegisters(registers) {
106
+ if (registers[2] !== undefined || registers[3] !== undefined) {
107
+ const high = registers[2] !== undefined ? registers[2] : getRegister(2);
108
+ const low = registers[3] !== undefined ? registers[3] : getRegister(3);
109
+ state.nominalPower = clamp(toSigned32(high, low), minNominal, maxNominal);
110
+ }
111
+
112
+ if (registers[4] !== undefined) {
113
+ state.rampType = normalizeRampType(Number(registers[4]));
114
+ }
115
+
116
+ if (registers[5] !== undefined || registers[6] !== undefined) {
117
+ const high = registers[5] !== undefined ? registers[5] : getRegister(5);
118
+ const low = registers[6] !== undefined ? registers[6] : getRegister(6);
119
+ state.rampValue = Math.max(0, toSigned32(high, low));
120
+ }
121
+
122
+ state.sequence = (state.sequence + 1) & 0xFFFF;
123
+ state.lastWriteTimestampMs = Date.now();
124
+
125
+ if (typeof opts.onCommand === "function") {
126
+ opts.onCommand(getCommand());
127
+ }
128
+ }
129
+
130
+ function writeRegisters(startAddress, values) {
131
+ const start = normalizeAddress(startAddress);
132
+ if (start < 2 || start > 6 || (start + values.length - 1) > 6) {
133
+ return { exceptionCode: 2, issueCode: "a3_illegal_write_address" };
134
+ }
135
+
136
+ const registers = {};
137
+ values.forEach(function (value, index) {
138
+ registers[start + index] = Number(value) & 0xFFFF;
139
+ });
140
+ applyWritableRegisters(registers);
141
+ return { ok: true };
142
+ }
143
+
144
+ function getCommand() {
145
+ return {
146
+ nominalPower: state.nominalPower,
147
+ rampType: state.rampType,
148
+ rampValue: state.rampValue,
149
+ sequence: state.sequence,
150
+ timestampMs: state.lastWriteTimestampMs || Date.now()
151
+ };
152
+ }
153
+
154
+ const server = createModbusServer({
155
+ host: host,
156
+ port: port,
157
+ unitId: unitId,
158
+ readHoldingRegisters: function (startAddress, quantity) {
159
+ const start = normalizeAddress(startAddress);
160
+ const words = [];
161
+
162
+ for (let i = 0; i < quantity; i++) {
163
+ const word = getRegister(start + i);
164
+ if (word === null) {
165
+ return { exceptionCode: 2, issueCode: "a3_illegal_read_address" };
166
+ }
167
+ words.push(word);
168
+ }
169
+
170
+ return words;
171
+ },
172
+ writeSingleRegister: function (address, value) {
173
+ return writeRegisters(address, [value]);
174
+ },
175
+ writeHoldingRegisters: writeRegisters,
176
+ onError: opts.onError,
177
+ onProtocolIssue: opts.onProtocolIssue,
178
+ onClientChange: opts.onClientChange,
179
+ onWrite: opts.onWrite
180
+ });
181
+
182
+ return {
183
+ start: server.start,
184
+ stop: server.stop,
185
+ address: server.address,
186
+ getCommand: getCommand,
187
+ getState: function () {
188
+ return Object.assign({}, state);
189
+ }
190
+ };
191
+ }
192
+
193
+ module.exports = {
194
+ createA3TestServer,
195
+ normalizeRampType,
196
+ toSigned16,
197
+ toSigned32,
198
+ fromSigned32
199
+ };