@enyo-energy/sunspec-sdk 0.0.52 → 0.0.54

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.
@@ -20,6 +20,9 @@ var SunspecModelId;
20
20
  SunspecModelId[SunspecModelId["Common"] = 1] = "Common";
21
21
  SunspecModelId[SunspecModelId["Inverter3Phase"] = 103] = "Inverter3Phase";
22
22
  SunspecModelId[SunspecModelId["InverterSinglePhase"] = 101] = "InverterSinglePhase";
23
+ SunspecModelId[SunspecModelId["InverterSinglePhaseFloat"] = 111] = "InverterSinglePhaseFloat";
24
+ SunspecModelId[SunspecModelId["InverterSplitPhaseFloat"] = 112] = "InverterSplitPhaseFloat";
25
+ SunspecModelId[SunspecModelId["Inverter3PhaseFloat"] = 113] = "Inverter3PhaseFloat";
23
26
  SunspecModelId[SunspecModelId["MPPT"] = 160] = "MPPT";
24
27
  SunspecModelId[SunspecModelId["Battery"] = 124] = "Battery";
25
28
  SunspecModelId[SunspecModelId["BatteryBase"] = 802] = "BatteryBase";
@@ -30,6 +30,9 @@ export declare enum SunspecModelId {
30
30
  Common = 1,
31
31
  Inverter3Phase = 103,
32
32
  InverterSinglePhase = 101,
33
+ InverterSinglePhaseFloat = 111,
34
+ InverterSplitPhaseFloat = 112,
35
+ Inverter3PhaseFloat = 113,
33
36
  MPPT = 160,
34
37
  Battery = 124,
35
38
  BatteryBase = 802,
@@ -74,10 +77,12 @@ export interface SunspecCommonBlock extends SunspecBlock {
74
77
  deviceAddress?: number;
75
78
  }
76
79
  /**
77
- * Inverter data structure based on Model 103
80
+ * Inverter data structure. Covers SunSpec int+SF inverter models 101/103 and float variants 111/112/113.
81
+ * The per-field offset comments below describe the int+SF (101/103) layout; float variants 111/112/113
82
+ * use 32-bit IEEE 754 float values at different offsets — see the float reader implementations.
78
83
  */
79
84
  export interface SunspecInverterData extends SunspecBlock {
80
- blockNumber: 103 | 101;
85
+ blockNumber: 103 | 101 | 113 | 112 | 111;
81
86
  acCurrent?: number;
82
87
  phaseACurrent?: number;
83
88
  phaseBCurrent?: number;
@@ -504,7 +504,7 @@ class SunspecModbusClient {
504
504
  catch (error) {
505
505
  console.error(`Error during model discovery at address ${currentAddress} (unit ${unitId}): ${error}`);
506
506
  }
507
- console.log(`Discovery complete for unit ${unitId}. Found ${models.size} models`);
507
+ console.log(`Discovery complete for unit ${unitId}. Found ${models.size} models: [${[...models.keys()].sort((a, b) => a - b).join(', ')}]`);
508
508
  return models;
509
509
  }
510
510
  /**
@@ -686,28 +686,36 @@ class SunspecModbusClient {
686
686
  }
687
687
  }
688
688
  /**
689
- * Read inverter data from Model 101 (Single Phase) / Model 103 (Three Phase)
689
+ * Read inverter data. Detects which SunSpec inverter model the device exposes
690
+ * int+SF (101/103) or float (111/112/113) — by checking the discovered model directory,
691
+ * and dispatches to the appropriate decoder.
690
692
  */
691
693
  async readInverterData(unitId) {
692
- const model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Inverter3Phase);
693
- if (!model) {
694
- console.debug(`Inverter model 103 not found on unit ${unitId}, trying single phase model 101`);
695
- const singlePhaseModel = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.InverterSinglePhase);
696
- if (!singlePhaseModel) {
697
- console.error(`No inverter model found on unit ${unitId}`);
698
- return null;
694
+ const tryOrder = [
695
+ { id: 103, reader: m => this.readThreePhaseInverterData_IntSF(unitId, m) },
696
+ { id: 113, reader: m => this.readFloatInverterData(unitId, m, 113) },
697
+ { id: 112, reader: m => this.readFloatInverterData(unitId, m, 112) },
698
+ { id: 101, reader: m => this.readSinglePhaseInverterData(unitId, m) },
699
+ { id: 111, reader: m => this.readFloatInverterData(unitId, m, 111) },
700
+ ];
701
+ for (const { id, reader } of tryOrder) {
702
+ const model = this.findModel(unitId, id);
703
+ if (model) {
704
+ console.debug(`Using inverter Model ${id} at address ${model.address} (length ${model.length}) on unit ${unitId}`);
705
+ return reader(model);
699
706
  }
700
- console.warn('IMPORTANT: Working with single-phase inverter, but 3-phase is expected!');
701
- return this.readSinglePhaseInverterData(unitId, singlePhaseModel);
702
707
  }
703
- console.debug(`Found 3-phase inverter model 103 at address ${model.address} with length ${model.length} (unit ${unitId})`);
708
+ console.debug(`No inverter model (101/103/111/112/113) on unit ${unitId}`);
709
+ return null;
710
+ }
711
+ /**
712
+ * Read three-phase inverter data from Model 103 (int + scale factor encoding).
713
+ */
714
+ async readThreePhaseInverterData_IntSF(unitId, model) {
704
715
  try {
705
- // Read entire model block in a single Modbus call
706
- console.debug(`Reading Inverter Data from Model ${model.id} at base address: ${model.address} (unit ${unitId})`);
716
+ console.debug(`Reading Inverter Data from Model 103 at base address: ${model.address} (unit ${unitId})`);
707
717
  const buffer = await this.readModelBlock(unitId, model);
708
- // Extract all scale factors from buffer
709
718
  const scaleFactors = this.extractInverterScaleFactors(buffer);
710
- // Extract raw values from buffer
711
719
  const acCurrentRaw = this.extractValue(buffer, 2, 'uint16');
712
720
  const voltageANRaw = this.extractValue(buffer, 10, 'uint16');
713
721
  const voltageBNRaw = this.extractValue(buffer, 11, 'uint16');
@@ -759,7 +767,6 @@ class SunspecModbusClient {
759
767
  vendorEvents3: this.extractValue(buffer, 48, 'uint32', 2),
760
768
  vendorEvents4: this.extractValue(buffer, 50, 'uint32', 2)
761
769
  };
762
- // Log non-scaled fields
763
770
  this.logRegisterRead(103, 38, 'Operating State', data.operatingState, 'enum16');
764
771
  this.logRegisterRead(103, 39, 'Vendor State', data.vendorState, 'uint16');
765
772
  this.logRegisterRead(103, 40, 'Events', data.events, 'bitfield32');
@@ -768,7 +775,6 @@ class SunspecModbusClient {
768
775
  this.logRegisterRead(103, 46, 'Vendor Events 2', data.vendorEvents2, 'bitfield32');
769
776
  this.logRegisterRead(103, 48, 'Vendor Events 3', data.vendorEvents3, 'bitfield32');
770
777
  this.logRegisterRead(103, 50, 'Vendor Events 4', data.vendorEvents4, 'bitfield32');
771
- // Read AC Energy (32-bit accumulator) - Offset 24-25
772
778
  const acEnergy = this.extractValue(buffer, 24, 'uint32', 2);
773
779
  this.logRegisterRead(103, 24, 'AC Energy', acEnergy, 'acc32');
774
780
  data.acEnergy = !this.isUnimplementedValue(acEnergy, 'acc32')
@@ -790,44 +796,44 @@ class SunspecModbusClient {
790
796
  console.debug(`Reading Single-Phase Inverter Data from Model 101 at base address: ${model.address} (unit ${unitId})`);
791
797
  // Read entire model block in a single Modbus call
792
798
  const buffer = await this.readModelBlock(unitId, model);
793
- // Extract scale factors from buffer
799
+ // Extract scale factors from buffer (offsets aligned with Model 103 reader, which works on real
800
+ // hardware; differs from the published SunSpec Model 101 spec for DC fields).
794
801
  const scaleFactors = {
795
802
  A_SF: this.extractValue(buffer, 6, 'int16'),
796
803
  V_SF: this.extractValue(buffer, 13, 'int16'),
797
- W_SF: this.extractValue(buffer, 10, 'int16'),
798
- Hz_SF: this.extractValue(buffer, 12, 'int16'),
804
+ W_SF: this.extractValue(buffer, 15, 'int16'),
805
+ Hz_SF: this.extractValue(buffer, 17, 'int16'),
799
806
  WH_SF: this.extractValue(buffer, 26, 'int16'),
800
- DCA_SF: this.extractValue(buffer, 18, 'int16'),
801
- DCV_SF: this.extractValue(buffer, 19, 'int16'),
802
- DCW_SF: this.extractValue(buffer, 21, 'int16')
807
+ DCA_SF: this.extractValue(buffer, 28, 'int16'),
808
+ DCV_SF: this.extractValue(buffer, 29, 'int16'),
809
+ DCW_SF: this.extractValue(buffer, 31, 'int16')
803
810
  };
804
811
  this.logRegisterRead(101, 6, 'A_SF', scaleFactors.A_SF, 'int16');
805
812
  this.logRegisterRead(101, 13, 'V_SF', scaleFactors.V_SF, 'int16');
806
- this.logRegisterRead(101, 10, 'W_SF', scaleFactors.W_SF, 'int16');
807
- this.logRegisterRead(101, 12, 'Hz_SF', scaleFactors.Hz_SF, 'int16');
813
+ this.logRegisterRead(101, 15, 'W_SF', scaleFactors.W_SF, 'int16');
814
+ this.logRegisterRead(101, 17, 'Hz_SF', scaleFactors.Hz_SF, 'int16');
808
815
  this.logRegisterRead(101, 26, 'WH_SF', scaleFactors.WH_SF, 'int16');
809
- this.logRegisterRead(101, 18, 'DCA_SF', scaleFactors.DCA_SF, 'int16');
810
- this.logRegisterRead(101, 19, 'DCV_SF', scaleFactors.DCV_SF, 'int16');
811
- this.logRegisterRead(101, 21, 'DCW_SF', scaleFactors.DCW_SF, 'int16');
812
- // Extract raw values from buffer
816
+ this.logRegisterRead(101, 28, 'DCA_SF', scaleFactors.DCA_SF, 'int16');
817
+ this.logRegisterRead(101, 29, 'DCV_SF', scaleFactors.DCV_SF, 'int16');
818
+ this.logRegisterRead(101, 31, 'DCW_SF', scaleFactors.DCW_SF, 'int16');
813
819
  const acCurrentRaw = this.extractValue(buffer, 2, 'uint16');
814
- const voltageRaw = this.extractValue(buffer, 7, 'uint16');
815
- const acPowerRaw = this.extractValue(buffer, 9, 'int16');
816
- const freqRaw = this.extractValue(buffer, 11, 'uint16');
817
- const dcCurrentRaw = this.extractValue(buffer, 14, 'uint16');
818
- const dcVoltageRaw = this.extractValue(buffer, 15, 'uint16');
819
- const dcPowerRaw = this.extractValue(buffer, 20, 'int16');
820
- const stateRaw = this.extractValue(buffer, 24, 'uint16');
821
- this.logRegisterRead(101, 24, 'Operating State', stateRaw, 'enum16');
820
+ const voltageRaw = this.extractValue(buffer, 10, 'uint16');
821
+ const acPowerRaw = this.extractValue(buffer, 14, 'int16');
822
+ const freqRaw = this.extractValue(buffer, 16, 'uint16');
823
+ const dcCurrentRaw = this.extractValue(buffer, 27, 'uint16');
824
+ const dcVoltageRaw = this.extractValue(buffer, 28, 'uint16');
825
+ const dcPowerRaw = this.extractValue(buffer, 30, 'int16');
826
+ const stateRaw = this.extractValue(buffer, 38, 'uint16');
827
+ this.logRegisterRead(101, 38, 'Operating State', stateRaw, 'enum16');
822
828
  const data = {
823
829
  blockNumber: 101,
824
- voltageAN: this.applyScaleFactor(voltageRaw, scaleFactors.V_SF, 'uint16', 'Voltage AN', 7, 101),
830
+ voltageAN: this.applyScaleFactor(voltageRaw, scaleFactors.V_SF, 'uint16', 'Voltage AN', 10, 101),
825
831
  acCurrent: this.applyScaleFactor(acCurrentRaw, scaleFactors.A_SF, 'uint16', 'AC Current', 2, 101),
826
- acPower: this.applyScaleFactor(acPowerRaw, scaleFactors.W_SF, 'int16', 'AC Power', 9, 101),
827
- frequency: this.applyScaleFactor(freqRaw, scaleFactors.Hz_SF, 'uint16', 'Frequency', 11, 101),
828
- dcCurrent: this.applyScaleFactor(dcCurrentRaw, scaleFactors.DCA_SF, 'uint16', 'DC Current', 14, 101),
829
- dcVoltage: this.applyScaleFactor(dcVoltageRaw, scaleFactors.DCV_SF, 'uint16', 'DC Voltage', 15, 101),
830
- dcPower: this.applyScaleFactor(dcPowerRaw, scaleFactors.DCW_SF, 'int16', 'DC Power', 20, 101),
832
+ acPower: this.applyScaleFactor(acPowerRaw, scaleFactors.W_SF, 'int16', 'AC Power', 14, 101),
833
+ frequency: this.applyScaleFactor(freqRaw, scaleFactors.Hz_SF, 'uint16', 'Frequency', 16, 101),
834
+ dcCurrent: this.applyScaleFactor(dcCurrentRaw, scaleFactors.DCA_SF, 'uint16', 'DC Current', 27, 101),
835
+ dcVoltage: this.applyScaleFactor(dcVoltageRaw, scaleFactors.DCV_SF, 'uint16', 'DC Voltage', 28, 101),
836
+ dcPower: this.applyScaleFactor(dcPowerRaw, scaleFactors.DCW_SF, 'int16', 'DC Power', 30, 101),
831
837
  operatingState: stateRaw
832
838
  };
833
839
  // Read AC Energy (32-bit accumulator) - Offset 24-25
@@ -844,6 +850,70 @@ class SunspecModbusClient {
844
850
  return null;
845
851
  }
846
852
  }
853
+ /**
854
+ * Read inverter data from a float-variant model: 111 (single-phase), 112 (split-phase), or 113 (three-phase).
855
+ *
856
+ * All three models share the same SunSpec register layout — measurement fields are 32-bit IEEE 754 floats
857
+ * (no scale factors), status registers stay enum16, event registers stay bitfield32. Phase scope differs
858
+ * by model: 111 populates phase A only; 112 phases A+B; 113 all three. Unpopulated phase fields read NaN
859
+ * and are returned as undefined.
860
+ */
861
+ async readFloatInverterData(unitId, model, blockNumber) {
862
+ try {
863
+ console.debug(`Reading Float Inverter Data from Model ${blockNumber} at base address: ${model.address} (unit ${unitId})`);
864
+ const buffer = await this.readModelBlock(unitId, model);
865
+ const data = {
866
+ blockNumber,
867
+ blockAddress: model.address,
868
+ blockLength: model.length,
869
+ acCurrent: this.extractFloat32OrUndefined(buffer, 2, blockNumber, 'AC Current'),
870
+ phaseACurrent: this.extractFloat32OrUndefined(buffer, 4, blockNumber, 'Phase A Current'),
871
+ phaseBCurrent: this.extractFloat32OrUndefined(buffer, 6, blockNumber, 'Phase B Current'),
872
+ phaseCCurrent: this.extractFloat32OrUndefined(buffer, 8, blockNumber, 'Phase C Current'),
873
+ voltageAB: this.extractFloat32OrUndefined(buffer, 10, blockNumber, 'Voltage AB'),
874
+ voltageBC: this.extractFloat32OrUndefined(buffer, 12, blockNumber, 'Voltage BC'),
875
+ voltageCA: this.extractFloat32OrUndefined(buffer, 14, blockNumber, 'Voltage CA'),
876
+ voltageAN: this.extractFloat32OrUndefined(buffer, 16, blockNumber, 'Voltage AN'),
877
+ voltageBN: this.extractFloat32OrUndefined(buffer, 18, blockNumber, 'Voltage BN'),
878
+ voltageCN: this.extractFloat32OrUndefined(buffer, 20, blockNumber, 'Voltage CN'),
879
+ acPower: this.extractFloat32OrUndefined(buffer, 22, blockNumber, 'AC Power'),
880
+ frequency: this.extractFloat32OrUndefined(buffer, 24, blockNumber, 'Frequency'),
881
+ apparentPower: this.extractFloat32OrUndefined(buffer, 26, blockNumber, 'Apparent Power'),
882
+ reactivePower: this.extractFloat32OrUndefined(buffer, 28, blockNumber, 'Reactive Power'),
883
+ powerFactor: this.extractFloat32OrUndefined(buffer, 30, blockNumber, 'Power Factor'),
884
+ acEnergy: this.extractFloat32OrUndefined(buffer, 32, blockNumber, 'AC Energy'),
885
+ dcCurrent: this.extractFloat32OrUndefined(buffer, 34, blockNumber, 'DC Current'),
886
+ dcVoltage: this.extractFloat32OrUndefined(buffer, 36, blockNumber, 'DC Voltage'),
887
+ dcPower: this.extractFloat32OrUndefined(buffer, 38, blockNumber, 'DC Power'),
888
+ cabinetTemperature: this.extractFloat32OrUndefined(buffer, 40, blockNumber, 'Cabinet Temperature'),
889
+ heatSinkTemperature: this.extractFloat32OrUndefined(buffer, 42, blockNumber, 'Heat Sink Temperature'),
890
+ transformerTemperature: this.extractFloat32OrUndefined(buffer, 44, blockNumber, 'Transformer Temperature'),
891
+ otherTemperature: this.extractFloat32OrUndefined(buffer, 46, blockNumber, 'Other Temperature'),
892
+ operatingState: this.extractValue(buffer, 48, 'uint16'),
893
+ vendorState: this.extractValue(buffer, 49, 'uint16'),
894
+ events: this.extractValue(buffer, 50, 'uint32', 2),
895
+ events2: this.extractValue(buffer, 52, 'uint32', 2),
896
+ vendorEvents1: this.extractValue(buffer, 54, 'uint32', 2),
897
+ vendorEvents2: this.extractValue(buffer, 56, 'uint32', 2),
898
+ vendorEvents3: this.extractValue(buffer, 58, 'uint32', 2),
899
+ vendorEvents4: this.extractValue(buffer, 60, 'uint32', 2),
900
+ };
901
+ this.logRegisterRead(blockNumber, 48, 'Operating State', data.operatingState, 'enum16');
902
+ this.logRegisterRead(blockNumber, 49, 'Vendor State', data.vendorState, 'enum16');
903
+ this.logRegisterRead(blockNumber, 50, 'Events', data.events, 'bitfield32');
904
+ this.logRegisterRead(blockNumber, 52, 'Events2', data.events2, 'bitfield32');
905
+ this.logRegisterRead(blockNumber, 54, 'Vendor Events 1', data.vendorEvents1, 'bitfield32');
906
+ this.logRegisterRead(blockNumber, 56, 'Vendor Events 2', data.vendorEvents2, 'bitfield32');
907
+ this.logRegisterRead(blockNumber, 58, 'Vendor Events 3', data.vendorEvents3, 'bitfield32');
908
+ this.logRegisterRead(blockNumber, 60, 'Vendor Events 4', data.vendorEvents4, 'bitfield32');
909
+ console.debug(`[Model ${blockNumber}] Float Inverter Data:`, data);
910
+ return data;
911
+ }
912
+ catch (error) {
913
+ console.error(`Error reading float inverter data (Model ${blockNumber}): ${error}`);
914
+ return null;
915
+ }
916
+ }
847
917
  /**
848
918
  * Extract inverter scale factors from a pre-read model buffer
849
919
  */
@@ -911,6 +981,16 @@ class SunspecModbusClient {
911
981
  console.debug(`[Model ${modelId}] offset ${offset}: ${fieldName} = "${rawValue}"${typeInfo}`);
912
982
  }
913
983
  }
984
+ /**
985
+ * Read a float32 (IEEE 754, big-endian, 2 registers) from a model buffer.
986
+ * SunSpec uses NaN as the "unimplemented" sentinel for floats; returns undefined in that case.
987
+ */
988
+ extractFloat32OrUndefined(buffer, offset, modelId, fieldName) {
989
+ const value = this.extractValue(buffer, offset, 'float32', 2);
990
+ const display = Number.isNaN(value) ? 'NaN' : value;
991
+ console.debug(`[Model ${modelId}] offset ${offset}: ${fieldName} = ${display} (float32)`);
992
+ return Number.isNaN(value) ? undefined : value;
993
+ }
914
994
  /**
915
995
  * Read scale factors from Model 160 (MPPT)
916
996
  *
@@ -201,13 +201,28 @@ export declare class SunspecModbusClient {
201
201
  */
202
202
  writeRegisterValue(unitId: number, address: number, value: number | number[], dataType?: EnergyAppModbusDataType): Promise<boolean>;
203
203
  /**
204
- * Read inverter data from Model 101 (Single Phase) / Model 103 (Three Phase)
204
+ * Read inverter data. Detects which SunSpec inverter model the device exposes
205
+ * int+SF (101/103) or float (111/112/113) — by checking the discovered model directory,
206
+ * and dispatches to the appropriate decoder.
205
207
  */
206
208
  readInverterData(unitId: number): Promise<SunspecInverterData | null>;
209
+ /**
210
+ * Read three-phase inverter data from Model 103 (int + scale factor encoding).
211
+ */
212
+ private readThreePhaseInverterData_IntSF;
207
213
  /**
208
214
  * Read single phase inverter data (Model 101)
209
215
  */
210
216
  private readSinglePhaseInverterData;
217
+ /**
218
+ * Read inverter data from a float-variant model: 111 (single-phase), 112 (split-phase), or 113 (three-phase).
219
+ *
220
+ * All three models share the same SunSpec register layout — measurement fields are 32-bit IEEE 754 floats
221
+ * (no scale factors), status registers stay enum16, event registers stay bitfield32. Phase scope differs
222
+ * by model: 111 populates phase A only; 112 phases A+B; 113 all three. Unpopulated phase fields read NaN
223
+ * and are returned as undefined.
224
+ */
225
+ private readFloatInverterData;
211
226
  /**
212
227
  * Extract inverter scale factors from a pre-read model buffer
213
228
  */
@@ -218,6 +233,11 @@ export declare class SunspecModbusClient {
218
233
  */
219
234
  applyScaleFactor(value: number, scaleFactor: number, dataType?: 'uint16' | 'int16' | 'acc32', fieldName?: string, offset?: number, modelId?: number): number | undefined;
220
235
  logRegisterRead(modelId: number, offset: number, fieldName: string, rawValue: number | string | undefined, dataType?: string): void;
236
+ /**
237
+ * Read a float32 (IEEE 754, big-endian, 2 registers) from a model buffer.
238
+ * SunSpec uses NaN as the "unimplemented" sentinel for floats; returns undefined in that case.
239
+ */
240
+ private extractFloat32OrUndefined;
221
241
  /**
222
242
  * Read scale factors from Model 160 (MPPT)
223
243
  *
@@ -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.52';
12
+ exports.SDK_VERSION = '0.0.54';
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.52";
8
+ export declare const SDK_VERSION = "0.0.54";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
@@ -30,6 +30,9 @@ export declare enum SunspecModelId {
30
30
  Common = 1,
31
31
  Inverter3Phase = 103,
32
32
  InverterSinglePhase = 101,
33
+ InverterSinglePhaseFloat = 111,
34
+ InverterSplitPhaseFloat = 112,
35
+ Inverter3PhaseFloat = 113,
33
36
  MPPT = 160,
34
37
  Battery = 124,
35
38
  BatteryBase = 802,
@@ -74,10 +77,12 @@ export interface SunspecCommonBlock extends SunspecBlock {
74
77
  deviceAddress?: number;
75
78
  }
76
79
  /**
77
- * Inverter data structure based on Model 103
80
+ * Inverter data structure. Covers SunSpec int+SF inverter models 101/103 and float variants 111/112/113.
81
+ * The per-field offset comments below describe the int+SF (101/103) layout; float variants 111/112/113
82
+ * use 32-bit IEEE 754 float values at different offsets — see the float reader implementations.
78
83
  */
79
84
  export interface SunspecInverterData extends SunspecBlock {
80
- blockNumber: 103 | 101;
85
+ blockNumber: 103 | 101 | 113 | 112 | 111;
81
86
  acCurrent?: number;
82
87
  phaseACurrent?: number;
83
88
  phaseBCurrent?: number;
@@ -17,6 +17,9 @@ export var SunspecModelId;
17
17
  SunspecModelId[SunspecModelId["Common"] = 1] = "Common";
18
18
  SunspecModelId[SunspecModelId["Inverter3Phase"] = 103] = "Inverter3Phase";
19
19
  SunspecModelId[SunspecModelId["InverterSinglePhase"] = 101] = "InverterSinglePhase";
20
+ SunspecModelId[SunspecModelId["InverterSinglePhaseFloat"] = 111] = "InverterSinglePhaseFloat";
21
+ SunspecModelId[SunspecModelId["InverterSplitPhaseFloat"] = 112] = "InverterSplitPhaseFloat";
22
+ SunspecModelId[SunspecModelId["Inverter3PhaseFloat"] = 113] = "Inverter3PhaseFloat";
20
23
  SunspecModelId[SunspecModelId["MPPT"] = 160] = "MPPT";
21
24
  SunspecModelId[SunspecModelId["Battery"] = 124] = "Battery";
22
25
  SunspecModelId[SunspecModelId["BatteryBase"] = 802] = "BatteryBase";
@@ -201,13 +201,28 @@ export declare class SunspecModbusClient {
201
201
  */
202
202
  writeRegisterValue(unitId: number, address: number, value: number | number[], dataType?: EnergyAppModbusDataType): Promise<boolean>;
203
203
  /**
204
- * Read inverter data from Model 101 (Single Phase) / Model 103 (Three Phase)
204
+ * Read inverter data. Detects which SunSpec inverter model the device exposes
205
+ * int+SF (101/103) or float (111/112/113) — by checking the discovered model directory,
206
+ * and dispatches to the appropriate decoder.
205
207
  */
206
208
  readInverterData(unitId: number): Promise<SunspecInverterData | null>;
209
+ /**
210
+ * Read three-phase inverter data from Model 103 (int + scale factor encoding).
211
+ */
212
+ private readThreePhaseInverterData_IntSF;
207
213
  /**
208
214
  * Read single phase inverter data (Model 101)
209
215
  */
210
216
  private readSinglePhaseInverterData;
217
+ /**
218
+ * Read inverter data from a float-variant model: 111 (single-phase), 112 (split-phase), or 113 (three-phase).
219
+ *
220
+ * All three models share the same SunSpec register layout — measurement fields are 32-bit IEEE 754 floats
221
+ * (no scale factors), status registers stay enum16, event registers stay bitfield32. Phase scope differs
222
+ * by model: 111 populates phase A only; 112 phases A+B; 113 all three. Unpopulated phase fields read NaN
223
+ * and are returned as undefined.
224
+ */
225
+ private readFloatInverterData;
211
226
  /**
212
227
  * Extract inverter scale factors from a pre-read model buffer
213
228
  */
@@ -218,6 +233,11 @@ export declare class SunspecModbusClient {
218
233
  */
219
234
  applyScaleFactor(value: number, scaleFactor: number, dataType?: 'uint16' | 'int16' | 'acc32', fieldName?: string, offset?: number, modelId?: number): number | undefined;
220
235
  logRegisterRead(modelId: number, offset: number, fieldName: string, rawValue: number | string | undefined, dataType?: string): void;
236
+ /**
237
+ * Read a float32 (IEEE 754, big-endian, 2 registers) from a model buffer.
238
+ * SunSpec uses NaN as the "unimplemented" sentinel for floats; returns undefined in that case.
239
+ */
240
+ private extractFloat32OrUndefined;
221
241
  /**
222
242
  * Read scale factors from Model 160 (MPPT)
223
243
  *
@@ -499,7 +499,7 @@ export class SunspecModbusClient {
499
499
  catch (error) {
500
500
  console.error(`Error during model discovery at address ${currentAddress} (unit ${unitId}): ${error}`);
501
501
  }
502
- console.log(`Discovery complete for unit ${unitId}. Found ${models.size} models`);
502
+ console.log(`Discovery complete for unit ${unitId}. Found ${models.size} models: [${[...models.keys()].sort((a, b) => a - b).join(', ')}]`);
503
503
  return models;
504
504
  }
505
505
  /**
@@ -681,28 +681,36 @@ export class SunspecModbusClient {
681
681
  }
682
682
  }
683
683
  /**
684
- * Read inverter data from Model 101 (Single Phase) / Model 103 (Three Phase)
684
+ * Read inverter data. Detects which SunSpec inverter model the device exposes
685
+ * int+SF (101/103) or float (111/112/113) — by checking the discovered model directory,
686
+ * and dispatches to the appropriate decoder.
685
687
  */
686
688
  async readInverterData(unitId) {
687
- const model = this.findModel(unitId, SunspecModelId.Inverter3Phase);
688
- if (!model) {
689
- console.debug(`Inverter model 103 not found on unit ${unitId}, trying single phase model 101`);
690
- const singlePhaseModel = this.findModel(unitId, SunspecModelId.InverterSinglePhase);
691
- if (!singlePhaseModel) {
692
- console.error(`No inverter model found on unit ${unitId}`);
693
- return null;
689
+ const tryOrder = [
690
+ { id: 103, reader: m => this.readThreePhaseInverterData_IntSF(unitId, m) },
691
+ { id: 113, reader: m => this.readFloatInverterData(unitId, m, 113) },
692
+ { id: 112, reader: m => this.readFloatInverterData(unitId, m, 112) },
693
+ { id: 101, reader: m => this.readSinglePhaseInverterData(unitId, m) },
694
+ { id: 111, reader: m => this.readFloatInverterData(unitId, m, 111) },
695
+ ];
696
+ for (const { id, reader } of tryOrder) {
697
+ const model = this.findModel(unitId, id);
698
+ if (model) {
699
+ console.debug(`Using inverter Model ${id} at address ${model.address} (length ${model.length}) on unit ${unitId}`);
700
+ return reader(model);
694
701
  }
695
- console.warn('IMPORTANT: Working with single-phase inverter, but 3-phase is expected!');
696
- return this.readSinglePhaseInverterData(unitId, singlePhaseModel);
697
702
  }
698
- console.debug(`Found 3-phase inverter model 103 at address ${model.address} with length ${model.length} (unit ${unitId})`);
703
+ console.debug(`No inverter model (101/103/111/112/113) on unit ${unitId}`);
704
+ return null;
705
+ }
706
+ /**
707
+ * Read three-phase inverter data from Model 103 (int + scale factor encoding).
708
+ */
709
+ async readThreePhaseInverterData_IntSF(unitId, model) {
699
710
  try {
700
- // Read entire model block in a single Modbus call
701
- console.debug(`Reading Inverter Data from Model ${model.id} at base address: ${model.address} (unit ${unitId})`);
711
+ console.debug(`Reading Inverter Data from Model 103 at base address: ${model.address} (unit ${unitId})`);
702
712
  const buffer = await this.readModelBlock(unitId, model);
703
- // Extract all scale factors from buffer
704
713
  const scaleFactors = this.extractInverterScaleFactors(buffer);
705
- // Extract raw values from buffer
706
714
  const acCurrentRaw = this.extractValue(buffer, 2, 'uint16');
707
715
  const voltageANRaw = this.extractValue(buffer, 10, 'uint16');
708
716
  const voltageBNRaw = this.extractValue(buffer, 11, 'uint16');
@@ -754,7 +762,6 @@ export class SunspecModbusClient {
754
762
  vendorEvents3: this.extractValue(buffer, 48, 'uint32', 2),
755
763
  vendorEvents4: this.extractValue(buffer, 50, 'uint32', 2)
756
764
  };
757
- // Log non-scaled fields
758
765
  this.logRegisterRead(103, 38, 'Operating State', data.operatingState, 'enum16');
759
766
  this.logRegisterRead(103, 39, 'Vendor State', data.vendorState, 'uint16');
760
767
  this.logRegisterRead(103, 40, 'Events', data.events, 'bitfield32');
@@ -763,7 +770,6 @@ export class SunspecModbusClient {
763
770
  this.logRegisterRead(103, 46, 'Vendor Events 2', data.vendorEvents2, 'bitfield32');
764
771
  this.logRegisterRead(103, 48, 'Vendor Events 3', data.vendorEvents3, 'bitfield32');
765
772
  this.logRegisterRead(103, 50, 'Vendor Events 4', data.vendorEvents4, 'bitfield32');
766
- // Read AC Energy (32-bit accumulator) - Offset 24-25
767
773
  const acEnergy = this.extractValue(buffer, 24, 'uint32', 2);
768
774
  this.logRegisterRead(103, 24, 'AC Energy', acEnergy, 'acc32');
769
775
  data.acEnergy = !this.isUnimplementedValue(acEnergy, 'acc32')
@@ -785,44 +791,44 @@ export class SunspecModbusClient {
785
791
  console.debug(`Reading Single-Phase Inverter Data from Model 101 at base address: ${model.address} (unit ${unitId})`);
786
792
  // Read entire model block in a single Modbus call
787
793
  const buffer = await this.readModelBlock(unitId, model);
788
- // Extract scale factors from buffer
794
+ // Extract scale factors from buffer (offsets aligned with Model 103 reader, which works on real
795
+ // hardware; differs from the published SunSpec Model 101 spec for DC fields).
789
796
  const scaleFactors = {
790
797
  A_SF: this.extractValue(buffer, 6, 'int16'),
791
798
  V_SF: this.extractValue(buffer, 13, 'int16'),
792
- W_SF: this.extractValue(buffer, 10, 'int16'),
793
- Hz_SF: this.extractValue(buffer, 12, 'int16'),
799
+ W_SF: this.extractValue(buffer, 15, 'int16'),
800
+ Hz_SF: this.extractValue(buffer, 17, 'int16'),
794
801
  WH_SF: this.extractValue(buffer, 26, 'int16'),
795
- DCA_SF: this.extractValue(buffer, 18, 'int16'),
796
- DCV_SF: this.extractValue(buffer, 19, 'int16'),
797
- DCW_SF: this.extractValue(buffer, 21, 'int16')
802
+ DCA_SF: this.extractValue(buffer, 28, 'int16'),
803
+ DCV_SF: this.extractValue(buffer, 29, 'int16'),
804
+ DCW_SF: this.extractValue(buffer, 31, 'int16')
798
805
  };
799
806
  this.logRegisterRead(101, 6, 'A_SF', scaleFactors.A_SF, 'int16');
800
807
  this.logRegisterRead(101, 13, 'V_SF', scaleFactors.V_SF, 'int16');
801
- this.logRegisterRead(101, 10, 'W_SF', scaleFactors.W_SF, 'int16');
802
- this.logRegisterRead(101, 12, 'Hz_SF', scaleFactors.Hz_SF, 'int16');
808
+ this.logRegisterRead(101, 15, 'W_SF', scaleFactors.W_SF, 'int16');
809
+ this.logRegisterRead(101, 17, 'Hz_SF', scaleFactors.Hz_SF, 'int16');
803
810
  this.logRegisterRead(101, 26, 'WH_SF', scaleFactors.WH_SF, 'int16');
804
- this.logRegisterRead(101, 18, 'DCA_SF', scaleFactors.DCA_SF, 'int16');
805
- this.logRegisterRead(101, 19, 'DCV_SF', scaleFactors.DCV_SF, 'int16');
806
- this.logRegisterRead(101, 21, 'DCW_SF', scaleFactors.DCW_SF, 'int16');
807
- // Extract raw values from buffer
811
+ this.logRegisterRead(101, 28, 'DCA_SF', scaleFactors.DCA_SF, 'int16');
812
+ this.logRegisterRead(101, 29, 'DCV_SF', scaleFactors.DCV_SF, 'int16');
813
+ this.logRegisterRead(101, 31, 'DCW_SF', scaleFactors.DCW_SF, 'int16');
808
814
  const acCurrentRaw = this.extractValue(buffer, 2, 'uint16');
809
- const voltageRaw = this.extractValue(buffer, 7, 'uint16');
810
- const acPowerRaw = this.extractValue(buffer, 9, 'int16');
811
- const freqRaw = this.extractValue(buffer, 11, 'uint16');
812
- const dcCurrentRaw = this.extractValue(buffer, 14, 'uint16');
813
- const dcVoltageRaw = this.extractValue(buffer, 15, 'uint16');
814
- const dcPowerRaw = this.extractValue(buffer, 20, 'int16');
815
- const stateRaw = this.extractValue(buffer, 24, 'uint16');
816
- this.logRegisterRead(101, 24, 'Operating State', stateRaw, 'enum16');
815
+ const voltageRaw = this.extractValue(buffer, 10, 'uint16');
816
+ const acPowerRaw = this.extractValue(buffer, 14, 'int16');
817
+ const freqRaw = this.extractValue(buffer, 16, 'uint16');
818
+ const dcCurrentRaw = this.extractValue(buffer, 27, 'uint16');
819
+ const dcVoltageRaw = this.extractValue(buffer, 28, 'uint16');
820
+ const dcPowerRaw = this.extractValue(buffer, 30, 'int16');
821
+ const stateRaw = this.extractValue(buffer, 38, 'uint16');
822
+ this.logRegisterRead(101, 38, 'Operating State', stateRaw, 'enum16');
817
823
  const data = {
818
824
  blockNumber: 101,
819
- voltageAN: this.applyScaleFactor(voltageRaw, scaleFactors.V_SF, 'uint16', 'Voltage AN', 7, 101),
825
+ voltageAN: this.applyScaleFactor(voltageRaw, scaleFactors.V_SF, 'uint16', 'Voltage AN', 10, 101),
820
826
  acCurrent: this.applyScaleFactor(acCurrentRaw, scaleFactors.A_SF, 'uint16', 'AC Current', 2, 101),
821
- acPower: this.applyScaleFactor(acPowerRaw, scaleFactors.W_SF, 'int16', 'AC Power', 9, 101),
822
- frequency: this.applyScaleFactor(freqRaw, scaleFactors.Hz_SF, 'uint16', 'Frequency', 11, 101),
823
- dcCurrent: this.applyScaleFactor(dcCurrentRaw, scaleFactors.DCA_SF, 'uint16', 'DC Current', 14, 101),
824
- dcVoltage: this.applyScaleFactor(dcVoltageRaw, scaleFactors.DCV_SF, 'uint16', 'DC Voltage', 15, 101),
825
- dcPower: this.applyScaleFactor(dcPowerRaw, scaleFactors.DCW_SF, 'int16', 'DC Power', 20, 101),
827
+ acPower: this.applyScaleFactor(acPowerRaw, scaleFactors.W_SF, 'int16', 'AC Power', 14, 101),
828
+ frequency: this.applyScaleFactor(freqRaw, scaleFactors.Hz_SF, 'uint16', 'Frequency', 16, 101),
829
+ dcCurrent: this.applyScaleFactor(dcCurrentRaw, scaleFactors.DCA_SF, 'uint16', 'DC Current', 27, 101),
830
+ dcVoltage: this.applyScaleFactor(dcVoltageRaw, scaleFactors.DCV_SF, 'uint16', 'DC Voltage', 28, 101),
831
+ dcPower: this.applyScaleFactor(dcPowerRaw, scaleFactors.DCW_SF, 'int16', 'DC Power', 30, 101),
826
832
  operatingState: stateRaw
827
833
  };
828
834
  // Read AC Energy (32-bit accumulator) - Offset 24-25
@@ -839,6 +845,70 @@ export class SunspecModbusClient {
839
845
  return null;
840
846
  }
841
847
  }
848
+ /**
849
+ * Read inverter data from a float-variant model: 111 (single-phase), 112 (split-phase), or 113 (three-phase).
850
+ *
851
+ * All three models share the same SunSpec register layout — measurement fields are 32-bit IEEE 754 floats
852
+ * (no scale factors), status registers stay enum16, event registers stay bitfield32. Phase scope differs
853
+ * by model: 111 populates phase A only; 112 phases A+B; 113 all three. Unpopulated phase fields read NaN
854
+ * and are returned as undefined.
855
+ */
856
+ async readFloatInverterData(unitId, model, blockNumber) {
857
+ try {
858
+ console.debug(`Reading Float Inverter Data from Model ${blockNumber} at base address: ${model.address} (unit ${unitId})`);
859
+ const buffer = await this.readModelBlock(unitId, model);
860
+ const data = {
861
+ blockNumber,
862
+ blockAddress: model.address,
863
+ blockLength: model.length,
864
+ acCurrent: this.extractFloat32OrUndefined(buffer, 2, blockNumber, 'AC Current'),
865
+ phaseACurrent: this.extractFloat32OrUndefined(buffer, 4, blockNumber, 'Phase A Current'),
866
+ phaseBCurrent: this.extractFloat32OrUndefined(buffer, 6, blockNumber, 'Phase B Current'),
867
+ phaseCCurrent: this.extractFloat32OrUndefined(buffer, 8, blockNumber, 'Phase C Current'),
868
+ voltageAB: this.extractFloat32OrUndefined(buffer, 10, blockNumber, 'Voltage AB'),
869
+ voltageBC: this.extractFloat32OrUndefined(buffer, 12, blockNumber, 'Voltage BC'),
870
+ voltageCA: this.extractFloat32OrUndefined(buffer, 14, blockNumber, 'Voltage CA'),
871
+ voltageAN: this.extractFloat32OrUndefined(buffer, 16, blockNumber, 'Voltage AN'),
872
+ voltageBN: this.extractFloat32OrUndefined(buffer, 18, blockNumber, 'Voltage BN'),
873
+ voltageCN: this.extractFloat32OrUndefined(buffer, 20, blockNumber, 'Voltage CN'),
874
+ acPower: this.extractFloat32OrUndefined(buffer, 22, blockNumber, 'AC Power'),
875
+ frequency: this.extractFloat32OrUndefined(buffer, 24, blockNumber, 'Frequency'),
876
+ apparentPower: this.extractFloat32OrUndefined(buffer, 26, blockNumber, 'Apparent Power'),
877
+ reactivePower: this.extractFloat32OrUndefined(buffer, 28, blockNumber, 'Reactive Power'),
878
+ powerFactor: this.extractFloat32OrUndefined(buffer, 30, blockNumber, 'Power Factor'),
879
+ acEnergy: this.extractFloat32OrUndefined(buffer, 32, blockNumber, 'AC Energy'),
880
+ dcCurrent: this.extractFloat32OrUndefined(buffer, 34, blockNumber, 'DC Current'),
881
+ dcVoltage: this.extractFloat32OrUndefined(buffer, 36, blockNumber, 'DC Voltage'),
882
+ dcPower: this.extractFloat32OrUndefined(buffer, 38, blockNumber, 'DC Power'),
883
+ cabinetTemperature: this.extractFloat32OrUndefined(buffer, 40, blockNumber, 'Cabinet Temperature'),
884
+ heatSinkTemperature: this.extractFloat32OrUndefined(buffer, 42, blockNumber, 'Heat Sink Temperature'),
885
+ transformerTemperature: this.extractFloat32OrUndefined(buffer, 44, blockNumber, 'Transformer Temperature'),
886
+ otherTemperature: this.extractFloat32OrUndefined(buffer, 46, blockNumber, 'Other Temperature'),
887
+ operatingState: this.extractValue(buffer, 48, 'uint16'),
888
+ vendorState: this.extractValue(buffer, 49, 'uint16'),
889
+ events: this.extractValue(buffer, 50, 'uint32', 2),
890
+ events2: this.extractValue(buffer, 52, 'uint32', 2),
891
+ vendorEvents1: this.extractValue(buffer, 54, 'uint32', 2),
892
+ vendorEvents2: this.extractValue(buffer, 56, 'uint32', 2),
893
+ vendorEvents3: this.extractValue(buffer, 58, 'uint32', 2),
894
+ vendorEvents4: this.extractValue(buffer, 60, 'uint32', 2),
895
+ };
896
+ this.logRegisterRead(blockNumber, 48, 'Operating State', data.operatingState, 'enum16');
897
+ this.logRegisterRead(blockNumber, 49, 'Vendor State', data.vendorState, 'enum16');
898
+ this.logRegisterRead(blockNumber, 50, 'Events', data.events, 'bitfield32');
899
+ this.logRegisterRead(blockNumber, 52, 'Events2', data.events2, 'bitfield32');
900
+ this.logRegisterRead(blockNumber, 54, 'Vendor Events 1', data.vendorEvents1, 'bitfield32');
901
+ this.logRegisterRead(blockNumber, 56, 'Vendor Events 2', data.vendorEvents2, 'bitfield32');
902
+ this.logRegisterRead(blockNumber, 58, 'Vendor Events 3', data.vendorEvents3, 'bitfield32');
903
+ this.logRegisterRead(blockNumber, 60, 'Vendor Events 4', data.vendorEvents4, 'bitfield32');
904
+ console.debug(`[Model ${blockNumber}] Float Inverter Data:`, data);
905
+ return data;
906
+ }
907
+ catch (error) {
908
+ console.error(`Error reading float inverter data (Model ${blockNumber}): ${error}`);
909
+ return null;
910
+ }
911
+ }
842
912
  /**
843
913
  * Extract inverter scale factors from a pre-read model buffer
844
914
  */
@@ -906,6 +976,16 @@ export class SunspecModbusClient {
906
976
  console.debug(`[Model ${modelId}] offset ${offset}: ${fieldName} = "${rawValue}"${typeInfo}`);
907
977
  }
908
978
  }
979
+ /**
980
+ * Read a float32 (IEEE 754, big-endian, 2 registers) from a model buffer.
981
+ * SunSpec uses NaN as the "unimplemented" sentinel for floats; returns undefined in that case.
982
+ */
983
+ extractFloat32OrUndefined(buffer, offset, modelId, fieldName) {
984
+ const value = this.extractValue(buffer, offset, 'float32', 2);
985
+ const display = Number.isNaN(value) ? 'NaN' : value;
986
+ console.debug(`[Model ${modelId}] offset ${offset}: ${fieldName} = ${display} (float32)`);
987
+ return Number.isNaN(value) ? undefined : value;
988
+ }
909
989
  /**
910
990
  * Read scale factors from Model 160 (MPPT)
911
991
  *
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.52";
8
+ export declare const SDK_VERSION = "0.0.54";
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.52';
8
+ export const SDK_VERSION = '0.0.54';
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.52",
3
+ "version": "0.0.54",
4
4
  "description": "enyo Energy Sunspec SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",