@enyo-energy/sunspec-sdk 0.0.62 → 0.0.63

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.
@@ -699,23 +699,64 @@ class SunspecModbusClient {
699
699
  throw error;
700
700
  }
701
701
  }
702
+ /**
703
+ * SunSpec inverter + meter models come in two encoding families: int+SF (101/103 inverter,
704
+ * 201/203/204 meter) and float (111/112/113 inverter, 211/212/213/214 meter). Some devices
705
+ * advertise BOTH for compatibility. We must pick one family and use it consistently across
706
+ * inverter and meter on the same unit — otherwise we end up decoding the int+SF inverter
707
+ * while reading the float meter (or vice-versa), which is confusing and risks subtle bugs.
708
+ *
709
+ * Rule: count how many models each family contributes to the discovered directory for this
710
+ * unit; use the family with the higher count. On a tie, prefer int+SF (the SDK's historical
711
+ * default and the more common encoding in the wild).
712
+ */
713
+ getPreferredEncoding(unitId) {
714
+ const INT_SF_IDS = [101, 103, 201, 203, 204];
715
+ const FLOAT_IDS = [111, 112, 113, 211, 212, 213, 214];
716
+ const models = this.discoveredModelsByUnit.get(unitId);
717
+ if (!models)
718
+ return 'int_sf';
719
+ let intSfCount = 0;
720
+ let floatCount = 0;
721
+ for (const id of INT_SF_IDS)
722
+ if (models.has(id))
723
+ intSfCount++;
724
+ for (const id of FLOAT_IDS)
725
+ if (models.has(id))
726
+ floatCount++;
727
+ const preferred = floatCount > intSfCount ? 'float' : 'int_sf';
728
+ if (intSfCount > 0 && floatCount > 0) {
729
+ console.debug(`Unit ${unitId} advertises both int+SF (${intSfCount}) and float (${floatCount}) models — ` +
730
+ `using ${preferred} consistently across inverter+meter.`);
731
+ }
732
+ return preferred;
733
+ }
702
734
  /**
703
735
  * Read inverter data. Detects which SunSpec inverter model the device exposes —
704
736
  * int+SF (101/103) or float (111/112/113) — by checking the discovered model directory,
705
737
  * and dispatches to the appropriate decoder.
738
+ *
739
+ * When a device advertises BOTH encodings, picks the family with more discovered models
740
+ * for this unit (see getPreferredEncoding). Never reads both.
706
741
  */
