@enyo-energy/sunspec-sdk 0.0.1

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.
@@ -0,0 +1,715 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SunspecModbusClient = void 0;
4
+ const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
5
+ const EnergyAppModbusConnectionHealth_js_1 = require("@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusConnectionHealth.js");
6
+ const EnergyAppModbusFaultTolerantReader_js_1 = require("@enyo-energy/energy-app-sdk/dist/implementations/modbus/EnergyAppModbusFaultTolerantReader.js");
7
+ class SunspecModbusClient {
8
+ energyApp;
9
+ modbusClient = null;
10
+ discoveredModels = new Map();
11
+ scaleFactors = {};
12
+ connected = false;
13
+ baseAddress = 40001;
14
+ connectionHealth;
15
+ faultTolerantReader = null;
16
+ constructor(energyApp) {
17
+ this.energyApp = energyApp;
18
+ this.connectionHealth = new EnergyAppModbusConnectionHealth_js_1.EnergyAppModbusConnectionHealth();
19
+ }
20
+ /**
21
+ * Connect to Modbus device
22
+ */
23
+ async connect(host, port = 502, unitId = 1) {
24
+ if (this.connected) {
25
+ await this.disconnect();
26
+ }
27
+ this.modbusClient = await this.energyApp.useModbus().connect({
28
+ host,
29
+ port,
30
+ unitId,
31
+ timeout: 5000
32
+ });
33
+ // Create fault-tolerant reader with connection health monitoring
34
+ if (this.modbusClient) {
35
+ this.faultTolerantReader = new EnergyAppModbusFaultTolerantReader_js_1.EnergyAppModbusFaultTolerantReader(this.modbusClient, this.connectionHealth);
36
+ }
37
+ this.connected = true;
38
+ this.connectionHealth.recordSuccess();
39
+ console.log(`Connected to Sunspec device at ${host}:${port} unit ${unitId}`);
40
+ }
41
+ /**
42
+ * Disconnect from Modbus device
43
+ */
44
+ async disconnect() {
45
+ if (this.modbusClient && this.connected) {
46
+ await this.modbusClient.disconnect();
47
+ this.modbusClient = null;
48
+ this.faultTolerantReader = null;
49
+ this.connected = false;
50
+ this.discoveredModels.clear();
51
+ this.scaleFactors = {};
52
+ }
53
+ }
54
+ /**
55
+ * Discover all available Sunspec models
56
+ * Scans through the address space starting at 40001
57
+ */
58
+ async discoverModels() {
59
+ if (!this.connected) {
60
+ throw new Error('Not connected to Modbus device');
61
+ }
62
+ this.discoveredModels.clear();
63
+ let currentAddress = this.baseAddress;
64
+ const maxAddress = 50000; // Safety limit
65
+ console.log('Starting Sunspec model discovery...');
66
+ try {
67
+ // First, check for Sunspec identifier "SunS" at 40001
68
+ if (!this.modbusClient) {
69
+ throw new Error('Modbus client not initialized');
70
+ }
71
+ const sunspecId = await this.modbusClient.readRegisterStringValue(40001, 2);
72
+ if (!sunspecId.includes('SunS')) {
73
+ console.warn('Device may not be Sunspec compliant - missing SunS identifier');
74
+ }
75
+ // Start scanning after the SunS identifier
76
+ currentAddress = 40003;
77
+ while (currentAddress < maxAddress) {
78
+ // Read model ID and length
79
+ if (!this.modbusClient) {
80
+ throw new Error('Modbus client not initialized');
81
+ }
82
+ const buffer = await this.modbusClient.readHoldingRegisters(currentAddress, 2);
83
+ const modelData = [buffer.readUInt16BE(0), buffer.readUInt16BE(2)];
84
+ if (!modelData || modelData.length < 2) {
85
+ console.log(`No data at address ${currentAddress}, ending discovery`);
86
+ break;
87
+ }
88
+ const modelId = modelData[0];
89
+ const modelLength = modelData[1];
90
+ // Check for end marker
91
+ if (modelId === 0xFFFF || modelId === 65535) {
92
+ console.log(`Found end marker at address ${currentAddress}`);
93
+ break;
94
+ }
95
+ // Store discovered model
96
+ const model = {
97
+ id: modelId,
98
+ address: currentAddress,
99
+ length: modelLength
100
+ };
101
+ this.discoveredModels.set(modelId, model);
102
+ console.log(`Discovered Model ${modelId} at address ${currentAddress} with length ${modelLength}`);
103
+ // Jump to next model: current address + 2 (header) + model length
104
+ currentAddress = currentAddress + 2 + modelLength;
105
+ }
106
+ }
107
+ catch (error) {
108
+ console.error(`Error during model discovery at address ${currentAddress}: ${error}`);
109
+ }
110
+ console.log(`Discovery complete. Found ${this.discoveredModels.size} models`);
111
+ return this.discoveredModels;
112
+ }
113
+ /**
114
+ * Find a specific model by ID
115
+ */
116
+ findModel(modelId) {
117
+ return this.discoveredModels.get(modelId);
118
+ }
119
+ /**
120
+ * Read a register value and apply scale factor
121
+ */
122
+ async readRegisterWithScaleFactor(valueAddress, scaleFactorAddress, quantity = 1) {
123
+ if (!this.connected) {
124
+ throw new Error('Not connected to Modbus device');
125
+ }
126
+ // Read the raw value
127
+ if (!this.modbusClient) {
128
+ throw new Error('Modbus client not initialized');
129
+ }
130
+ const buffer = await this.modbusClient.readHoldingRegisters(valueAddress, quantity);
131
+ let value = buffer.readUInt16BE(0);
132
+ // Apply scale factor if provided
133
+ if (scaleFactorAddress) {
134
+ let scaleFactor = this.scaleFactors[`sf_${scaleFactorAddress}`];
135
+ if (scaleFactor === undefined) {
136
+ if (!this.modbusClient) {
137
+ throw new Error('Modbus client not initialized');
138
+ }
139
+ const sfBuffer = await this.modbusClient.readHoldingRegisters(scaleFactorAddress, 1);
140
+ // Scale factors are signed int16
141
+ scaleFactor = this.convertToSigned16(sfBuffer.readUInt16BE(0));
142
+ this.scaleFactors[`sf_${scaleFactorAddress}`] = scaleFactor;
143
+ }
144
+ // Apply scale factor: value * 10^scaleFactor
145
+ value = value * Math.pow(10, scaleFactor);
146
+ }
147
+ return value;
148
+ }
149
+ /**
150
+ * Convert unsigned 16-bit value to signed
151
+ */
152
+ convertToSigned16(value) {
153
+ if (value > 32767) {
154
+ return value - 65536;
155
+ }
156
+ return value;
157
+ }
158
+ /**
159
+ * Helper to clean string values by removing null characters
160
+ */
161
+ cleanString(value) {
162
+ return value.replace(/\u0000/g, '').trim();
163
+ }
164
+ /**
165
+ * Helper to read register value(s) using the fault-tolerant reader with data type conversion
166
+ */
167
+ async readRegisterValue(address, quantity = 1, dataType = 'uint16') {
168
+ if (!this.faultTolerantReader) {
169
+ throw new Error('Fault-tolerant reader not initialized');
170
+ }
171
+ try {
172
+ const result = await this.faultTolerantReader.readHoldingRegisters(address, quantity);
173
+ // Check if the read was successful
174
+ if (!result.success || !result.value) {
175
+ throw new Error(`Failed to read register at address ${address}: ${result.error?.message || 'Unknown error'}`);
176
+ }
177
+ const buffer = result.value;
178
+ this.connectionHealth.recordSuccess();
179
+ switch (dataType) {
180
+ case 'string':
181
+ // Convert buffer to string and clean null characters
182
+ let str = '';
183
+ for (let i = 0; i < buffer.length; i += 2) {
184
+ const char1 = buffer[i];
185
+ const char2 = buffer[i + 1];
186
+ if (char1 !== 0)
187
+ str += String.fromCharCode(char1);
188
+ if (char2 !== 0)
189
+ str += String.fromCharCode(char2);
190
+ }
191
+ return this.cleanString(str);
192
+ case 'int16':
193
+ return this.convertToSigned16(buffer.readUInt16BE(0));
194
+ case 'uint32':
195
+ case 'acc32':
196
+ // 32-bit values use 2 registers
197
+ return (buffer.readUInt16BE(0) << 16) | buffer.readUInt16BE(2);
198
+ case 'int32':
199
+ const val = (buffer.readUInt16BE(0) << 16) | buffer.readUInt16BE(2);
200
+ return val > 0x7FFFFFFF ? val - 0x100000000 : val;
201
+ case 'uint16':
202
+ default:
203
+ if (quantity === 1) {
204
+ return buffer.readUInt16BE(0);
205
+ }
206
+ const values = [];
207
+ for (let i = 0; i < quantity; i++) {
208
+ values.push(buffer.readUInt16BE(i * 2));
209
+ }
210
+ return values;
211
+ }
212
+ }
213
+ catch (error) {
214
+ this.connectionHealth.recordFailure(error);
215
+ throw error;
216
+ }
217
+ }
218
+ /**
219
+ * Read inverter data from Model 103 (Three Phase)
220
+ */
221
+ async readInverterData() {
222
+ const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Inverter3Phase);
223
+ if (!model) {
224
+ console.log('Inverter model 103 not found, trying single phase model 101');
225
+ const singlePhaseModel = this.findModel(sunspec_interfaces_js_1.SunspecModelId.InverterSinglePhase);
226
+ if (!singlePhaseModel) {
227
+ console.error('No inverter model found');
228
+ return null;
229
+ }
230
+ return this.readSinglePhaseInverterData(singlePhaseModel);
231
+ }
232
+ const baseAddr = model.address + 2; // Skip ID and Length
233
+ try {
234
+ // Read all scale factors first using fault-tolerant reader
235
+ const scaleFactors = await this.readInverterScaleFactors(baseAddr);
236
+ // Read values using fault-tolerant reader with proper data types
237
+ const data = {
238
+ blockNumber: 103,
239
+ blockAddress: model.address,
240
+ blockLength: model.length,
241
+ // AC Current values - Offsets 2-5
242
+ acCurrent: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 2, 1, 'uint16'), scaleFactors.A_SF),
243
+ phaseACurrent: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 3, 1, 'uint16'), scaleFactors.A_SF),
244
+ phaseBCurrent: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 4, 1, 'uint16'), scaleFactors.A_SF),
245
+ phaseCCurrent: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 5, 1, 'uint16'), scaleFactors.A_SF),
246
+ // Voltage values - Offsets 7-12
247
+ voltageAB: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 7, 1, 'uint16'), scaleFactors.V_SF),
248
+ voltageBC: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 8, 1, 'uint16'), scaleFactors.V_SF),
249
+ voltageCA: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 9, 1, 'uint16'), scaleFactors.V_SF),
250
+ voltageAN: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 10, 1, 'uint16'), scaleFactors.V_SF),
251
+ voltageBN: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 11, 1, 'uint16'), scaleFactors.V_SF),
252
+ voltageCN: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 12, 1, 'uint16'), scaleFactors.V_SF),
253
+ // Power values - Offsets 14, 18, 20, 22
254
+ acPower: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 14, 1, 'int16'), scaleFactors.W_SF),
255
+ apparentPower: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 18, 1, 'uint16'), scaleFactors.VA_SF),
256
+ reactivePower: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 20, 1, 'int16'), scaleFactors.VAr_SF),
257
+ powerFactor: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 22, 1, 'int16'), scaleFactors.PF_SF),
258
+ // Frequency - Offset 16
259
+ frequency: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 16, 1, 'uint16'), scaleFactors.Hz_SF),
260
+ // DC values - Offsets 27, 28, 30
261
+ dcCurrent: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 27, 1, 'uint16'), scaleFactors.DCA_SF),
262
+ dcVoltage: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 28, 1, 'uint16'), scaleFactors.DCV_SF),
263
+ dcPower: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 30, 1, 'int16'), scaleFactors.DCW_SF),
264
+ // Temperature values - Offsets 32, 34, 35, 36
265
+ cabinetTemperature: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 32, 1, 'int16'), scaleFactors.Tmp_SF),
266
+ heatSinkTemperature: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 34, 1, 'int16'), scaleFactors.Tmp_SF),
267
+ transformerTemperature: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 35, 1, 'int16'), scaleFactors.Tmp_SF),
268
+ otherTemperature: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 36, 1, 'int16'), scaleFactors.Tmp_SF),
269
+ // Status values - Offsets 38, 39
270
+ operatingState: await this.readRegisterValue(baseAddr + 38, 1, 'uint16'),
271
+ vendorState: await this.readRegisterValue(baseAddr + 39, 1, 'uint16'),
272
+ // Event bitfields - Offsets 40-51
273
+ events: await this.readRegisterValue(baseAddr + 40, 2, 'uint32'),
274
+ events2: await this.readRegisterValue(baseAddr + 42, 2, 'uint32'),
275
+ vendorEvents1: await this.readRegisterValue(baseAddr + 44, 2, 'uint32'),
276
+ vendorEvents2: await this.readRegisterValue(baseAddr + 46, 2, 'uint32'),
277
+ vendorEvents3: await this.readRegisterValue(baseAddr + 48, 2, 'uint32'),
278
+ vendorEvents4: await this.readRegisterValue(baseAddr + 50, 2, 'uint32')
279
+ };
280
+ // Read AC Energy (32-bit accumulator) - Offset 24-25
281
+ const acEnergy = await this.readRegisterValue(baseAddr + 24, 2, 'acc32');
282
+ data.acEnergy = BigInt(acEnergy) * BigInt(Math.pow(10, scaleFactors.WH_SF));
283
+ return data;
284
+ }
285
+ catch (error) {
286
+ console.error(`Error reading inverter data: ${error}`);
287
+ return null;
288
+ }
289
+ }
290
+ /**
291
+ * Read single phase inverter data (Model 101)
292
+ */
293
+ async readSinglePhaseInverterData(model) {
294
+ // Similar to 3-phase but with fewer phase-specific values
295
+ // Implementation would be similar but simplified
296
+ const baseAddr = model.address + 2;
297
+ try {
298
+ // Simplified implementation for single phase
299
+ return {
300
+ blockNumber: 101,
301
+ voltageAN: await this.readRegisterWithScaleFactor(baseAddr + 7, baseAddr + 13), // PhVphA with V_SF
302
+ acCurrent: await this.readRegisterWithScaleFactor(baseAddr + 2, baseAddr + 6),
303
+ acPower: await this.readRegisterWithScaleFactor(baseAddr + 9, baseAddr + 10),
304
+ frequency: await this.readRegisterWithScaleFactor(baseAddr + 11, baseAddr + 12),
305
+ dcPower: await this.readRegisterWithScaleFactor(baseAddr + 20, baseAddr + 21),
306
+ operatingState: await this.readRegisterValue(baseAddr + 24, 1)
307
+ };
308
+ }
309
+ catch (error) {
310
+ console.error(`Error reading single phase inverter data: ${error}`);
311
+ return null;
312
+ }
313
+ }
314
+ /**
315
+ * Read inverter scale factors
316
+ */
317
+ async readInverterScaleFactors(baseAddr) {
318
+ return {
319
+ A_SF: await this.readRegisterValue(baseAddr + 6, 1, 'int16'), // Offset 6
320
+ V_SF: await this.readRegisterValue(baseAddr + 13, 1, 'int16'), // Offset 13
321
+ W_SF: await this.readRegisterValue(baseAddr + 15, 1, 'int16'), // Offset 15
322
+ Hz_SF: await this.readRegisterValue(baseAddr + 17, 1, 'int16'), // Offset 17
323
+ VA_SF: await this.readRegisterValue(baseAddr + 19, 1, 'int16'), // Offset 19
324
+ VAr_SF: await this.readRegisterValue(baseAddr + 21, 1, 'int16'), // Offset 21
325
+ PF_SF: await this.readRegisterValue(baseAddr + 23, 1, 'int16'), // Offset 23
326
+ WH_SF: await this.readRegisterValue(baseAddr + 26, 1, 'int16'), // Offset 26
327
+ DCA_SF: await this.readRegisterValue(baseAddr + 28, 1, 'int16'), // Offset 28
328
+ DCV_SF: await this.readRegisterValue(baseAddr + 29, 1, 'int16'), // Offset 29
329
+ DCW_SF: await this.readRegisterValue(baseAddr + 31, 1, 'int16'), // Offset 31
330
+ Tmp_SF: await this.readRegisterValue(baseAddr + 36, 1, 'int16') // Offset 36
331
+ };
332
+ }
333
+ /**
334
+ * Apply scale factor to a value
335
+ */
336
+ applyScaleFactor(value, scaleFactor) {
337
+ return value * Math.pow(10, scaleFactor);
338
+ }
339
+ /**
340
+ * Read MPPT data from Model 160
341
+ */
342
+ async readMPPTData(moduleId = 1) {
343
+ const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MPPT);
344
+ if (!model) {
345
+ console.log('MPPT model 160 not found');
346
+ return null;
347
+ }
348
+ const baseAddr = model.address + 2; // Skip ID and Length
349
+ try {
350
+ // MPPT modules are repeating blocks, calculate offset for specific module
351
+ const moduleSize = 26; // Size of each MPPT module based on CSV (offsets 0-25)
352
+ const offset = (moduleId - 1) * moduleSize;
353
+ const moduleAddr = baseAddr + offset;
354
+ // Read scale factors first (offsets 10, 12, 14, 17, 21)
355
+ const scaleFactors = {
356
+ DCA_SF: await this.readRegisterValue(moduleAddr + 10, 1, 'int16'),
357
+ DCV_SF: await this.readRegisterValue(moduleAddr + 12, 1, 'int16'),
358
+ DCW_SF: await this.readRegisterValue(moduleAddr + 14, 1, 'int16'),
359
+ DCWH_SF: await this.readRegisterValue(moduleAddr + 17, 1, 'int16'),
360
+ Tmp_SF: await this.readRegisterValue(moduleAddr + 21, 1, 'int16')
361
+ };
362
+ const data = {
363
+ blockNumber: 160,
364
+ blockAddress: model.address,
365
+ blockLength: model.length,
366
+ id: moduleId,
367
+ // String ID - Offset 1 (8 registers for string)
368
+ stringId: await this.readRegisterValue(moduleAddr + 1, 8, 'string'),
369
+ // DC Current - Offset 9
370
+ dcCurrent: this.applyScaleFactor(await this.readRegisterValue(moduleAddr + 9, 1, 'uint16'), scaleFactors.DCA_SF),
371
+ dcCurrentSF: scaleFactors.DCA_SF,
372
+ // DC Voltage - Offset 11
373
+ dcVoltage: this.applyScaleFactor(await this.readRegisterValue(moduleAddr + 11, 1, 'uint16'), scaleFactors.DCV_SF),
374
+ dcVoltageSF: scaleFactors.DCV_SF,
375
+ // DC Power - Offset 13
376
+ dcPower: this.applyScaleFactor(await this.readRegisterValue(moduleAddr + 13, 1, 'uint16'), scaleFactors.DCW_SF),
377
+ dcPowerSF: scaleFactors.DCW_SF,
378
+ // DC Energy - Offset 15-16 (32-bit accumulator)
379
+ dcEnergy: BigInt(await this.readRegisterValue(moduleAddr + 15, 2, 'acc32')) *
380
+ BigInt(Math.pow(10, scaleFactors.DCWH_SF)),
381
+ dcEnergySF: scaleFactors.DCWH_SF,
382
+ // Timestamp - Offset 18-19 (32-bit)
383
+ timestamp: await this.readRegisterValue(moduleAddr + 18, 2, 'uint32'),
384
+ // Temperature - Offset 20
385
+ temperature: this.applyScaleFactor(await this.readRegisterValue(moduleAddr + 20, 1, 'int16'), scaleFactors.Tmp_SF),
386
+ temperatureSF: scaleFactors.Tmp_SF,
387
+ // Operating State - Offset 22
388
+ operatingState: await this.readRegisterValue(moduleAddr + 22, 1, 'uint16'),
389
+ // Vendor State - Offset 23
390
+ vendorState: await this.readRegisterValue(moduleAddr + 23, 1, 'uint16'),
391
+ // Events - Offset 24-25 (32-bit bitfield)
392
+ events: await this.readRegisterValue(moduleAddr + 24, 2, 'uint32')
393
+ };
394
+ return data;
395
+ }
396
+ catch (error) {
397
+ console.error(`Error reading MPPT data for module ${moduleId}: ${error}`);
398
+ return null;
399
+ }
400
+ }
401
+ /**
402
+ * Read all available MPPT strings
403
+ */
404
+ async readAllMPPTData() {
405
+ const mpptData = [];
406
+ // Try to read up to 4 MPPT strings (typical maximum)
407
+ for (let i = 1; i <= 4; i++) {
408
+ const data = await this.readMPPTData(i);
409
+ if (data && data.dcCurrent !== undefined && data.dcCurrent > 0) {
410
+ mpptData.push(data);
411
+ }
412
+ }
413
+ return mpptData;
414
+ }
415
+ /**
416
+ * Read meter data (Model 203 for 3-phase)
417
+ */
418
+ async readMeterData() {
419
+ let model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Meter3Phase);
420
+ if (!model) {
421
+ model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MeterWye);
422
+ }
423
+ if (!model) {
424
+ model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.MeterSinglePhase);
425
+ }
426
+ if (!model) {
427
+ console.log('No meter model found');
428
+ return null;
429
+ }
430
+ const baseAddr = model.address + 2;
431
+ try {
432
+ // This is a simplified implementation
433
+ // Actual register offsets depend on specific meter model
434
+ return {
435
+ blockNumber: model.id,
436
+ totalPower: await this.readRegisterWithScaleFactor(baseAddr + 10, baseAddr + 11),
437
+ frequency: await this.readRegisterWithScaleFactor(baseAddr + 20, baseAddr + 21)
438
+ };
439
+ }
440
+ catch (error) {
441
+ console.error(`Error reading meter data: ${error}`);
442
+ return null;
443
+ }
444
+ }
445
+ /**
446
+ * Read common block data (Model 1)
447
+ */
448
+ async readCommonBlock() {
449
+ const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Common);
450
+ if (!model) {
451
+ console.error('Common block model not found');
452
+ return null;
453
+ }
454
+ const baseAddr = model.address + 2; // Skip ID and Length
455
+ try {
456
+ // Read all strings using fault-tolerant reader with proper data type conversion
457
+ const manufacturer = await this.readRegisterValue(baseAddr + 2, 16, 'string'); // Offset 2-17
458
+ const modelName = await this.readRegisterValue(baseAddr + 18, 16, 'string'); // Offset 18-33
459
+ const options = await this.readRegisterValue(baseAddr + 34, 8, 'string'); // Offset 34-41
460
+ const version = await this.readRegisterValue(baseAddr + 42, 8, 'string'); // Offset 42-49
461
+ const serialNumber = await this.readRegisterValue(baseAddr + 50, 16, 'string'); // Offset 50-65
462
+ const deviceAddress = await this.readRegisterValue(baseAddr + 66, 1, 'uint16'); // Offset 66
463
+ return {
464
+ manufacturer: manufacturer,
465
+ model: modelName,
466
+ options: options,
467
+ version: version,
468
+ serialNumber: serialNumber,
469
+ deviceAddress: deviceAddress
470
+ };
471
+ }
472
+ catch (error) {
473
+ console.error(`Error reading common block: ${error}`);
474
+ return null;
475
+ }
476
+ }
477
+ /**
478
+ * Get serial number from device
479
+ */
480
+ async getSerialNumber() {
481
+ const commonData = await this.readCommonBlock();
482
+ return commonData?.serialNumber;
483
+ }
484
+ /**
485
+ * Check if connected
486
+ */
487
+ isConnected() {
488
+ return this.connected;
489
+ }
490
+ /**
491
+ * Check if connection is healthy
492
+ */
493
+ isHealthy() {
494
+ return this.connected && this.connectionHealth.isHealthy();
495
+ }
496
+ /**
497
+ * Get connection health details
498
+ */
499
+ getConnectionHealth() {
500
+ return this.connectionHealth;
501
+ }
502
+ /**
503
+ * Read Block 121 - Inverter Basic Settings
504
+ */
505
+ async readInverterSettings() {
506
+ const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Settings);
507
+ if (!model) {
508
+ console.log('Settings model 121 not found');
509
+ return null;
510
+ }
511
+ const baseAddr = model.address + 2; // Skip ID and Length
512
+ try {
513
+ // Read scale factors first (offsets 22-31)
514
+ const scaleFactors = {
515
+ WMax_SF: await this.readRegisterValue(baseAddr + 22, 1, 'int16'),
516
+ VRef_SF: await this.readRegisterValue(baseAddr + 23, 1, 'int16'),
517
+ VRefOfs_SF: await this.readRegisterValue(baseAddr + 24, 1, 'int16'),
518
+ VMinMax_SF: await this.readRegisterValue(baseAddr + 25, 1, 'int16'),
519
+ VAMax_SF: await this.readRegisterValue(baseAddr + 26, 1, 'int16'),
520
+ VArMax_SF: await this.readRegisterValue(baseAddr + 27, 1, 'int16'),
521
+ WGra_SF: await this.readRegisterValue(baseAddr + 28, 1, 'int16'),
522
+ PFMin_SF: await this.readRegisterValue(baseAddr + 29, 1, 'int16'),
523
+ MaxRmpRte_SF: await this.readRegisterValue(baseAddr + 30, 1, 'int16'),
524
+ ECPNomHz_SF: await this.readRegisterValue(baseAddr + 31, 1, 'int16')
525
+ };
526
+ const settings = {
527
+ blockNumber: 121,
528
+ blockAddress: model.address,
529
+ blockLength: model.length,
530
+ // Power settings - Offset 2
531
+ WMax: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 2, 1, 'uint16'), scaleFactors.WMax_SF),
532
+ WMax_SF: scaleFactors.WMax_SF,
533
+ // Voltage settings - Offsets 3-6
534
+ VRef: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 3, 1, 'uint16'), scaleFactors.VRef_SF),
535
+ VRef_SF: scaleFactors.VRef_SF,
536
+ VRefOfs: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 4, 1, 'int16'), scaleFactors.VRefOfs_SF),
537
+ VRefOfs_SF: scaleFactors.VRefOfs_SF,
538
+ VMax: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 5, 1, 'uint16'), scaleFactors.VMinMax_SF),
539
+ VMin: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 6, 1, 'uint16'), scaleFactors.VMinMax_SF),
540
+ VMinMax_SF: scaleFactors.VMinMax_SF,
541
+ // Apparent power settings - Offset 7
542
+ VAMax: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 7, 1, 'uint16'), scaleFactors.VAMax_SF),
543
+ VAMax_SF: scaleFactors.VAMax_SF,
544
+ // Reactive power settings - Offsets 8-11
545
+ VArMaxQ1: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 8, 1, 'int16'), scaleFactors.VArMax_SF),
546
+ VArMaxQ2: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 9, 1, 'int16'), scaleFactors.VArMax_SF),
547
+ VArMaxQ3: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 10, 1, 'int16'), scaleFactors.VArMax_SF),
548
+ VArMaxQ4: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 11, 1, 'int16'), scaleFactors.VArMax_SF),
549
+ VArMax_SF: scaleFactors.VArMax_SF,
550
+ // Ramp rate settings - Offset 12
551
+ WGra: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 12, 1, 'uint16'), scaleFactors.WGra_SF),
552
+ WGra_SF: scaleFactors.WGra_SF,
553
+ // Power factor settings - Offsets 13-16
554
+ PFMinQ1: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 13, 1, 'int16'), scaleFactors.PFMin_SF),
555
+ PFMinQ2: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 14, 1, 'int16'), scaleFactors.PFMin_SF),
556
+ PFMinQ3: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 15, 1, 'int16'), scaleFactors.PFMin_SF),
557
+ PFMinQ4: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 16, 1, 'int16'), scaleFactors.PFMin_SF),
558
+ PFMin_SF: scaleFactors.PFMin_SF,
559
+ // Other settings - Offsets 17-21
560
+ VArAct: await this.readRegisterValue(baseAddr + 17, 1, 'uint16'),
561
+ ClcTotVA: await this.readRegisterValue(baseAddr + 18, 1, 'uint16'),
562
+ MaxRmpRte: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 19, 1, 'uint16'), scaleFactors.MaxRmpRte_SF),
563
+ MaxRmpRte_SF: scaleFactors.MaxRmpRte_SF,
564
+ ECPNomHz: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 20, 1, 'uint16'), scaleFactors.ECPNomHz_SF),
565
+ ECPNomHz_SF: scaleFactors.ECPNomHz_SF,
566
+ ConnPh: await this.readRegisterValue(baseAddr + 21, 1, 'uint16')
567
+ };
568
+ return settings;
569
+ }
570
+ catch (error) {
571
+ console.error(`Error reading inverter settings: ${error}`);
572
+ return null;
573
+ }
574
+ }
575
+ /**
576
+ * Read Block 123 - Immediate Inverter Controls
577
+ */
578
+ async readInverterControls() {
579
+ const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Controls);
580
+ if (!model) {
581
+ console.log('Controls model 123 not found');
582
+ return null;
583
+ }
584
+ const baseAddr = model.address + 2; // Skip ID and Length
585
+ try {
586
+ // Read scale factors first (offsets 21-23)
587
+ const scaleFactors = {
588
+ WMaxLimPct_SF: await this.readRegisterValue(baseAddr + 21, 1, 'int16'),
589
+ OutPFSet_SF: await this.readRegisterValue(baseAddr + 22, 1, 'int16'),
590
+ VArPct_SF: await this.readRegisterValue(baseAddr + 23, 1, 'int16')
591
+ };
592
+ const controls = {
593
+ blockNumber: 123,
594
+ blockAddress: model.address,
595
+ blockLength: model.length,
596
+ // Connection control - Offsets 0-2
597
+ Conn_WinTms: await this.readRegisterValue(baseAddr + 0, 1, 'uint16'),
598
+ Conn_RvrtTms: await this.readRegisterValue(baseAddr + 1, 1, 'uint16'),
599
+ Conn: await this.readRegisterValue(baseAddr + 2, 1, 'uint16'),
600
+ // Power limit control - Offsets 3-7
601
+ WMaxLimPct: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 3, 1, 'uint16'), scaleFactors.WMaxLimPct_SF),
602
+ WMaxLimPct_SF: scaleFactors.WMaxLimPct_SF,
603
+ WMaxLimPct_WinTms: await this.readRegisterValue(baseAddr + 4, 1, 'uint16'),
604
+ WMaxLimPct_RvrtTms: await this.readRegisterValue(baseAddr + 5, 1, 'uint16'),
605
+ WMaxLimPct_RmpTms: await this.readRegisterValue(baseAddr + 6, 1, 'uint16'),
606
+ WMaxLim_Ena: await this.readRegisterValue(baseAddr + 7, 1, 'uint16'),
607
+ // Power factor control - Offsets 8-12
608
+ OutPFSet: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 8, 1, 'int16'), scaleFactors.OutPFSet_SF),
609
+ OutPFSet_SF: scaleFactors.OutPFSet_SF,
610
+ OutPFSet_WinTms: await this.readRegisterValue(baseAddr + 9, 1, 'uint16'),
611
+ OutPFSet_RvrtTms: await this.readRegisterValue(baseAddr + 10, 1, 'uint16'),
612
+ OutPFSet_RmpTms: await this.readRegisterValue(baseAddr + 11, 1, 'uint16'),
613
+ OutPFSet_Ena: await this.readRegisterValue(baseAddr + 12, 1, 'uint16'),
614
+ // Reactive power control - Offsets 13-20
615
+ VArWMaxPct: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 13, 1, 'int16'), scaleFactors.VArPct_SF),
616
+ VArMaxPct: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 14, 1, 'int16'), scaleFactors.VArPct_SF),
617
+ VArAvalPct: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 15, 1, 'int16'), scaleFactors.VArPct_SF),
618
+ VArPct_SF: scaleFactors.VArPct_SF,
619
+ VArPct_WinTms: await this.readRegisterValue(baseAddr + 16, 1, 'uint16'),
620
+ VArPct_RvrtTms: await this.readRegisterValue(baseAddr + 17, 1, 'uint16'),
621
+ VArPct_RmpTms: await this.readRegisterValue(baseAddr + 18, 1, 'uint16'),
622
+ VArPct_Mod: await this.readRegisterValue(baseAddr + 19, 1, 'uint16'),
623
+ VArPct_Ena: await this.readRegisterValue(baseAddr + 20, 1, 'uint16')
624
+ };
625
+ return controls;
626
+ }
627
+ catch (error) {
628
+ console.error(`Error reading inverter controls: ${error}`);
629
+ return null;
630
+ }
631
+ }
632
+ /**
633
+ * Write Block 121 - Inverter Basic Settings
634
+ */
635
+ async writeInverterSettings(settings) {
636
+ const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Settings);
637
+ if (!model) {
638
+ console.error('Settings model 121 not found');
639
+ return false;
640
+ }
641
+ const baseAddr = model.address + 2;
642
+ try {
643
+ // For each setting, write the value if provided
644
+ // Note: This is a simplified implementation. In production, you'd batch writes
645
+ if (settings.WMax !== undefined && this.modbusClient) {
646
+ // Need to read scale factor first if not provided
647
+ const sfBuffer = await this.modbusClient.readHoldingRegisters(baseAddr + 22, 1);
648
+ const scaleFactor = this.convertToSigned16(sfBuffer.readUInt16BE(0));
649
+ const scaledValue = Math.round(settings.WMax / Math.pow(10, scaleFactor));
650
+ // Writing registers needs to be implemented in EnergyAppModbusInstance
651
+ // For now, log the write operation
652
+ console.log(`Would write value ${scaledValue} to register ${baseAddr}`);
653
+ }
654
+ if (settings.VRef !== undefined && this.modbusClient) {
655
+ const sfBuffer = await this.modbusClient.readHoldingRegisters(baseAddr + 23, 1);
656
+ const scaleFactor = this.convertToSigned16(sfBuffer.readUInt16BE(0));
657
+ const scaledValue = Math.round(settings.VRef / Math.pow(10, scaleFactor));
658
+ console.log(`Would write value ${scaledValue} to register ${baseAddr + 1}`);
659
+ }
660
+ // Add more write operations for other settings as needed
661
+ console.log('Inverter settings written successfully');
662
+ return true;
663
+ }
664
+ catch (error) {
665
+ console.error(`Error writing inverter settings: ${error}`);
666
+ return false;
667
+ }
668
+ }
669
+ /**
670
+ * Write Block 123 - Immediate Inverter Controls
671
+ */
672
+ async writeInverterControls(controls) {
673
+ const model = this.findModel(sunspec_interfaces_js_1.SunspecModelId.Controls);
674
+ if (!model) {
675
+ console.error('Controls model 123 not found');
676
+ return false;
677
+ }
678
+ const baseAddr = model.address + 2;
679
+ try {
680
+ // Connection control
681
+ if (controls.Conn !== undefined && this.modbusClient) {
682
+ // Writing registers needs to be implemented in EnergyAppModbusInstance
683
+ console.log(`Would write connection control ${controls.Conn} to register ${baseAddr + 2}`);
684
+ }
685
+ // Power limit control
686
+ if (controls.WMaxLimPct !== undefined && this.modbusClient) {
687
+ const sfBuffer = await this.modbusClient.readHoldingRegisters(baseAddr + 21, 1);
688
+ const scaleFactor = this.convertToSigned16(sfBuffer.readUInt16BE(0));
689
+ const scaledValue = Math.round(controls.WMaxLimPct / Math.pow(10, scaleFactor));
690
+ console.log(`Would write power limit ${scaledValue} to register ${baseAddr + 3}`);
691
+ }
692
+ if (controls.WMaxLim_Ena !== undefined && this.modbusClient) {
693
+ console.log(`Would write throttle enable ${controls.WMaxLim_Ena} to register ${baseAddr + 7}`);
694
+ }
695
+ // Power factor control
696
+ if (controls.OutPFSet !== undefined && this.modbusClient) {
697
+ const sfBuffer = await this.modbusClient.readHoldingRegisters(baseAddr + 22, 1);
698
+ const scaleFactor = this.convertToSigned16(sfBuffer.readUInt16BE(0));
699
+ const scaledValue = Math.round(controls.OutPFSet / Math.pow(10, scaleFactor));
700
+ console.log(`Would write power factor ${scaledValue} to register ${baseAddr + 8}`);
701
+ }
702
+ if (controls.OutPFSet_Ena !== undefined && this.modbusClient) {
703
+ console.log(`Would write PF enable ${controls.OutPFSet_Ena} to register ${baseAddr + 12}`);
704
+ }
705
+ // Add more control writes as needed
706
+ console.log('Inverter controls written successfully');
707
+ return true;
708
+ }
709
+ catch (error) {
710
+ console.error(`Error writing inverter controls: ${error}`);
711
+ return false;
712
+ }
713
+ }
714
+ }
715
+ exports.SunspecModbusClient = SunspecModbusClient;