@enyo-energy/sunspec-sdk 0.0.75 → 0.0.76

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.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SunspecMeter = exports.SunspecBattery = exports.SunspecInverter = exports.BaseSunspecDevice = void 0;
4
4
  exports.detectFeaturesFromRegisters = detectFeaturesFromRegisters;
5
+ exports.selectLiveBatteryPowerW = selectLiveBatteryPowerW;
5
6
  exports.filterFeaturesByCalibrationResult = filterFeaturesByCalibrationResult;
6
7
  const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
7
8
  const node_crypto_1 = require("node:crypto");
@@ -852,6 +853,35 @@ function detectFeaturesFromRegisters(batteryData) {
852
853
  * the full detected set to preserve the old all-or-nothing semantics on
853
854
  * upgrade.
854
855
  */
856
+ /**
857
+ * Pick the live battery power, in preference order:
858
+ *
859
+ * 1. Model 160 (MPPT) `StCha 3` / `StDisCha 4` — direct DC-link reading,
860
+ * passed in as the already-summed `mpptBatteryPowerW`.
861
+ * 2. Model 802 `w` (offset 47) — surfaced as `chargePower` / `dischargePower`
862
+ * by the Model 802 path of `SunspecModbusClient.readBatteryData`.
863
+ * 3. `undefined` — no reliable live source on this device.
864
+ *
865
+ * The Model 124 path of `readBatteryData` no longer manufactures
866
+ * `chargePower` / `dischargePower` from `inWRte × wChaMax` (a commanded rate
867
+ * cap, not a live measurement). So whenever this function sees
868
+ * `chargePower` / `dischargePower` defined, they came from Model 802.
869
+ *
870
+ * Exported as a free function so the preference logic can be unit-tested
871
+ * without the full `SunspecBattery` scaffold.
872
+ */
873
+ function selectLiveBatteryPowerW(mpptBatteryPowerW, batteryData) {
874
+ if (mpptBatteryPowerW !== undefined) {
875
+ return mpptBatteryPowerW;
876
+ }
877
+ if (!batteryData) {
878
+ return undefined;
879
+ }
880
+ if (batteryData.chargePower === undefined && batteryData.dischargePower === undefined) {
881
+ return undefined;
882
+ }
883
+ return (batteryData.chargePower ?? 0) - (batteryData.dischargePower ?? 0);
884
+ }
855
885
  function filterFeaturesByCalibrationResult(detected, result, controllable) {
856
886
  if (!result || result.state !== 'calibrated') {
857
887
  return detected.filter(f => !controllable.includes(f));
@@ -1115,17 +1145,8 @@ class SunspecBattery extends BaseSunspecDevice {
1115
1145
  return messages;
1116
1146
  }
1117
1147
  if (batteryData) {
1118
- const advancedBatteryModel = this.sunspecClient.findModel(this.unitId, 801);
1119
- const batteryBaseModel = this.sunspecClient.findModel(this.unitId, sunspec_interfaces_js_1.SunspecModelId.BatteryBase);
1120
- // Determine battery power: prefer model 802 w field, then MPPT extraction, then undefined
1121
- let batteryPowerW;
1122
- if (batteryBaseModel && (batteryData.chargePower !== undefined || batteryData.dischargePower !== undefined)) {
1123
- // Model 802 provides power directly: positive = charge, negative = discharge
1124
- batteryPowerW = (batteryData.chargePower || 0) - (batteryData.dischargePower || 0);
1125
- }
1126
- else if (!advancedBatteryModel) {
1127
- batteryPowerW = mpptBatteryPowerW;
1128
- }
1148
+ // See `selectLiveBatteryPowerW` for the source-preference rationale.
1149
+ const batteryPowerW = selectLiveBatteryPowerW(mpptBatteryPowerW, batteryData);
1129
1150
  // Feed the calibration driver's synchronous power cache. No-op when calibration
1130
1151
  // was never configured.
1131
1152
  if (batteryPowerW !== undefined && this.calibrationDriver) {
@@ -215,6 +215,24 @@ export declare function detectFeaturesFromRegisters(batteryData: SunspecBatteryD
215
215
  * the full detected set to preserve the old all-or-nothing semantics on
216
216
  * upgrade.
217
217
  */
218
+ /**
219
+ * Pick the live battery power, in preference order:
220
+ *
221
+ * 1. Model 160 (MPPT) `StCha 3` / `StDisCha 4` — direct DC-link reading,
222
+ * passed in as the already-summed `mpptBatteryPowerW`.
223
+ * 2. Model 802 `w` (offset 47) — surfaced as `chargePower` / `dischargePower`
224
+ * by the Model 802 path of `SunspecModbusClient.readBatteryData`.
225
+ * 3. `undefined` — no reliable live source on this device.
226
+ *
227
+ * The Model 124 path of `readBatteryData` no longer manufactures
228
+ * `chargePower` / `dischargePower` from `inWRte × wChaMax` (a commanded rate
229
+ * cap, not a live measurement). So whenever this function sees
230
+ * `chargePower` / `dischargePower` defined, they came from Model 802.
231
+ *
232
+ * Exported as a free function so the preference logic can be unit-tested
233
+ * without the full `SunspecBattery` scaffold.
234
+ */
235
+ export declare function selectLiveBatteryPowerW(mpptBatteryPowerW: number | undefined, batteryData: SunspecBatteryData | null): number | undefined;
218
236
  export declare function filterFeaturesByCalibrationResult(detected: EnyoBatteryFeature[], result: CalibrationResult | undefined, controllable: readonly EnyoBatteryFeature[]): EnyoBatteryFeature[];
219
237
  /**
220
238
  * Sunspec Battery implementation
@@ -1596,16 +1596,17 @@ class SunspecModbusClient {
1596
1596
  voltage: this.applyScaleFactor(inBatVRaw, scaleFactors.InBatV_SF, 'uint16'),
1597
1597
  status: !this.isUnimplementedValue(chaStRaw, 'enum16') ? chaStRaw : undefined
1598
1598
  };
1599
- // Calculate charge/discharge power if rates are available
1600
- if (data.inWRte !== undefined && data.wChaMax !== undefined) {
1601
- data.chargePower = (data.inWRte / 100) * data.wChaMax;
1602
- console.debug(`Calculated Charge Power: (inWRte: ${data.inWRte}, wChaMax: ${data.wChaMax}) ${data.chargePower?.toFixed(2)} W`);
1603
- }
1604
- if (data.outWRte !== undefined && data.wChaMax !== undefined) {
1605
- // Assuming WDisChaMax is similar to WChaMax for simplicity
1606
- data.dischargePower = Math.abs((data.outWRte / 100) * data.wChaMax);
1607
- console.debug(`Calculated Discharge Power (inWRte: ${data.outWRte}, wChaMax: ${data.wChaMax}): ${data.dischargePower?.toFixed(2)} W`);
1608
- }
1599
+ // chargePower / dischargePower used to be derived from
1600
+ // `(inWRte / 100) * wChaMax` and `(outWRte / 100) * wChaMax`
1601
+ // here. That formula uses Model 124's COMMANDED RATE CAPS — not
1602
+ // a live measurement and produces e.g. 5000 W of "charging"
1603
+ // for an idle battery with inWRte = 100 % and a 5 kW nameplate.
1604
+ // Live battery power must come from a real source: Model 160
1605
+ // StCha 3 / StDisCha 4 (see SunspecBattery.extractBatteryPower
1606
+ // FromMPPT) or Model 802 `w` at offset 47 (the Model 802 branch
1607
+ // of this method, just below). Model-124-only devices should
1608
+ // publish "no signal" rather than the commanded cap.
1609
+ // See plans/please-review-the-implementation-groovy-thacker.md.
1609
1610
  // Single-line JSON debug dump — Node's default formatter would split
1610
1611
  // the object across many lines; stringify keeps the whole snapshot
1611
1612
  // on one log entry so it stays grep-able and round-trippable.
@@ -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.75';
12
+ exports.SDK_VERSION = '0.0.76';
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.75";
8
+ export declare const SDK_VERSION = "0.0.76";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
@@ -215,6 +215,24 @@ export declare function detectFeaturesFromRegisters(batteryData: SunspecBatteryD
215
215
  * the full detected set to preserve the old all-or-nothing semantics on
216
216
  * upgrade.
217
217
  */
218
+ /**
219
+ * Pick the live battery power, in preference order:
220
+ *
221
+ * 1. Model 160 (MPPT) `StCha 3` / `StDisCha 4` — direct DC-link reading,
222
+ * passed in as the already-summed `mpptBatteryPowerW`.
223
+ * 2. Model 802 `w` (offset 47) — surfaced as `chargePower` / `dischargePower`
224
+ * by the Model 802 path of `SunspecModbusClient.readBatteryData`.
225
+ * 3. `undefined` — no reliable live source on this device.
226
+ *
227
+ * The Model 124 path of `readBatteryData` no longer manufactures
228
+ * `chargePower` / `dischargePower` from `inWRte × wChaMax` (a commanded rate
229
+ * cap, not a live measurement). So whenever this function sees
230
+ * `chargePower` / `dischargePower` defined, they came from Model 802.
231
+ *
232
+ * Exported as a free function so the preference logic can be unit-tested
233
+ * without the full `SunspecBattery` scaffold.
234
+ */
235
+ export declare function selectLiveBatteryPowerW(mpptBatteryPowerW: number | undefined, batteryData: SunspecBatteryData | null): number | undefined;
218
236
  export declare function filterFeaturesByCalibrationResult(detected: EnyoBatteryFeature[], result: CalibrationResult | undefined, controllable: readonly EnyoBatteryFeature[]): EnyoBatteryFeature[];
219
237
  /**
220
238
  * Sunspec Battery implementation
@@ -845,6 +845,35 @@ export function detectFeaturesFromRegisters(batteryData) {
845
845
  * the full detected set to preserve the old all-or-nothing semantics on
846
846
  * upgrade.
847
847
  */
848
+ /**
849
+ * Pick the live battery power, in preference order:
850
+ *
851
+ * 1. Model 160 (MPPT) `StCha 3` / `StDisCha 4` — direct DC-link reading,
852
+ * passed in as the already-summed `mpptBatteryPowerW`.
853
+ * 2. Model 802 `w` (offset 47) — surfaced as `chargePower` / `dischargePower`
854
+ * by the Model 802 path of `SunspecModbusClient.readBatteryData`.
855
+ * 3. `undefined` — no reliable live source on this device.
856
+ *
857
+ * The Model 124 path of `readBatteryData` no longer manufactures
858
+ * `chargePower` / `dischargePower` from `inWRte × wChaMax` (a commanded rate
859
+ * cap, not a live measurement). So whenever this function sees
860
+ * `chargePower` / `dischargePower` defined, they came from Model 802.
861
+ *
862
+ * Exported as a free function so the preference logic can be unit-tested
863
+ * without the full `SunspecBattery` scaffold.
864
+ */
865
+ export function selectLiveBatteryPowerW(mpptBatteryPowerW, batteryData) {
866
+ if (mpptBatteryPowerW !== undefined) {
867
+ return mpptBatteryPowerW;
868
+ }
869
+ if (!batteryData) {
870
+ return undefined;
871
+ }
872
+ if (batteryData.chargePower === undefined && batteryData.dischargePower === undefined) {
873
+ return undefined;
874
+ }
875
+ return (batteryData.chargePower ?? 0) - (batteryData.dischargePower ?? 0);
876
+ }
848
877
  export function filterFeaturesByCalibrationResult(detected, result, controllable) {
849
878
  if (!result || result.state !== 'calibrated') {
850
879
  return detected.filter(f => !controllable.includes(f));
@@ -1108,17 +1137,8 @@ export class SunspecBattery extends BaseSunspecDevice {
1108
1137
  return messages;
1109
1138
  }
1110
1139
  if (batteryData) {
1111
- const advancedBatteryModel = this.sunspecClient.findModel(this.unitId, 801);
1112
- const batteryBaseModel = this.sunspecClient.findModel(this.unitId, SunspecModelId.BatteryBase);
1113
- // Determine battery power: prefer model 802 w field, then MPPT extraction, then undefined
1114
- let batteryPowerW;
1115
- if (batteryBaseModel && (batteryData.chargePower !== undefined || batteryData.dischargePower !== undefined)) {
1116
- // Model 802 provides power directly: positive = charge, negative = discharge
1117
- batteryPowerW = (batteryData.chargePower || 0) - (batteryData.dischargePower || 0);
1118
- }
1119
- else if (!advancedBatteryModel) {
1120
- batteryPowerW = mpptBatteryPowerW;
1121
- }
1140
+ // See `selectLiveBatteryPowerW` for the source-preference rationale.
1141
+ const batteryPowerW = selectLiveBatteryPowerW(mpptBatteryPowerW, batteryData);
1122
1142
  // Feed the calibration driver's synchronous power cache. No-op when calibration
1123
1143
  // was never configured.
1124
1144
  if (batteryPowerW !== undefined && this.calibrationDriver) {
@@ -1591,16 +1591,17 @@ export class SunspecModbusClient {
1591
1591
  voltage: this.applyScaleFactor(inBatVRaw, scaleFactors.InBatV_SF, 'uint16'),
1592
1592
  status: !this.isUnimplementedValue(chaStRaw, 'enum16') ? chaStRaw : undefined
1593
1593
  };
1594
- // Calculate charge/discharge power if rates are available
1595
- if (data.inWRte !== undefined && data.wChaMax !== undefined) {
1596
- data.chargePower = (data.inWRte / 100) * data.wChaMax;
1597
- console.debug(`Calculated Charge Power: (inWRte: ${data.inWRte}, wChaMax: ${data.wChaMax}) ${data.chargePower?.toFixed(2)} W`);
1598
- }
1599
- if (data.outWRte !== undefined && data.wChaMax !== undefined) {
1600
- // Assuming WDisChaMax is similar to WChaMax for simplicity
1601
- data.dischargePower = Math.abs((data.outWRte / 100) * data.wChaMax);
1602
- console.debug(`Calculated Discharge Power (inWRte: ${data.outWRte}, wChaMax: ${data.wChaMax}): ${data.dischargePower?.toFixed(2)} W`);
1603
- }
1594
+ // chargePower / dischargePower used to be derived from
1595
+ // `(inWRte / 100) * wChaMax` and `(outWRte / 100) * wChaMax`
1596
+ // here. That formula uses Model 124's COMMANDED RATE CAPS — not
1597
+ // a live measurement and produces e.g. 5000 W of "charging"
1598
+ // for an idle battery with inWRte = 100 % and a 5 kW nameplate.
1599
+ // Live battery power must come from a real source: Model 160
1600
+ // StCha 3 / StDisCha 4 (see SunspecBattery.extractBatteryPower
1601
+ // FromMPPT) or Model 802 `w` at offset 47 (the Model 802 branch
1602
+ // of this method, just below). Model-124-only devices should
1603
+ // publish "no signal" rather than the commanded cap.
1604
+ // See plans/please-review-the-implementation-groovy-thacker.md.
1604
1605
  // Single-line JSON debug dump — Node's default formatter would split
1605
1606
  // the object across many lines; stringify keeps the whole snapshot
1606
1607
  // on one log entry so it stays grep-able and round-trippable.
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.75";
8
+ export declare const SDK_VERSION = "0.0.76";
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.75';
8
+ export const SDK_VERSION = '0.0.76';
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.75",
3
+ "version": "0.0.76",
4
4
  "description": "enyo Energy Sunspec SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",