707
742
  async readInverterData(unitId) {
708
- const tryOrder = [
743
+ const intSfOrder = [
709
744
  { id: 103, reader: m => this.readThreePhaseInverterData_IntSF(unitId, m) },
745
+ { id: 101, reader: m => this.readSinglePhaseInverterData(unitId, m) },
746
+ ];
747
+ const floatOrder = [
710
748
  { id: 113, reader: m => this.readFloatInverterData(unitId, m, 113) },
711
749
  { id: 112, reader: m => this.readFloatInverterData(unitId, m, 112) },
712
- { id: 101, reader: m => this.readSinglePhaseInverterData(unitId, m) },
713
750
  { id: 111, reader: m => this.readFloatInverterData(unitId, m, 111) },
714
751
  ];
752
+ const preferred = this.getPreferredEncoding(unitId);
753
+ const tryOrder = preferred === 'float'
754
+ ? [...floatOrder, ...intSfOrder] // fall through to int+SF only if no float model exists
755
+ : [...intSfOrder, ...floatOrder];
715
756
  for (const { id, reader } of tryOrder) {
716
757
  const model = this.findModel(unitId, id);
717
758
  if (model) {
718
- console.debug(`Using inverter Model ${id} at address ${model.address} (length ${model.length}) on unit ${unitId}`);
759
+ console.debug(`Using inverter Model ${id} at address ${model.address} (length ${model.length}) on unit ${unitId} (preferred encoding: ${preferred})`);
719
760
  return reader(model);
720
761
  }
721
762
  }
@@ -1753,27 +1794,46 @@ class SunspecModbusClient {
1753
1794
  * Read meter data. Detects which SunSpec meter model the device exposes —
1754
1795
  * int+SF (201/203/204) or float (211/212/213/214) — by checking the discovered
1755
1796
  * model directory, and dispatches to the appropriate decoder.
1797
+ *
1798
+ * When a device advertises BOTH encodings, picks the family with more discovered models
1799
+ * for this unit (see getPreferredEncoding). Never reads both.
1756
1800
  */
1757
1801
  async readMeterData(unitId) {
1758
- const floatIds = [213, 214, 212, 211];
1759
- for (const id of floatIds) {
1760
- const model = this.findModel(unitId, id);
1761
- if (model) {
1762
- console.debug(`Using meter Model ${id} at address ${model.address} (length ${model.length}) on unit ${unitId}`);
1763
- return this.readFloatMeterData(unitId, model, id);
1802
+ const preferred = this.getPreferredEncoding(unitId);
1803
+ const tryFloat = async () => {
1804
+ const floatIds = [213, 214, 212, 211];
1805
+ for (const id of floatIds) {
1806
+ const model = this.findModel(unitId, id);
1807
+ if (model) {
1808
+ console.debug(`Using meter Model ${id} at address ${model.address} (length ${model.length}) on unit ${unitId} (preferred encoding: ${preferred})`);
1809
+ return this.readFloatMeterData(unitId, model, id);
1810
+ }
1764
1811
  }
1765
- }
1766
- let model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Meter3Phase);
1767
- if (!model) {
1768
- model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MeterWye);
1769
- }
1770
- if (!model) {
1771
- model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MeterSinglePhase);
1772
- }
1773
- if (!model) {
1774
- console.debug(`No meter model found on unit ${unitId}`);
1775
1812
  return null;
1776
- }
1813
+ };
1814
+ const tryIntSf = async () => {
1815
+ let model = this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.Meter3Phase)
1816
+ ?? this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MeterWye)
1817
+ ?? this.findModel(unitId, sunspec_interfaces_js_1.SunspecModelId.MeterSinglePhase);
1818
+ if (!model)
1819
+ return null;
1820
+ console.debug(`Using meter Model ${model.id} at address ${model.address} (length ${model.length}) on unit ${unitId} (preferred encoding: ${preferred})`);
1821
+ return this.readIntSfMeterData(unitId, model);
1822
+ };
1823
+ const primary = preferred === 'float' ? tryFloat : tryIntSf;
1824
+ const fallback = preferred === 'float' ? tryIntSf : tryFloat;
1825
+ const primaryResult = await primary();
1826
+ if (primaryResult)
1827
+ return primaryResult;
1828
+ // Fall back to the other family only when the preferred family has no model at all.
1829
+ return fallback();
1830
+ }
1831
+ /**
1832
+ * Read meter data from an int+SF variant model (201/203/204). Extracted from the original
1833
+ * readMeterData body so the preferred-encoding dispatcher above can branch cleanly without
1834
+ * duplicating ~60 lines of register decoding.
1835
+ */
1836
+ async readIntSfMeterData(unitId, model) {
1777
1837
  console.debug(`Reading Meter Data from Model ${model.id} at base address: ${model.address} (unit ${unitId})`);
1778
1838
  try {
1779
1839
  // Different meter models have different register offsets
@@ -201,10 +201,25 @@ export declare class SunspecModbusClient {
201
201
  * Helper to write register value(s)
202
202
  */
203
203
  writeRegisterValue(unitId: number, address: number, value: number | number[], dataType?: EnergyAppModbusDataType): Promise<boolean>;
204
+ /**
205
+ * SunSpec inverter + meter models come in two encoding families: int+SF (101/103 inverter,
206
+ * 201/203/204 meter) and float (111/112/113 inverter, 211/212/213/214 meter). Some devices
207
+ * advertise BOTH for compatibility. We must pick one family and use it consistently across
208
+ * inverter and meter on the same unit — otherwise we end up decoding the int+SF inverter
209
+ * while reading the float meter (or vice-versa), which is confusing and risks subtle bugs.
210
+ *
211
+ * Rule: count how many models each family contributes to the discovered directory for this
212
+ * unit; use the family with the higher count. On a tie, prefer int+SF (the SDK's historical
213
+ * default and the more common encoding in the wild).
214
+ */
215
+ private getPreferredEncoding;
204
216
  /**
205
217
  * Read inverter data. Detects which SunSpec inverter model the device exposes —
206
218
  * int+SF (101/103) or float (111/112/113) — by checking the discovered model directory,
207
219
  * and dispatches to the appropriate decoder.
220
+ *
221
+ * When a device advertises BOTH encodings, picks the family with more discovered models
222
+ * for this unit (see getPreferredEncoding). Never reads both.
208
223
  */
209
224
  readInverterData(unitId: number): Promise<SunspecInverterData | null>;
210
225
  /**
@@ -318,8 +333,17 @@ export declare class SunspecModbusClient {
318
333
  * Read meter data. Detects which SunSpec meter model the device exposes —
319
334
  * int+SF (201/203/204) or float (211/212/213/214) — by checking the discovered
320
335
  * model directory, and dispatches to the appropriate decoder.
336
+ *
337
+ * When a device advertises BOTH encodings, picks the family with more discovered models
338
+ * for this unit (see getPreferredEncoding). Never reads both.
321
339
  */
322
340
  readMeterData(unitId: number): Promise<SunspecMeterData | null>;
341
+ /**
342
+ * Read meter data from an int+SF variant model (201/203/204). Extracted from the original
343
+ * readMeterData body so the preferred-encoding dispatcher above can branch cleanly without
344
+ * duplicating ~60 lines of register decoding.
345
+ */
346
+ private readIntSfMeterData;
323
347
  /**
324
348
  * Read meter data from a float-variant model: 211 (single-phase), 212 (split-phase),
325
349
  * 213 (3-phase Wye), or 214 (3-phase Delta).
@@ -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.62';
12
+ exports.SDK_VERSION = '0.0.63';
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.62";
8
+ export declare const SDK_VERSION = "0.0.63";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
@@ -201,10 +201,25 @@ export declare class SunspecModbusClient {
201
201
  * Helper to write register value(s)
202
202
  */
203
203
  writeRegisterValue(unitId: number, address: number, value: number | number[], dataType?: EnergyAppModbusDataType): Promise<boolean>;
204
+ /**
205
+ * SunSpec inverter + meter models come in two encoding families: int+SF (101/103 inverter,
206
+ * 201/203/204 meter) and float (111/112/113 inverter, 211/212/213/214 meter). Some devices
207
+ * advertise BOTH for compatibility. We must pick one family and use it consistently across
208
+ * inverter and meter on the same unit — otherwise we end up decoding the int+SF inverter
209
+ * while reading the float meter (or vice-versa), which is confusing and risks subtle bugs.
210
+ *
211
+ * Rule: count how many models each family contributes to the discovered directory for this
212
+ * unit; use the family with the higher count. On a tie, prefer int+SF (the SDK's historical
213
+ * default and the more common encoding in the wild).
214
+ */
215
+ private getPreferredEncoding;
204
216
  /**
205
217
  * Read inverter data. Detects which SunSpec inverter model the device exposes —
206
218
  * int+SF (101/103) or float (111/112/113) — by checking the discovered model directory,
207
219
  * and dispatches to the appropriate decoder.
220
+ *
221
+ * When a device advertises BOTH encodings, picks the family with more discovered models
222
+ * for this unit (see getPreferredEncoding). Never reads both.
208
223
  */
209
224
  readInverterData(unitId: number): Promise<SunspecInverterData | null>;
210
225
  /**
@@ -318,8 +333,17 @@ export declare class SunspecModbusClient {
318
333
  * Read meter data. Detects which SunSpec meter model the device exposes —
319
334
  * int+SF (201/203/204) or float (211/212/213/214) — by checking the discovered
320
335
  * model directory, and dispatches to the appropriate decoder.
336
+ *
337
+ * When a device advertises BOTH encodings, picks the family with more discovered models
338
+ * for this unit (see getPreferredEncoding). Never reads both.
321
339
  */
322
340
  readMeterData(unitId: number): Promise<SunspecMeterData | null>;
341
+ /**
342
+ * Read meter data from an int+SF variant model (201/203/204). Extracted from the original
343
+ * readMeterData body so the preferred-encoding dispatcher above can branch cleanly without
344
+ * duplicating ~60 lines of register decoding.
345
+ */
346
+ private readIntSfMeterData;
323
347
  /**
324
348
  * Read meter data from a float-variant model: 211 (single-phase), 212 (split-phase),
325
349
  * 213 (3-phase Wye), or 214 (3-phase Delta).
@@ -694,23 +694,64 @@ export class SunspecModbusClient {
694
694
  throw error;
695
695
  }
696
696
  }
697
+ /**
698
+ * SunSpec inverter + meter models come in two encoding families: int+SF (101/103 inverter,
699
+ * 201/203/204 meter) and float (111/112/113 inverter, 211/212/213/214 meter). Some devices
700
+ * advertise BOTH for compatibility. We must pick one family and use it consistently across
701
+ * inverter and meter on the same unit — otherwise we end up decoding the int+SF inverter
702
+ * while reading the float meter (or vice-versa), which is confusing and risks subtle bugs.
703
+ *
704
+ * Rule: count how many models each family contributes to the discovered directory for this
705
+ * unit; use the family with the higher count. On a tie, prefer int+SF (the SDK's historical
706
+ * default and the more common encoding in the wild).
707
+ */
708
+ getPreferredEncoding(unitId) {
709
+ const INT_SF_IDS = [101, 103, 201, 203, 204];
710
+ const FLOAT_IDS = [111, 112, 113, 211, 212, 213, 214];
711
+ const models = this.discoveredModelsByUnit.get(unitId);
712
+ if (!models)
713
+ return 'int_sf';
714
+ let intSfCount = 0;
715
+ let floatCount = 0;
716
+ for (const id of INT_SF_IDS)
717
+ if (models.has(id))
718
+ intSfCount++;
719
+ for (const id of FLOAT_IDS)
720
+ if (models.has(id))
721
+ floatCount++;
722
+ const preferred = floatCount > intSfCount ? 'float' : 'int_sf';
723
+ if (intSfCount > 0 && floatCount > 0) {
724
+ console.debug(`Unit ${unitId} advertises both int+SF (${intSfCount}) and float (${floatCount}) models — ` +
725
+ `using ${preferred} consistently across inverter+meter.`);
726
+ }
727
+ return preferred;
728
+ }
697
729
  /**
698
730
  * Read inverter data. Detects which SunSpec inverter model the device exposes —
699
731
  * int+SF (101/103) or float (111/112/113) — by checking the discovered model directory,
700
732
  * and dispatches to the appropriate decoder.
733
+ *
734
+ * When a device advertises BOTH encodings, picks the family with more discovered models
735
+ * for this unit (see getPreferredEncoding). Never reads both.
701
736
  */
702
737
  async readInverterData(unitId) {
703
- const tryOrder = [
738
+ const intSfOrder = [
704
739
  { id: 103, reader: m => this.readThreePhaseInverterData_IntSF(unitId, m) },
740
+ { id: 101, reader: m => this.readSinglePhaseInverterData(unitId, m) },
741
+ ];
742
+ const floatOrder = [
705
743
  { id: 113, reader: m => this.readFloatInverterData(unitId, m, 113) },
706
744
  { id: 112, reader: m => this.readFloatInverterData(unitId, m, 112) },
707
- { id: 101, reader: m => this.readSinglePhaseInverterData(unitId, m) },
708
745
  { id: 111, reader: m => this.readFloatInverterData(unitId, m, 111) },
709
746
  ];
747
+ const preferred = this.getPreferredEncoding(unitId);
748
+ const tryOrder = preferred === 'float'
749
+ ? [...floatOrder, ...intSfOrder] // fall through to int+SF only if no float model exists
750
+ : [...intSfOrder, ...floatOrder];
710
751
  for (const { id, reader } of tryOrder) {
711
752
  const model = this.findModel(unitId, id);
712
753
  if (model) {
713
- console.debug(`Using inverter Model ${id} at address ${model.address} (length ${model.length}) on unit ${unitId}`);
754
+ console.debug(`Using inverter Model ${id} at address ${model.address} (length ${model.length}) on unit ${unitId} (preferred encoding: ${preferred})`);
714
755
  return reader(model);
715
756
  }
716
757
  }
@@ -1748,27 +1789,46 @@ export class SunspecModbusClient {
1748
1789
  * Read meter data. Detects which SunSpec meter model the device exposes —
1749
1790
  * int+SF (201/203/204) or float (211/212/213/214) — by checking the discovered
1750
1791
  * model directory, and dispatches to the appropriate decoder.
1792
+ *
1793
+ * When a device advertises BOTH encodings, picks the family with more discovered models
1794
+ * for this unit (see getPreferredEncoding). Never reads both.
1751
1795
  */
1752
1796
  async readMeterData(unitId) {
1753
- const floatIds = [213, 214, 212, 211];
1754
- for (const id of floatIds) {
1755
- const model = this.findModel(unitId, id);
1756
- if (model) {
1757
- console.debug(`Using meter Model ${id} at address ${model.address} (length ${model.length}) on unit ${unitId}`);
1758
- return this.readFloatMeterData(unitId, model, id);
1797
+ const preferred = this.getPreferredEncoding(unitId);
1798
+ const tryFloat = async () => {
1799
+ const floatIds = [213, 214, 212, 211];
1800
+ for (const id of floatIds) {
1801
+ const model = this.findModel(unitId, id);
1802
+ if (model) {
1803
+ console.debug(`Using meter Model ${id} at address ${model.address} (length ${model.length}) on unit ${unitId} (preferred encoding: ${preferred})`);
1804
+ return this.readFloatMeterData(unitId, model, id);
1805
+ }
1759
1806
  }
1760
- }
1761
- let model = this.findModel(unitId, SunspecModelId.Meter3Phase);
1762
- if (!model) {
1763
- model = this.findModel(unitId, SunspecModelId.MeterWye);
1764
- }
1765
- if (!model) {
1766
- model = this.findModel(unitId, SunspecModelId.MeterSinglePhase);
1767
- }
1768
- if (!model) {
1769
- console.debug(`No meter model found on unit ${unitId}`);
1770
1807
  return null;
1771
- }
1808
+ };
1809
+ const tryIntSf = async () => {
1810
+ let model = this.findModel(unitId, SunspecModelId.Meter3Phase)
1811
+ ?? this.findModel(unitId, SunspecModelId.MeterWye)
1812
+ ?? this.findModel(unitId, SunspecModelId.MeterSinglePhase);
1813
+ if (!model)
1814
+ return null;
1815
+ console.debug(`Using meter Model ${model.id} at address ${model.address} (length ${model.length}) on unit ${unitId} (preferred encoding: ${preferred})`);
1816
+ return this.readIntSfMeterData(unitId, model);
1817
+ };
1818
+ const primary = preferred === 'float' ? tryFloat : tryIntSf;
1819
+ const fallback = preferred === 'float' ? tryIntSf : tryFloat;
1820
+ const primaryResult = await primary();
1821
+ if (primaryResult)
1822
+ return primaryResult;
1823
+ // Fall back to the other family only when the preferred family has no model at all.
1824
+ return fallback();
1825
+ }
1826
+ /**
1827
+ * Read meter data from an int+SF variant model (201/203/204). Extracted from the original
1828
+ * readMeterData body so the preferred-encoding dispatcher above can branch cleanly without
1829
+ * duplicating ~60 lines of register decoding.
1830
+ */
1831
+ async readIntSfMeterData(unitId, model) {
1772
1832
  console.debug(`Reading Meter Data from Model ${model.id} at base address: ${model.address} (unit ${unitId})`);
1773
1833
  try {
1774
1834
  // Different meter models have different register offsets
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.62";
8
+ export declare const SDK_VERSION = "0.0.63";
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.62';
8
+ export const SDK_VERSION = '0.0.63';
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.62",
3
+ "version": "0.0.63",
4
4
  "description": "enyo Energy Sunspec SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",