@enyo-energy/sunspec-sdk 0.0.4 → 0.0.6

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.
@@ -113,12 +113,12 @@ class SunspecInverter extends BaseSunspecDevice {
113
113
  timestampIso: timestamp.toISOString(),
114
114
  resolution,
115
115
  data: {
116
- pvPowerW: inverterData.acPower || 0,
117
- activePowerLimitationW: inverterData.acPower || 0, // Using AC power as default
116
+ pvPowerW: inverterData.dcPower || 0, // Use DC power for PV power
117
+ activePowerLimitationW: inverterData.acPower || 0,
118
118
  state: this.mapOperatingState(inverterData.operatingState),
119
119
  voltageL1: inverterData.voltageAN || 0,
120
- voltageL2: inverterData.voltageBN,
121
- voltageL3: inverterData.voltageCN,
120
+ voltageL2: inverterData.voltageBN ?? undefined, // Use null if undefined (unimplemented phase)
121
+ voltageL3: inverterData.voltageCN ?? undefined, // Use null if undefined (unimplemented phase)
122
122
  strings: this.mapMPPTToStrings(mpptDataList)
123
123
  }
124
124
  };
@@ -152,11 +152,14 @@ class SunspecInverter extends BaseSunspecDevice {
152
152
  mapMPPTToStrings(mpptDataList) {
153
153
  const result = [];
154
154
  mpptDataList.forEach((mppt, index) => {
155
- result.push({
156
- index: index + 1,
157
- voltage: mppt.dcVoltage,
158
- powerW: mppt.dcPower
159
- });
155
+ // Only include strings with valid data
156
+ if (mppt.dcVoltage !== undefined || mppt.dcPower !== undefined) {
157
+ result.push({
158
+ index: index + 1,
159
+ voltage: mppt.dcVoltage ?? undefined, // Use null if undefined
160
+ powerW: mppt.dcPower ?? 0 // Default to 0 if undefined
161
+ });
162
+ }
160
163
  });
161
164
  return result;
162
165
  }
@@ -2,7 +2,7 @@ import { ApplianceManager, EnergyApp } from "@enyo-energy/energy-app-sdk";
2
2
  import { EnyoApplianceName } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
3
3
  import { EnyoNetworkDevice } from "@enyo-energy/energy-app-sdk/dist/types/enyo-network-device.js";
4
4
  import { SunspecModbusClient } from "./sunspec-modbus-client.cjs";
5
- import { EnyoDataBusMessage } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
5
+ import { EnyoDataBusMessage, EnyoDataBusMessageResolution } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
6
6
  /**
7
7
  * Base abstract class for all Sunspec devices
8
8
  */
@@ -27,7 +27,7 @@ export declare abstract class BaseSunspecDevice {
27
27
  /**
28
28
  * Update device data and return data bus messages
29
29
  */
30
- abstract readData(clockId: string, resolution: '10s' | '30s' | '1m' | '15m'): Promise<EnyoDataBusMessage[]>;
30
+ abstract readData(clockId: string, resolution: EnyoDataBusMessageResolution): Promise<EnyoDataBusMessage[]>;
31
31
  /**
32
32
  * Check if the device is connected
33
33
  */
@@ -83,5 +83,5 @@ export declare class SunspecMeter extends BaseSunspecDevice {
83
83
  /**
84
84
  * Update meter data and return data bus messages
85
85
  */
86
- readData(clockId: string, resolution: '10s' | '30s' | '1m' | '15m'): Promise<EnyoDataBusMessage[]>;
86
+ readData(clockId: string, resolution: EnyoDataBusMessageResolution): Promise<EnyoDataBusMessage[]>;
87
87
  }
@@ -51,29 +51,63 @@ class SunspecModbusClient {
51
51
  this.scaleFactors = {};
52
52
  }
53
53
  }
54
+ /**
55
+ * Detect the base address and addressing mode (0-based or 1-based) for SunSpec
56
+ */
57
+ async detectSunspecBaseAddress() {
58
+ if (!this.modbusClient) {
59
+ throw new Error('Modbus client not initialized');
60
+ }
61
+ // Try 1-based addressing first (most common)
62
+ try {
63
+ const sunspecId = await this.modbusClient.readRegisterStringValue(40001, 2);
64
+ if (sunspecId.includes('SunS')) {
65
+ console.log('Detected 1-based addressing mode (base address: 40001)');
66
+ this.baseAddress = 40001;
67
+ return {
68
+ baseAddress: 40001,
69
+ isZeroBased: false,
70
+ nextAddress: 40003
71
+ };
72
+ }
73
+ }
74
+ catch (error) {
75
+ console.debug('Could not read SunS at 40001:', error);
76
+ }
77
+ // Try 0-based addressing
78
+ try {
79
+ const sunspecId = await this.modbusClient.readRegisterStringValue(40000, 2);
80
+ if (sunspecId.includes('SunS')) {
81
+ console.log('Detected 0-based addressing mode (base address: 40000)');
82
+ this.baseAddress = 40000;
83
+ return {
84
+ baseAddress: 40000,
85
+ isZeroBased: true,
86
+ nextAddress: 40002
87
+ };
88
+ }
89
+ }
90
+ catch (error) {
91
+ console.debug('Could not read SunS at 40000:', error);
92
+ }
93
+ throw new Error('Device is not SunSpec compliant - "SunS" identifier not found at addresses 40000 or 40001');
94
+ }
54
95
  /**
55
96
  * Discover all available Sunspec models
56
- * Scans through the address space starting at 40001
97
+ * Automatically detects base address (40000 or 40001) and scans from there
57
98
  */
58
99
  async discoverModels() {
59
100
  if (!this.connected) {
60
101
  throw new Error('Not connected to Modbus device');
61
102
  }
62
103
  this.discoveredModels.clear();
63
- let currentAddress = this.baseAddress;
64
104
  const maxAddress = 50000; // Safety limit
105
+ let currentAddress = 0;
65
106
  console.log('Starting Sunspec model discovery...');
66
107
  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;
108
+ // Detect the base address and addressing mode
109
+ const addressInfo = await this.detectSunspecBaseAddress();
110
+ currentAddress = addressInfo.nextAddress;
77
111
  while (currentAddress < maxAddress) {
78
112
  // Read model ID and length
79
113
  if (!this.modbusClient) {
@@ -155,6 +189,24 @@ class SunspecModbusClient {
155
189
  }
156
190
  return value;
157
191
  }
192
+ /**
193
+ * Check if a value is "unimplemented" according to Sunspec
194
+ * Returns true if the value represents an unimplemented/not applicable register
195
+ */
196
+ isUnimplementedValue(value, dataType = 'uint16') {
197
+ switch (dataType) {
198
+ case 'uint16':
199
+ return value === 0xFFFF || value === 65535;
200
+ case 'int16':
201
+ return value === 0x7FFF || value === 32767;
202
+ case 'uint32':
203
+ return value === 0xFFFFFFFF;
204
+ case 'int32':
205
+ return value === 0x7FFFFFFF;
206
+ default:
207
+ return false;
208
+ }
209
+ }
158
210
  /**
159
211
  * Helper to clean string values by removing null characters
160
212
  */
@@ -234,6 +286,15 @@ class SunspecModbusClient {
234
286
  // Read all scale factors first using fault-tolerant reader
235
287
  const scaleFactors = await this.readInverterScaleFactors(baseAddr);
236
288
  // Read values using fault-tolerant reader with proper data types
289
+ // Read raw voltage values to check for unimplemented phases
290
+ const voltageANRaw = await this.readRegisterValue(baseAddr + 10, 1, 'uint16');
291
+ const voltageBNRaw = await this.readRegisterValue(baseAddr + 11, 1, 'uint16');
292
+ const voltageCNRaw = await this.readRegisterValue(baseAddr + 12, 1, 'uint16');
293
+ console.log('Inverter Raw Voltage Values:', {
294
+ voltageANRaw: `0x${voltageANRaw.toString(16).toUpperCase()} (${voltageANRaw})`,
295
+ voltageBNRaw: `0x${voltageBNRaw.toString(16).toUpperCase()} (${voltageBNRaw})`,
296
+ voltageCNRaw: `0x${voltageCNRaw.toString(16).toUpperCase()} (${voltageCNRaw})`
297
+ });
237
298
  const data = {
238
299
  blockNumber: 103,
239
300
  blockAddress: model.address,
@@ -247,9 +308,10 @@ class SunspecModbusClient {
247
308
  voltageAB: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 7, 1, 'uint16'), scaleFactors.V_SF),
248
309
  voltageBC: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 8, 1, 'uint16'), scaleFactors.V_SF),
249
310
  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),
311
+ // Apply scale factor with unimplemented value checking
312
+ voltageAN: this.applyScaleFactor(voltageANRaw, scaleFactors.V_SF),
313
+ voltageBN: this.applyScaleFactor(voltageBNRaw, scaleFactors.V_SF),
314
+ voltageCN: this.applyScaleFactor(voltageCNRaw, scaleFactors.V_SF),
253
315
  // Power values - Offsets 14, 18, 20, 22
254
316
  acPower: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 14, 1, 'int16'), scaleFactors.W_SF),
