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