255
317
  apparentPower: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 18, 1, 'uint16'), scaleFactors.VA_SF),
@@ -315,7 +377,7 @@ class SunspecModbusClient {
315
377
  * Read inverter scale factors
316
378
  */
317
379
  async readInverterScaleFactors(baseAddr) {
318
- return {
380
+ const scaleFactors = {
319
381
  A_SF: await this.readRegisterValue(baseAddr + 6, 1, 'int16'), // Offset 6
320
382
  V_SF: await this.readRegisterValue(baseAddr + 13, 1, 'int16'), // Offset 13
321
383
  W_SF: await this.readRegisterValue(baseAddr + 15, 1, 'int16'), // Offset 15
@@ -329,11 +391,23 @@ class SunspecModbusClient {
329
391
  DCW_SF: await this.readRegisterValue(baseAddr + 31, 1, 'int16'), // Offset 31
330
392
  Tmp_SF: await this.readRegisterValue(baseAddr + 36, 1, 'int16') // Offset 36
331
393
  };
394
+ console.log('Inverter Scale Factors:', JSON.stringify(scaleFactors, null, 2));
395
+ return scaleFactors;
332
396
  }
333
397
  /**
334
398
  * Apply scale factor to a value
399
+ * Returns undefined if the value is unimplemented or scale factor is out of range
335
400
  */
336
- applyScaleFactor(value, scaleFactor) {
401
+ applyScaleFactor(value, scaleFactor, dataType = 'uint16') {
402
+ // Check for unimplemented values
403
+ if (this.isUnimplementedValue(value, dataType)) {
404
+ return undefined;
405
+ }
406
+ // Validate scale factor is within reasonable range (-10 to +10)
407
+ if (Math.abs(scaleFactor) > 10) {
408
+ console.warn(`Scale factor ${scaleFactor} is outside reasonable range, clamping to ±10`);
409
+ scaleFactor = Math.max(-10, Math.min(10, scaleFactor));
410
+ }
337
411
  return value * Math.pow(10, scaleFactor);
338
412
  }
339
413
  /**
@@ -359,6 +433,28 @@ class SunspecModbusClient {
359
433
  DCWH_SF: await this.readRegisterValue(moduleAddr + 17, 1, 'int16'),
360
434
  Tmp_SF: await this.readRegisterValue(moduleAddr + 21, 1, 'int16')
361
435
  };
436
+ console.log(`MPPT Module ${moduleId} Scale Factors:`, JSON.stringify(scaleFactors, null, 2));
437
+ // Read raw values first
438
+ const dcCurrentRaw = await this.readRegisterValue(moduleAddr + 9, 1, 'uint16');
439
+ const dcVoltageRaw = await this.readRegisterValue(moduleAddr + 11, 1, 'uint16');
440
+ const dcPowerRaw = await this.readRegisterValue(moduleAddr + 13, 1, 'uint16');
441
+ const dcEnergyRaw = await this.readRegisterValue(moduleAddr + 15, 2, 'acc32');
442
+ const temperatureRaw = await this.readRegisterValue(moduleAddr + 20, 1, 'int16');
443
+ console.log(`MPPT Module ${moduleId} Raw Values:`, {
444
+ dcCurrentRaw: `0x${dcCurrentRaw.toString(16).toUpperCase()} (${dcCurrentRaw})`,
445
+ dcVoltageRaw: `0x${dcVoltageRaw.toString(16).toUpperCase()} (${dcVoltageRaw})`,
446
+ dcPowerRaw: `0x${dcPowerRaw.toString(16).toUpperCase()} (${dcPowerRaw})`,
447
+ dcEnergyRaw: `0x${dcEnergyRaw.toString(16).toUpperCase()} (${dcEnergyRaw})`,
448
+ temperatureRaw: `0x${temperatureRaw.toString(16).toUpperCase()} (${temperatureRaw})`
449
+ });
450
+ // Check if this module is actually implemented/connected
451
+ // If all key values are unimplemented, this module doesn't exist
452
+ if (this.isUnimplementedValue(dcCurrentRaw, 'uint16') &&
453
+ this.isUnimplementedValue(dcVoltageRaw, 'uint16') &&
454
+ this.isUnimplementedValue(dcPowerRaw, 'uint16')) {
455
+ console.log(`MPPT module ${moduleId} appears to be unconnected (all values are 0xFFFF)`);
456
+ return null;
457
+ }
362
458
  const data = {
363
459
  blockNumber: 160,
364
460
  blockAddress: model.address,
@@ -367,22 +463,24 @@ class SunspecModbusClient {
367
463
  // String ID - Offset 1 (8 registers for string)
368
464
  stringId: await this.readRegisterValue(moduleAddr + 1, 8, 'string'),
369
465
  // DC Current - Offset 9
370
- dcCurrent: this.applyScaleFactor(await this.readRegisterValue(moduleAddr + 9, 1, 'uint16'), scaleFactors.DCA_SF),
466
+ dcCurrent: this.applyScaleFactor(dcCurrentRaw, scaleFactors.DCA_SF),
371
467
  dcCurrentSF: scaleFactors.DCA_SF,
372
468
  // DC Voltage - Offset 11
373
- dcVoltage: this.applyScaleFactor(await this.readRegisterValue(moduleAddr + 11, 1, 'uint16'), scaleFactors.DCV_SF),
469
+ dcVoltage: this.applyScaleFactor(dcVoltageRaw, scaleFactors.DCV_SF),
374
470
  dcVoltageSF: scaleFactors.DCV_SF,
375
471
  // DC Power - Offset 13
376
- dcPower: this.applyScaleFactor(await this.readRegisterValue(moduleAddr + 13, 1, 'uint16'), scaleFactors.DCW_SF),
472
+ dcPower: this.applyScaleFactor(dcPowerRaw, scaleFactors.DCW_SF),
377
473
  dcPowerSF: scaleFactors.DCW_SF,
378
474
  // 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)),
475
+ // Only calculate if value is not unimplemented
476
+ dcEnergy: !this.isUnimplementedValue(dcEnergyRaw, 'uint32')
477
+ ? BigInt(dcEnergyRaw) * BigInt(Math.pow(10, scaleFactors.DCWH_SF))
478
+ : undefined,
381
479
  dcEnergySF: scaleFactors.DCWH_SF,
382
480
  // Timestamp - Offset 18-19 (32-bit)
383
481
  timestamp: await this.readRegisterValue(moduleAddr + 18, 2, 'uint32'),
384
482
  // Temperature - Offset 20
385
- temperature: this.applyScaleFactor(await this.readRegisterValue(moduleAddr + 20, 1, 'int16'), scaleFactors.Tmp_SF),
483
+ temperature: this.applyScaleFactor(temperatureRaw, scaleFactors.Tmp_SF, 'int16'),
386
484
  temperatureSF: scaleFactors.Tmp_SF,
387
485
  // Operating State - Offset 22
388
486
  operatingState: await this.readRegisterValue(moduleAddr + 22, 1, 'uint16'),
@@ -405,9 +503,19 @@ class SunspecModbusClient {
405
503
  const mpptData = [];
406
504
  // Try to read up to 4 MPPT strings (typical maximum)
407
505
  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);
506
+ try {
507
+ const data = await this.readMPPTData(i);
508
+ // Only include if we got valid data (not null) and it has actual values
509
+ if (data &&
510
+ (data.dcCurrent !== undefined ||
511
+ data.dcVoltage !== undefined ||
512
+ data.dcPower !== undefined)) {
513
+ mpptData.push(data);
514
+ }
515
+ }
516
+ catch (error) {
517
+ console.debug(`Could not read MPPT module ${i}: ${error}`);
518
+ // Continue to try other modules
411
519
  }
412
520
  }
413
521
  return mpptData;
@@ -452,15 +560,31 @@ class SunspecModbusClient {
452
560
  return null;
453
561
  }
454
562
  const baseAddr = model.address + 2; // Skip ID and Length
563
+ console.log(`Reading Common Block - Model address: ${model.address}, Base address for data: ${baseAddr}`);
455
564
  try {
456
565
  // 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 {
566
+ // Common block offsets are relative to the start of the data (after ID and Length)
567
+ const manufacturerAddr = baseAddr; // Offset 0-15 (16 registers) from data start
568
+ const modelAddr = baseAddr + 16; // Offset 16-31 (16 registers) from data start
569
+ const optionsAddr = baseAddr + 32; // Offset 32-39 (8 registers) from data start
570
+ const versionAddr = baseAddr + 40; // Offset 40-47 (8 registers) from data start
571
+ const serialAddr = baseAddr + 48; // Offset 48-63 (16 registers) from data start
572
+ const deviceAddrAddr = baseAddr + 64; // Offset 64 from data start
573
+ console.log(`Reading manufacturer from address ${manufacturerAddr} (16 registers)`);
574
+ const manufacturer = await this.readRegisterValue(manufacturerAddr, 16, 'string');
575
+ console.log(`Manufacturer raw value: "${manufacturer}"`);
576
+ console.log(`Reading model from address ${modelAddr} (16 registers)`);
577
+ const modelName = await this.readRegisterValue(modelAddr, 16, 'string');
578
+ console.log(`Model raw value: "${modelName}"`);
579
+ console.log(`Reading options from address ${optionsAddr} (8 registers)`);
580
+ const options = await this.readRegisterValue(optionsAddr, 8, 'string');
581
+ console.log(`Reading version from address ${versionAddr} (8 registers)`);
582
+ const version = await this.readRegisterValue(versionAddr, 8, 'string');
583
+ console.log(`Reading serial from address ${serialAddr} (16 registers)`);
584
+ const serialNumber = await this.readRegisterValue(serialAddr, 16, 'string');
585
+ console.log(`Reading device address from address ${deviceAddrAddr}`);
586
+ const deviceAddress = await this.readRegisterValue(deviceAddrAddr, 1, 'uint16');
587
+ const result = {
464
588
  manufacturer: manufacturer,
465
589
  model: modelName,
466
590
  options: options,
@@ -468,6 +592,8 @@ class SunspecModbusClient {
468
592
  serialNumber: serialNumber,
469
593
  deviceAddress: deviceAddress
470
594
  };
595
+ console.log('Common Block Data:', JSON.stringify(result, null, 2));
596
+ return result;
471
597
  }
472
598
  catch (error) {
473
599
  console.error(`Error reading common block: ${error}`);
@@ -19,9 +19,17 @@ export declare class SunspecModbusClient {
19
19
  * Disconnect from Modbus device
20
20
  */
21
21
  disconnect(): Promise<void>;
22
+ /**
23
+ * Detect the base address and addressing mode (0-based or 1-based) for SunSpec
24
+ */
25
+ detectSunspecBaseAddress(): Promise<{
26
+ baseAddress: number;
27
+ isZeroBased: boolean;
28
+ nextAddress: number;
29
+ }>;
22
30
  /**
23
31
  * Discover all available Sunspec models
24
- * Scans through the address space starting at 40001
32
+ * Automatically detects base address (40000 or 40001) and scans from there
25
33
  */
26
34
  discoverModels(): Promise<Map<number, SunspecModel>>;
27
35
  /**
@@ -36,6 +44,11 @@ export declare class SunspecModbusClient {
36
44
  * Convert unsigned 16-bit value to signed
37
45
  */
38
46
  private convertToSigned16;
47
+ /**
48
+ * Check if a value is "unimplemented" according to Sunspec
49
+ * Returns true if the value represents an unimplemented/not applicable register
50
+ */
51
+ private isUnimplementedValue;
39
52
  /**
40
53
  * Helper to clean string values by removing null characters
41
54
  */
@@ -58,6 +71,7 @@ export declare class SunspecModbusClient {
58
71
  private readInverterScaleFactors;
59
72
  /**
60
73
  * Apply scale factor to a value
74
+ * Returns undefined if the value is unimplemented or scale factor is out of range
61
75
  */
62
76
  private applyScaleFactor;
63
77
  /**
@@ -9,7 +9,7 @@ exports.getSdkVersion = getSdkVersion;
9
9
  /**
10
10
  * Current version of the enyo Energy App SDK.
11
11
  */
12
- exports.SDK_VERSION = '0.0.4';
12
+ exports.SDK_VERSION = '0.0.6';
13
13
  /**
14
14
  * Gets the current SDK version.
15
15
  * @returns The semantic version string of the SDK
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export declare const SDK_VERSION = "0.0.4";
8
+ export declare const SDK_VERSION = "0.0.6";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
@@ -2,7 +2,7 @@ import { ApplianceManager, EnergyApp } from "@enyo-energy/energy-app-sdk";
2
2
  import { EnyoApplianceName } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
3
3
  import { EnyoNetworkDevice } from "@enyo-energy/energy-app-sdk/dist/types/enyo-network-device.js";
4
4
  import { SunspecModbusClient } from "./sunspec-modbus-client.js";
5
- import { EnyoDataBusMessage } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
5
+ import { EnyoDataBusMessage, EnyoDataBusMessageResolution } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
6
6
  /**
7
7
  * Base abstract class for all Sunspec devices
8
8
  */
@@ -27,7 +27,7 @@ export declare abstract class BaseSunspecDevice {
27
27
  /**
28
28
  * Update device data and return data bus messages
29
29
  */
30
- abstract readData(clockId: string, resolution: '10s' | '30s' | '1m' | '15m'): Promise<EnyoDataBusMessage[]>;
30
+ abstract readData(clockId: string, resolution: EnyoDataBusMessageResolution): Promise<EnyoDataBusMessage[]>;
31
31
  /**
32
32
  * Check if the device is connected
33
33
  */
@@ -83,5 +83,5 @@ export declare class SunspecMeter extends BaseSunspecDevice {
83
83
  /**
84
84
  * Update meter data and return data bus messages
85
85
  */
86
- readData(clockId: string, resolution: '10s' | '30s' | '1m' | '15m'): Promise<EnyoDataBusMessage[]>;
86
+ readData(clockId: string, resolution: EnyoDataBusMessageResolution): Promise<EnyoDataBusMessage[]>;
87
87
  }
@@ -109,12 +109,12 @@ export class SunspecInverter extends BaseSunspecDevice {
109
109
  timestampIso: timestamp.toISOString(),
110
110
  resolution,
111
111
  data: {
112
- pvPowerW: inverterData.acPower || 0,
113
- activePowerLimitationW: inverterData.acPower || 0, // Using AC power as default
112
+ pvPowerW: inverterData.dcPower || 0, // Use DC power for PV power
113
+ activePowerLimitationW: inverterData.acPower || 0,
114
114
  state: this.mapOperatingState(inverterData.operatingState),
115
115
  voltageL1: inverterData.voltageAN || 0,
116
- voltageL2: inverterData.voltageBN,
117
- voltageL3: inverterData.voltageCN,
116
+ voltageL2: inverterData.voltageBN ?? undefined, // Use null if undefined (unimplemented phase)
117
+ voltageL3: inverterData.voltageCN ?? undefined, // Use null if undefined (unimplemented phase)
118
118
  strings: this.mapMPPTToStrings(mpptDataList)
119
119
  }
120
120
  };
@@ -148,11 +148,14 @@ export class SunspecInverter extends BaseSunspecDevice {
148
148
  mapMPPTToStrings(mpptDataList) {
149
149
  const result = [];
150
150
  mpptDataList.forEach((mppt, index) => {
151
- result.push({
152
- index: index + 1,
153
- voltage: mppt.dcVoltage,
154
- powerW: mppt.dcPower
155
- });
151
+ // Only include strings with valid data
152
+ if (mppt.dcVoltage !== undefined || mppt.dcPower !== undefined) {
153
+ result.push({
154
+ index: index + 1,
155
+ voltage: mppt.dcVoltage ?? undefined, // Use null if undefined
156
+ powerW: mppt.dcPower ?? 0 // Default to 0 if undefined
157
+ });
158
+ }
156
159
  });
157
160
  return result;
158
161
  }
@@ -19,9 +19,17 @@ export declare class SunspecModbusClient {
19
19
  * Disconnect from Modbus device
20
20
  */
21
21
  disconnect(): Promise<void>;
22
+ /**
23
+ * Detect the base address and addressing mode (0-based or 1-based) for SunSpec
24
+ */
25
+ detectSunspecBaseAddress(): Promise<{
26
+ baseAddress: number;
27
+ isZeroBased: boolean;
28
+ nextAddress: number;
29
+ }>;
22
30
  /**
23
31
  * Discover all available Sunspec models
24
- * Scans through the address space starting at 40001
32
+ * Automatically detects base address (40000 or 40001) and scans from there
25
33
  */
26
34
  discoverModels(): Promise<Map<number, SunspecModel>>;
27
35
  /**
@@ -36,6 +44,11 @@ export declare class SunspecModbusClient {
36
44
  * Convert unsigned 16-bit value to signed
37
45
  */
38
46
  private convertToSigned16;
47
+ /**
48
+ * Check if a value is "unimplemented" according to Sunspec
49
+ * Returns true if the value represents an unimplemented/not applicable register
50
+ */
51
+ private isUnimplementedValue;
39
52
  /**
40
53
  * Helper to clean string values by removing null characters
41
54
  */
@@ -58,6 +71,7 @@ export declare class SunspecModbusClient {
58
71
  private readInverterScaleFactors;
59
72
  /**
60
73
  * Apply scale factor to a value
74
+ * Returns undefined if the value is unimplemented or scale factor is out of range
61
75
  */
62
76
  private applyScaleFactor;
63
77
  /**
@@ -48,29 +48,63 @@ export class SunspecModbusClient {
48
48
  this.scaleFactors = {};
49
49
  }
50
50
  }
51
+ /**
52
+ * Detect the base address and addressing mode (0-based or 1-based) for SunSpec
53
+ */
54
+ async detectSunspecBaseAddress() {
55
+ if (!this.modbusClient) {
56
+ throw new Error('Modbus client not initialized');
57
+ }
58
+ // Try 1-based addressing first (most common)
59
+ try {
60
+ const sunspecId = await this.modbusClient.readRegisterStringValue(40001, 2);
61
+ if (sunspecId.includes('SunS')) {
62
+ console.log('Detected 1-based addressing mode (base address: 40001)');
63
+ this.baseAddress = 40001;
64
+ return {
65
+ baseAddress: 40001,
66
+ isZeroBased: false,
67
+ nextAddress: 40003
68
+ };
69
+ }
70
+ }
71
+ catch (error) {
72
+ console.debug('Could not read SunS at 40001:', error);
73
+ }
74
+ // Try 0-based addressing
75
+ try {
76
+ const sunspecId = await this.modbusClient.readRegisterStringValue(40000, 2);
77
+ if (sunspecId.includes('SunS')) {
78
+ console.log('Detected 0-based addressing mode (base address: 40000)');
79
+ this.baseAddress = 40000;
80
+ return {
81
+ baseAddress: 40000,
82
+ isZeroBased: true,
83
+ nextAddress: 40002
84
+ };
85
+ }
86
+ }
87
+ catch (error) {
88
+ console.debug('Could not read SunS at 40000:', error);
89
+ }
90
+ throw new Error('Device is not SunSpec compliant - "SunS" identifier not found at addresses 40000 or 40001');
91
+ }
51
92
  /**
52
93
  * Discover all available Sunspec models
53
- * Scans through the address space starting at 40001
94
+ * Automatically detects base address (40000 or 40001) and scans from there
54
95
  */
55
96
  async discoverModels() {
56
97
  if (!this.connected) {
57
98
  throw new Error('Not connected to Modbus device');
58
99
  }
59
100
  this.discoveredModels.clear();
60
- let currentAddress = this.baseAddress;
61
101
  const maxAddress = 50000; // Safety limit
102
+ let currentAddress = 0;
62
103
  console.log('Starting Sunspec model discovery...');
63
104
  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;
105
+ // Detect the base address and addressing mode
106
+ const addressInfo = await this.detectSunspecBaseAddress();
107
+ currentAddress = addressInfo.nextAddress;
74
108
  while (currentAddress < maxAddress) {
75
109
  // Read model ID and length
76
110
  if (!this.modbusClient) {
@@ -152,6 +186,24 @@ export class SunspecModbusClient {
152
186
  }
153
187
  return value;
154
188
  }
189
+ /**
190
+ * Check if a value is "unimplemented" according to Sunspec
191
+ * Returns true if the value represents an unimplemented/not applicable register
192
+ */
193
+ isUnimplementedValue(value, dataType = 'uint16') {
194
+ switch (dataType) {
195
+ case 'uint16':
196
+ return value === 0xFFFF || value === 65535;
197
+ case 'int16':
198
+ return value === 0x7FFF || value === 32767;
199
+ case 'uint32':
200
+ return value === 0xFFFFFFFF;
201
+ case 'int32':
202
+ return value === 0x7FFFFFFF;
203
+ default:
204
+ return false;
205
+ }
206
+ }
155
207
  /**
156
208
  * Helper to clean string values by removing null characters
157
209
  */
@@ -231,6 +283,15 @@ export class SunspecModbusClient {
231
283
  // Read all scale factors first using fault-tolerant reader
232
284
  const scaleFactors = await this.readInverterScaleFactors(baseAddr);
233
285
  // Read values using fault-tolerant reader with proper data types
286
+ // Read raw voltage values to check for unimplemented phases
287
+ const voltageANRaw = await this.readRegisterValue(baseAddr + 10, 1, 'uint16');
288
+ const voltageBNRaw = await this.readRegisterValue(baseAddr + 11, 1, 'uint16');
289
+ const voltageCNRaw = await this.readRegisterValue(baseAddr + 12, 1, 'uint16');
290
+ console.log('Inverter Raw Voltage Values:', {
291
+ voltageANRaw: `0x${voltageANRaw.toString(16).toUpperCase()} (${voltageANRaw})`,
292
+ voltageBNRaw: `0x${voltageBNRaw.toString(16).toUpperCase()} (${voltageBNRaw})`,
293
+ voltageCNRaw: `0x${voltageCNRaw.toString(16).toUpperCase()} (${voltageCNRaw})`
294
+ });
234
295
  const data = {
235
296
  blockNumber: 103,
236
297
  blockAddress: model.address,
@@ -244,9 +305,10 @@ export class SunspecModbusClient {
244
305
  voltageAB: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 7, 1, 'uint16'), scaleFactors.V_SF),
245
306
  voltageBC: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 8, 1, 'uint16'), scaleFactors.V_SF),
246
307
  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),
308
+ // Apply scale factor with unimplemented value checking
309
+ voltageAN: this.applyScaleFactor(voltageANRaw, scaleFactors.V_SF),
310
+ voltageBN: this.applyScaleFactor(voltageBNRaw, scaleFactors.V_SF),
311
+ voltageCN: this.applyScaleFactor(voltageCNRaw, scaleFactors.V_SF),
250
312
  // Power values - Offsets 14, 18, 20, 22
251
313
  acPower: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 14, 1, 'int16'), scaleFactors.W_SF),
252
314
  apparentPower: this.applyScaleFactor(await this.readRegisterValue(baseAddr + 18, 1, 'uint16'), scaleFactors.VA_SF),
@@ -312,7 +374,7 @@ export class SunspecModbusClient {
312
374
  * Read inverter scale factors
313
375
  */
314
376
  async readInverterScaleFactors(baseAddr) {
315
- return {
377
+ const scaleFactors = {
316
378
  A_SF: await this.readRegisterValue(baseAddr + 6, 1, 'int16'), // Offset 6
317
379
  V_SF: await this.readRegisterValue(baseAddr + 13, 1, 'int16'), // Offset 13
318
380
  W_SF: await this.readRegisterValue(baseAddr + 15, 1, 'int16'), // Offset 15
@@ -326,11 +388,23 @@ export class SunspecModbusClient {
326
388
  DCW_SF: await this.readRegisterValue(baseAddr + 31, 1, 'int16'), // Offset 31
327
389
  Tmp_SF: await this.readRegisterValue(baseAddr + 36, 1, 'int16') // Offset 36
328
390
  };
391
+ console.log('Inverter Scale Factors:', JSON.stringify(scaleFactors, null, 2));
392
+ return scaleFactors;
329
393
  }
330
394
  /**
331
395
  * Apply scale factor to a value
396
+ * Returns undefined if the value is unimplemented or scale factor is out of range
332
397
  */
333
- applyScaleFactor(value, scaleFactor) {
398
+ applyScaleFactor(value, scaleFactor, dataType = 'uint16') {
399
+ // Check for unimplemented values
400
+ if (this.isUnimplementedValue(value, dataType)) {
401
+ return undefined;
402
+ }
403
+ // Validate scale factor is within reasonable range (-10 to +10)
404
+ if (Math.abs(scaleFactor) > 10) {
405
+ console.warn(`Scale factor ${scaleFactor} is outside reasonable range, clamping to ±10`);
406
+ scaleFactor = Math.max(-10, Math.min(10, scaleFactor));
407
+ }
334
408
  return value * Math.pow(10, scaleFactor);
335
409
  }
336
410
  /**
@@ -356,6 +430,28 @@ export class SunspecModbusClient {
356
430
  DCWH_SF: await this.readRegisterValue(moduleAddr + 17, 1, 'int16'),
357
431
  Tmp_SF: await this.readRegisterValue(moduleAddr + 21, 1, 'int16')
358
432
  };
433
+ console.log(`MPPT Module ${moduleId} Scale Factors:`, JSON.stringify(scaleFactors, null, 2));
434
+ // Read raw values first
435
+ const dcCurrentRaw = await this.readRegisterValue(moduleAddr + 9, 1, 'uint16');
436
+ const dcVoltageRaw = await this.readRegisterValue(moduleAddr + 11, 1, 'uint16');
437
+ const dcPowerRaw = await this.readRegisterValue(moduleAddr + 13, 1, 'uint16');
438
+ const dcEnergyRaw = await this.readRegisterValue(moduleAddr + 15, 2, 'acc32');
439
+ const temperatureRaw = await this.readRegisterValue(moduleAddr + 20, 1, 'int16');
440
+ console.log(`MPPT Module ${moduleId} Raw Values:`, {
441
+ dcCurrentRaw: `0x${dcCurrentRaw.toString(16).toUpperCase()} (${dcCurrentRaw})`,
442
+ dcVoltageRaw: `0x${dcVoltageRaw.toString(16).toUpperCase()} (${dcVoltageRaw})`,
443
+ dcPowerRaw: `0x${dcPowerRaw.toString(16).toUpperCase()} (${dcPowerRaw})`,
444
+ dcEnergyRaw: `0x${dcEnergyRaw.toString(16).toUpperCase()} (${dcEnergyRaw})`,
445
+ temperatureRaw: `0x${temperatureRaw.toString(16).toUpperCase()} (${temperatureRaw})`
446
+ });
447
+ // Check if this module is actually implemented/connected
448
+ // If all key values are unimplemented, this module doesn't exist
449
+ if (this.isUnimplementedValue(dcCurrentRaw, 'uint16') &&
450
+ this.isUnimplementedValue(dcVoltageRaw, 'uint16') &&
451
+ this.isUnimplementedValue(dcPowerRaw, 'uint16')) {
452
+ console.log(`MPPT module ${moduleId} appears to be unconnected (all values are 0xFFFF)`);
453
+ return null;
454
+ }
359
455
  const data = {
360
456
  blockNumber: 160,
361
457
  blockAddress: model.address,
@@ -364,22 +460,24 @@ export class SunspecModbusClient {
364
460
  // String ID - Offset 1 (8 registers for string)
365
461
  stringId: await this.readRegisterValue(moduleAddr + 1, 8, 'string'),
366
462
  // DC Current - Offset 9
367
- dcCurrent: this.applyScaleFactor(await this.readRegisterValue(moduleAddr + 9, 1, 'uint16'), scaleFactors.DCA_SF),
463
+ dcCurrent: this.applyScaleFactor(dcCurrentRaw, scaleFactors.DCA_SF),
368
464
  dcCurrentSF: scaleFactors.DCA_SF,
369
465
  // DC Voltage - Offset 11
370
- dcVoltage: this.applyScaleFactor(await this.readRegisterValue(moduleAddr + 11, 1, 'uint16'), scaleFactors.DCV_SF),
466
+ dcVoltage: this.applyScaleFactor(dcVoltageRaw, scaleFactors.DCV_SF),
371
467
  dcVoltageSF: scaleFactors.DCV_SF,
372
468
  // DC Power - Offset 13
373
- dcPower: this.applyScaleFactor(await this.readRegisterValue(moduleAddr + 13, 1, 'uint16'), scaleFactors.DCW_SF),
469
+ dcPower: this.applyScaleFactor(dcPowerRaw, scaleFactors.DCW_SF),
374
470
  dcPowerSF: scaleFactors.DCW_SF,
375
471
  // 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)),
472
+ // Only calculate if value is not unimplemented
473
+ dcEnergy: !this.isUnimplementedValue(dcEnergyRaw, 'uint32')
474
+ ? BigInt(dcEnergyRaw) * BigInt(Math.pow(10, scaleFactors.DCWH_SF))
475
+ : undefined,
378
476
  dcEnergySF: scaleFactors.DCWH_SF,
379
477
  // Timestamp - Offset 18-19 (32-bit)
380
478
  timestamp: await this.readRegisterValue(moduleAddr + 18, 2, 'uint32'),
381
479
  // Temperature - Offset 20
382
- temperature: this.applyScaleFactor(await this.readRegisterValue(moduleAddr + 20, 1, 'int16'), scaleFactors.Tmp_SF),
480
+ temperature: this.applyScaleFactor(temperatureRaw, scaleFactors.Tmp_SF, 'int16'),
383
481
  temperatureSF: scaleFactors.Tmp_SF,
384
482
  // Operating State - Offset 22
385
483
  operatingState: await this.readRegisterValue(moduleAddr + 22, 1, 'uint16'),
@@ -402,9 +500,19 @@ export class SunspecModbusClient {
402
500
  const mpptData = [];
403
501
  // Try to read up to 4 MPPT strings (typical maximum)
404
502
  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);
503
+ try {
504
+ const data = await this.readMPPTData(i);
505
+ // Only include if we got valid data (not null) and it has actual values
506
+ if (data &&
507
+ (data.dcCurrent !== undefined ||
508
+ data.dcVoltage !== undefined ||
509
+ data.dcPower !== undefined)) {
510
+ mpptData.push(data);
511
+ }
512
+ }
513
+ catch (error) {
514
+ console.debug(`Could not read MPPT module ${i}: ${error}`);
515
+ // Continue to try other modules
408
516
  }
409
517
  }
410
518
  return mpptData;
@@ -449,15 +557,31 @@ export class SunspecModbusClient {
449
557
  return null;
450
558
  }
451
559
  const baseAddr = model.address + 2; // Skip ID and Length
560
+ console.log(`Reading Common Block - Model address: ${model.address}, Base address for data: ${baseAddr}`);
452
561
  try {
453
562
  // 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 {
563
+ // Common block offsets are relative to the start of the data (after ID and Length)
564
+ const manufacturerAddr = baseAddr; // Offset 0-15 (16 registers) from data start
565
+ const modelAddr = baseAddr + 16; // Offset 16-31 (16 registers) from data start
566
+ const optionsAddr = baseAddr + 32; // Offset 32-39 (8 registers) from data start
567
+ const versionAddr = baseAddr + 40; // Offset 40-47 (8 registers) from data start
568
+ const serialAddr = baseAddr + 48; // Offset 48-63 (16 registers) from data start
569
+ const deviceAddrAddr = baseAddr + 64; // Offset 64 from data start
570
+ console.log(`Reading manufacturer from address ${manufacturerAddr} (16 registers)`);
571
+ const manufacturer = await this.readRegisterValue(manufacturerAddr, 16, 'string');
572
+ console.log(`Manufacturer raw value: "${manufacturer}"`);
573
+ console.log(`Reading model from address ${modelAddr} (16 registers)`);
574
+ const modelName = await this.readRegisterValue(modelAddr, 16, 'string');
575
+ console.log(`Model raw value: "${modelName}"`);
576
+ console.log(`Reading options from address ${optionsAddr} (8 registers)`);
577
+ const options = await this.readRegisterValue(optionsAddr, 8, 'string');
578
+ console.log(`Reading version from address ${versionAddr} (8 registers)`);
579
+ const version = await this.readRegisterValue(versionAddr, 8, 'string');
580
+ console.log(`Reading serial from address ${serialAddr} (16 registers)`);
581
+ const serialNumber = await this.readRegisterValue(serialAddr, 16, 'string');
582
+ console.log(`Reading device address from address ${deviceAddrAddr}`);
583
+ const deviceAddress = await this.readRegisterValue(deviceAddrAddr, 1, 'uint16');
584
+ const result = {
461
585
  manufacturer: manufacturer,
462
586
  model: modelName,
463
587
  options: options,
@@ -465,6 +589,8 @@ export class SunspecModbusClient {
465
589
  serialNumber: serialNumber,
466
590
  deviceAddress: deviceAddress
467
591
  };
592
+ console.log('Common Block Data:', JSON.stringify(result, null, 2));
593
+ return result;
468
594
  }
469
595
  catch (error) {
470
596
  console.error(`Error reading common block: ${error}`);
package/dist/version.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export declare const SDK_VERSION = "0.0.4";
8
+ export declare const SDK_VERSION = "0.0.6";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/dist/version.js CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export const SDK_VERSION = '0.0.4';
8
+ export const SDK_VERSION = '0.0.6';
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enyo-energy/sunspec-sdk",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "enyo Energy Sunspec SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",