@enyo-energy/sunspec-sdk 0.0.67 → 0.0.69

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.
package/README.md CHANGED
@@ -4,6 +4,7 @@ SunSpec Modbus client for reading data from solar inverters, batteries, meters,
4
4
 
5
5
  ## Table of Contents
6
6
 
7
+ - [Appliance Manager Integration](#appliance-manager-integration)
7
8
  - [How Addressing Works](#how-addressing-works)
8
9
  - [Bulk Register Reading](#bulk-register-reading)
9
10
  - [Data Types](#data-types)
@@ -22,6 +23,29 @@ SunSpec Modbus client for reading data from solar inverters, batteries, meters,
22
23
 
23
24
  ---
24
25
 
26
+ ## Appliance Manager Integration
27
+
28
+ `SunspecInverter`, `SunspecBattery`, and `SunspecMeter` take an `ApplianceManager` from `@enyo-energy/energy-app-sdk` and call `createOrUpdateAppliance` with the device's `networkDevice` plus the common block's `serialNumber` (which may be empty for some firmware revisions).
29
+
30
+ Since `@enyo-energy/energy-app-sdk` 0.0.135 the default identifier strategy is `SerialNumberStrategy`. If a device's common block has no serial number, `createOrUpdateAppliance` throws `MissingIdentifierError` and the SDK logs the error and skips appliance creation.
31
+
32
+ Consumers whose devices may not expose a serial number should configure a fallback when initializing the `ApplianceManager`, e.g.:
33
+
34
+ ```ts
35
+ import { ApplianceManager, FallbackIdentifierStrategy, SerialNumberStrategy, HostnameStrategy } from '@enyo-energy/energy-app-sdk';
36
+
37
+ const applianceManager = await ApplianceManager.initialize(energyApp, {
38
+ identifierStrategy: new FallbackIdentifierStrategy([
39
+ new SerialNumberStrategy(),
40
+ new HostnameStrategy(),
41
+ ]),
42
+ });
43
+ ```
44
+
45
+ This SDK does not configure the strategy itself — it is the consumer app's responsibility.
46
+
47
+ ---
48
+
25
49
  ## How Addressing Works
26
50
 
27
51
  ### Base Address Detection
@@ -278,7 +278,9 @@ class SunspecInverter extends BaseSunspecDevice {
278
278
  // Get device info from common block
279
279
  const commonData = await this.sunspecClient.readCommonBlock(this.unitId);
280
280
  const inverterSettings = await this.sunspecClient.readInverterSettings(this.unitId);
281
+ const inverterControls = await this.sunspecClient.readInverterControls(this.unitId);
281
282
  const mpptDataList = await this.sunspecClient.readAllMPPTData(this.unitId);
283
+ const activeProductionLimitationW = this.computeActiveProductionLimitW(inverterSettings, inverterControls);
282
284
  // Create or update appliance (skip if an existing appliance was provided)
283
285
  if (!this.applianceId) {
284
286
  try {
@@ -296,7 +298,8 @@ class SunspecInverter extends BaseSunspecDevice {
296
298
  },
297
299
  inverter: {
298
300
  dcStrings: this.mapDcStringToApplianceMetadata(this.mapMPPTToStrings(mpptDataList)),
299
- maxPvProductionW: inverterSettings?.WMax
301
+ maxPvProductionW: inverterSettings?.WMax,
302
+ activeProductionLimitationW: activeProductionLimitationW
300
303
  }
301
304
  });
302
305
  console.log(`Sunspec Inverter connected: ${this.networkDevice.hostname} (${this.applianceId})`);
@@ -318,7 +321,8 @@ class SunspecInverter extends BaseSunspecDevice {
318
321
  inverter: {
319
322
  ...existingAppliance?.inverter,
320
323
  dcStrings: this.mapDcStringToApplianceMetadata(this.mapMPPTToStrings(mpptDataList)),
321
- maxPvProductionW: inverterSettings?.WMax
324
+ maxPvProductionW: inverterSettings?.WMax,
325
+ activeProductionLimitationW: activeProductionLimitationW
322
326
  }
323
327
  });
324
328
  console.log(`Sunspec Inverter connected: ${this.networkDevice.hostname} (${this.applianceId})`);
@@ -338,7 +342,12 @@ class SunspecInverter extends BaseSunspecDevice {
338
342
  async disconnect() {
339
343
  this.stopDataBusListening();
340
344
  if (this.applianceId) {
341
- await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
345
+ try {
346
+ await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
347
+ }
348
+ catch (error) {
349
+ console.error(`Failed to mark inverter offline on disconnect: ${error}`);
350
+ }
342
351
  }
343
352
  // Note: We don't disconnect the sunspecClient as it may be shared
344
353
  }
@@ -355,7 +364,9 @@ class SunspecInverter extends BaseSunspecDevice {
355
364
  const inverterData = await this.sunspecClient.readInverterData(this.unitId);
356
365
  const mpptDataList = await this.sunspecClient.readAllMPPTData(this.unitId);
357
366
  const inverterSettings = await this.sunspecClient.readInverterSettings(this.unitId);
367
+ const inverterControls = await this.sunspecClient.readInverterControls(this.unitId);
358
368
  const dcStrings = this.mapMPPTToStrings(mpptDataList);
369
+ const activeProductionLimitationW = this.computeActiveProductionLimitW(inverterSettings, inverterControls);
359
370
  // SDK readers swallow modbus errors and return null/[]; detect that here so the
360
371
  // appliance is marked offline and the retry manager starts its backoff.
361
372
  if (await this.markOfflineIfUnhealthy()) {
@@ -396,7 +407,8 @@ class SunspecInverter extends BaseSunspecDevice {
396
407
  await this.applianceManager.updateAppliance(this.applianceId, {
397
408
  inverter: {
398
409
  ...appliance?.inverter,
399
- dcStrings: this.mapDcStringToApplianceMetadata(dcStrings)
410
+ dcStrings: this.mapDcStringToApplianceMetadata(dcStrings),
411
+ activeProductionLimitationW: activeProductionLimitationW
400
412
  }
401
413
  });
402
414
  }
@@ -568,6 +580,25 @@ class SunspecInverter extends BaseSunspecDevice {
568
580
  console.log(`Inverter ${this.applianceId}: emitting faulted (${sunspec_interfaces_js_1.SUNSPEC_CONNECTION_LOST_CODE}) after ${consecutiveFailures} consecutive reconnect failures`);
569
581
  this.dataBus.sendMessage([message]);
570
582
  }
583
+ /**
584
+ * Compute the currently active feed-in / production limit in Watts from the
585
+ * Model 121 settings (WMax) and Model 123 controls (WMaxLim_Ena, WMaxLimPct).
586
+ * Returns null when no limit is applied, when the registers cannot be read,
587
+ * or when the limit equals WMax (i.e. not actually throttling) — null is
588
+ * returned (not undefined) so the field is cleared on the appliance.
589
+ */
590
+ computeActiveProductionLimitW(settings, controls) {
591
+ if (!settings?.WMax || !controls)
592
+ return null;
593
+ if (controls.WMaxLim_Ena !== sunspec_interfaces_js_1.SunspecEnableControl.ENABLED)
594
+ return null;
595
+ if (controls.WMaxLimPct === undefined)
596
+ return null;
597
+ const limitW = (settings.WMax * controls.WMaxLimPct) / 100;
598
+ if (limitW >= settings.WMax)
599
+ return null;
600
+ return limitW;
601
+ }
571
602
  mapOperatingState(state) {
572
603
  if (!state)
573
604
  return enyo_data_bus_value_js_1.EnyoInverterStateEnum.Off;
@@ -726,6 +757,8 @@ class SunspecBattery extends BaseSunspecDevice {
726
757
  if (batteryData?.wChaMax !== undefined) {
727
758
  features.push(enyo_battery_appliance_js_1.EnyoBatteryFeature.ChargeLimitation);
728
759
  }
760
+ const activeChargeLimitW = this.computeActiveChargeLimitW(batteryData);
761
+ const activeDischargeLimitW = this.computeActiveDischargeLimitW(batteryData);
729
762
  // Create or update appliance (skip if an existing appliance was provided)
730
763
  if (!this.applianceId) {
731
764
  try {
@@ -746,6 +779,8 @@ class SunspecBattery extends BaseSunspecDevice {
746
779
  storageMode: this.mapToEnyoStorageMode(storageMode),
747
780
  maxChargingPowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.inWRte !== undefined ? batteryData.wChaMax * batteryData.inWRte : undefined,
748
781
  maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
782
+ activeChargeLimitW: activeChargeLimitW,
783
+ activeDischargeLimitW: activeDischargeLimitW,
749
784
  features,
750
785
  gridChargingEnabled: batteryData?.chaGriSet === 1
751
786
  }
@@ -772,6 +807,8 @@ class SunspecBattery extends BaseSunspecDevice {
772
807
  storageMode: this.mapToEnyoStorageMode(storageMode),
773
808
  maxChargingPowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.inWRte !== undefined ? batteryData.wChaMax * batteryData.inWRte : undefined,
774
809
  maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
810
+ activeChargeLimitW: activeChargeLimitW,
811
+ activeDischargeLimitW: activeDischargeLimitW,
775
812
  features,
776
813
  gridChargingEnabled: batteryData?.chaGriSet === 1
777
814
  }
@@ -787,7 +824,12 @@ class SunspecBattery extends BaseSunspecDevice {
787
824
  async disconnect() {
788
825
  this.stopDataBusListening();
789
826
  if (this.applianceId) {
790
- await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
827
+ try {
828
+ await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
829
+ }
830
+ catch (error) {
831
+ console.error(`Failed to mark battery offline on disconnect: ${error}`);
832
+ }
791
833
  }
792
834
  }
793
835
  /**
@@ -878,6 +920,8 @@ class SunspecBattery extends BaseSunspecDevice {
878
920
  storageMode: this.mapToEnyoStorageMode(storageMode),
879
921
  maxChargingPowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.inWRte !== undefined ? batteryData.wChaMax * batteryData.inWRte : undefined,
880
922
  maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
923
+ activeChargeLimitW: this.computeActiveChargeLimitW(batteryData),
924
+ activeDischargeLimitW: this.computeActiveDischargeLimitW(batteryData),
881
925
  gridChargingEnabled: batteryData?.chaGriSet === 1
882
926
  }
883
927
  });
@@ -1056,6 +1100,34 @@ class SunspecBattery extends BaseSunspecDevice {
1056
1100
  const controls = await this.getBatteryControls();
1057
1101
  return this.determineStorageMode(controls);
1058
1102
  }
1103
+ /**
1104
+ * Compute the currently active charge limit in Watts from Model 124's
1105
+ * wChaMax and the charge rate setpoint inWRte (% of WChaMax). Returns
1106
+ * null when no limit is applied, when the registers cannot be read, or
1107
+ * when the rate is at 100% (no throttling) — null is returned (not
1108
+ * undefined) so the field is cleared on the appliance.
1109
+ */
1110
+ computeActiveChargeLimitW(batteryData) {
1111
+ if (!batteryData || batteryData.wChaMax === undefined || batteryData.inWRte === undefined) {
1112
+ return null;
1113
+ }
1114
+ if (batteryData.inWRte >= 100)
1115
+ return null;
1116
+ return (batteryData.wChaMax * batteryData.inWRte) / 100;
1117
+ }
1118
+ /**
1119
+ * Compute the currently active discharge limit in Watts from Model 124's
1120
+ * wChaMax and the discharge rate setpoint outWRte (% of WDisChaMax).
1121
+ * Returns null when no limit is applied or the registers cannot be read.
1122
+ */
1123
+ computeActiveDischargeLimitW(batteryData) {
1124
+ if (!batteryData || batteryData.wChaMax === undefined || batteryData.outWRte === undefined) {
1125
+ return null;
1126
+ }
1127
+ if (batteryData.outWRte >= 100)
1128
+ return null;
1129
+ return (batteryData.wChaMax * batteryData.outWRte) / 100;
1130
+ }
1059
1131
  determineStorageMode(controls) {
1060
1132
  if (!controls || controls.storCtlMod === undefined) {
1061
1133
  return null;
@@ -1331,7 +1403,12 @@ class SunspecMeter extends BaseSunspecDevice {
1331
1403
  */
1332
1404
  async disconnect() {
1333
1405
  if (this.applianceId) {
1334
- await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
1406
+ try {
1407
+ await this.applianceManager.updateApplianceState(this.applianceId, enyo_appliance_js_1.EnyoApplianceConnectionType.Connector, enyo_appliance_js_1.EnyoApplianceStateEnum.Offline);
1408
+ }
1409
+ catch (error) {
1410
+ console.error(`Failed to mark meter offline on disconnect: ${error}`);
1411
+ }
1335
1412
  }
1336
1413
  // Close just this meter's unit; other devices on the same network device stay open.
1337
1414
  await this.sunspecClient.disconnectUnit(this.unitId);
@@ -129,6 +129,14 @@ export declare class SunspecInverter extends BaseSunspecDevice {
129
129
  private persistErrorState;
130
130
  private detectAndEmitStatusTransition;
131
131
  protected onConnectionFailure(consecutiveFailures: number): Promise<void>;
132
+ /**
133
+ * Compute the currently active feed-in / production limit in Watts from the
134
+ * Model 121 settings (WMax) and Model 123 controls (WMaxLim_Ena, WMaxLimPct).
135
+ * Returns null when no limit is applied, when the registers cannot be read,
136
+ * or when the limit equals WMax (i.e. not actually throttling) — null is
137
+ * returned (not undefined) so the field is cleared on the appliance.
138
+ */
139
+ private computeActiveProductionLimitW;
132
140
  private mapOperatingState;
133
141
  /**
134
142
  * Map MPPT data to DC string structure for data bus
@@ -245,6 +253,20 @@ export declare class SunspecBattery extends BaseSunspecDevice {
245
253
  * @returns Promise<SunspecStorageMode | null> - Current mode or null if error
246
254
  */
247
255
  getStorageMode(): Promise<SunspecStorageMode | null>;
256
+ /**
257
+ * Compute the currently active charge limit in Watts from Model 124's
258
+ * wChaMax and the charge rate setpoint inWRte (% of WChaMax). Returns
259
+ * null when no limit is applied, when the registers cannot be read, or
260
+ * when the rate is at 100% (no throttling) — null is returned (not
261
+ * undefined) so the field is cleared on the appliance.
262
+ */
263
+ private computeActiveChargeLimitW;
264
+ /**
265
+ * Compute the currently active discharge limit in Watts from Model 124's
266
+ * wChaMax and the discharge rate setpoint outWRte (% of WDisChaMax).
267
+ * Returns null when no limit is applied or the registers cannot be read.
268
+ */
269
+ private computeActiveDischargeLimitW;
248
270
  private determineStorageMode;
249
271
  /**
250
272
  * Check if grid charging is enabled
@@ -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.67';
12
+ exports.SDK_VERSION = '0.0.69';
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.67";
8
+ export declare const SDK_VERSION = "0.0.69";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
@@ -129,6 +129,14 @@ export declare class SunspecInverter extends BaseSunspecDevice {
129
129
  private persistErrorState;
130
130
  private detectAndEmitStatusTransition;
131
131
  protected onConnectionFailure(consecutiveFailures: number): Promise<void>;
132
+ /**
133
+ * Compute the currently active feed-in / production limit in Watts from the
134
+ * Model 121 settings (WMax) and Model 123 controls (WMaxLim_Ena, WMaxLimPct).
135
+ * Returns null when no limit is applied, when the registers cannot be read,
136
+ * or when the limit equals WMax (i.e. not actually throttling) — null is
137
+ * returned (not undefined) so the field is cleared on the appliance.
138
+ */
139
+ private computeActiveProductionLimitW;
132
140
  private mapOperatingState;
133
141
  /**
134
142
  * Map MPPT data to DC string structure for data bus
@@ -245,6 +253,20 @@ export declare class SunspecBattery extends BaseSunspecDevice {
245
253
  * @returns Promise<SunspecStorageMode | null> - Current mode or null if error
246
254
  */
247
255
  getStorageMode(): Promise<SunspecStorageMode | null>;
256
+ /**
257
+ * Compute the currently active charge limit in Watts from Model 124's
258
+ * wChaMax and the charge rate setpoint inWRte (% of WChaMax). Returns
259
+ * null when no limit is applied, when the registers cannot be read, or
260
+ * when the rate is at 100% (no throttling) — null is returned (not
261
+ * undefined) so the field is cleared on the appliance.
262
+ */
263
+ private computeActiveChargeLimitW;
264
+ /**
265
+ * Compute the currently active discharge limit in Watts from Model 124's
266
+ * wChaMax and the discharge rate setpoint outWRte (% of WDisChaMax).
267
+ * Returns null when no limit is applied or the registers cannot be read.
268
+ */
269
+ private computeActiveDischargeLimitW;
248
270
  private determineStorageMode;
249
271
  /**
250
272
  * Check if grid charging is enabled
@@ -1,4 +1,4 @@
1
- import { SunspecBatteryChargeState, SunspecInverterCapability, SunspecInverterEvent1, SunspecModelId, SunspecMPPTOperatingState, SunspecStorageMode, SUNSPEC_CONNECTION_LOST_CODE } from "./sunspec-interfaces.js";
1
+ import { SunspecBatteryChargeState, SunspecEnableControl, SunspecInverterCapability, SunspecInverterEvent1, SunspecModelId, SunspecMPPTOperatingState, SunspecStorageMode, SUNSPEC_CONNECTION_LOST_CODE } from "./sunspec-interfaces.js";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { EnyoApplianceConnectionType, EnyoApplianceStateEnum, EnyoApplianceStatusEnum, EnyoApplianceTopologyFeatureEnum, EnyoApplianceTypeEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
4
4
  import { ConnectionRetryManager } from "./connection-retry-manager.js";
@@ -274,7 +274,9 @@ export class SunspecInverter extends BaseSunspecDevice {
274
274
  // Get device info from common block
275
275
  const commonData = await this.sunspecClient.readCommonBlock(this.unitId);
276
276
  const inverterSettings = await this.sunspecClient.readInverterSettings(this.unitId);
277
+ const inverterControls = await this.sunspecClient.readInverterControls(this.unitId);
277
278
  const mpptDataList = await this.sunspecClient.readAllMPPTData(this.unitId);
279
+ const activeProductionLimitationW = this.computeActiveProductionLimitW(inverterSettings, inverterControls);
278
280
  // Create or update appliance (skip if an existing appliance was provided)
279
281
  if (!this.applianceId) {
280
282
  try {
@@ -292,7 +294,8 @@ export class SunspecInverter extends BaseSunspecDevice {
292
294
  },
293
295
  inverter: {
294
296
  dcStrings: this.mapDcStringToApplianceMetadata(this.mapMPPTToStrings(mpptDataList)),
295
- maxPvProductionW: inverterSettings?.WMax
297
+ maxPvProductionW: inverterSettings?.WMax,
298
+ activeProductionLimitationW: activeProductionLimitationW
296
299
  }
297
300
  });
298
301
  console.log(`Sunspec Inverter connected: ${this.networkDevice.hostname} (${this.applianceId})`);
@@ -314,7 +317,8 @@ export class SunspecInverter extends BaseSunspecDevice {
314
317
  inverter: {
315
318
  ...existingAppliance?.inverter,
316
319
  dcStrings: this.mapDcStringToApplianceMetadata(this.mapMPPTToStrings(mpptDataList)),
317
- maxPvProductionW: inverterSettings?.WMax
320
+ maxPvProductionW: inverterSettings?.WMax,
321
+ activeProductionLimitationW: activeProductionLimitationW
318
322
  }
319
323
  });
320
324
  console.log(`Sunspec Inverter connected: ${this.networkDevice.hostname} (${this.applianceId})`);
@@ -334,7 +338,12 @@ export class SunspecInverter extends BaseSunspecDevice {
334
338
  async disconnect() {
335
339
  this.stopDataBusListening();
336
340
  if (this.applianceId) {
337
- await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
341
+ try {
342
+ await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
343
+ }
344
+ catch (error) {
345
+ console.error(`Failed to mark inverter offline on disconnect: ${error}`);
346
+ }
338
347
  }
339
348
  // Note: We don't disconnect the sunspecClient as it may be shared
340
349
  }
@@ -351,7 +360,9 @@ export class SunspecInverter extends BaseSunspecDevice {
351
360
  const inverterData = await this.sunspecClient.readInverterData(this.unitId);
352
361
  const mpptDataList = await this.sunspecClient.readAllMPPTData(this.unitId);
353
362
  const inverterSettings = await this.sunspecClient.readInverterSettings(this.unitId);
363
+ const inverterControls = await this.sunspecClient.readInverterControls(this.unitId);
354
364
  const dcStrings = this.mapMPPTToStrings(mpptDataList);
365
+ const activeProductionLimitationW = this.computeActiveProductionLimitW(inverterSettings, inverterControls);
355
366
  // SDK readers swallow modbus errors and return null/[]; detect that here so the
356
367
  // appliance is marked offline and the retry manager starts its backoff.
357
368
  if (await this.markOfflineIfUnhealthy()) {
@@ -392,7 +403,8 @@ export class SunspecInverter extends BaseSunspecDevice {
392
403
  await this.applianceManager.updateAppliance(this.applianceId, {
393
404
  inverter: {
394
405
  ...appliance?.inverter,
395
- dcStrings: this.mapDcStringToApplianceMetadata(dcStrings)
406
+ dcStrings: this.mapDcStringToApplianceMetadata(dcStrings),
407
+ activeProductionLimitationW: activeProductionLimitationW
396
408
  }
397
409
  });
398
410
  }
@@ -564,6 +576,25 @@ export class SunspecInverter extends BaseSunspecDevice {
564
576
  console.log(`Inverter ${this.applianceId}: emitting faulted (${SUNSPEC_CONNECTION_LOST_CODE}) after ${consecutiveFailures} consecutive reconnect failures`);
565
577
  this.dataBus.sendMessage([message]);
566
578
  }
579
+ /**
580
+ * Compute the currently active feed-in / production limit in Watts from the
581
+ * Model 121 settings (WMax) and Model 123 controls (WMaxLim_Ena, WMaxLimPct).
582
+ * Returns null when no limit is applied, when the registers cannot be read,
583
+ * or when the limit equals WMax (i.e. not actually throttling) — null is
584
+ * returned (not undefined) so the field is cleared on the appliance.
585
+ */
586
+ computeActiveProductionLimitW(settings, controls) {
587
+ if (!settings?.WMax || !controls)
588
+ return null;
589
+ if (controls.WMaxLim_Ena !== SunspecEnableControl.ENABLED)
590
+ return null;
591
+ if (controls.WMaxLimPct === undefined)
592
+ return null;
593
+ const limitW = (settings.WMax * controls.WMaxLimPct) / 100;
594
+ if (limitW >= settings.WMax)
595
+ return null;
596
+ return limitW;
597
+ }
567
598
  mapOperatingState(state) {
568
599
  if (!state)
569
600
  return EnyoInverterStateEnum.Off;
@@ -721,6 +752,8 @@ export class SunspecBattery extends BaseSunspecDevice {
721
752
  if (batteryData?.wChaMax !== undefined) {
722
753
  features.push(EnyoBatteryFeature.ChargeLimitation);
723
754
  }
755
+ const activeChargeLimitW = this.computeActiveChargeLimitW(batteryData);
756
+ const activeDischargeLimitW = this.computeActiveDischargeLimitW(batteryData);
724
757
  // Create or update appliance (skip if an existing appliance was provided)
725
758
  if (!this.applianceId) {
726
759
  try {
@@ -741,6 +774,8 @@ export class SunspecBattery extends BaseSunspecDevice {
741
774
  storageMode: this.mapToEnyoStorageMode(storageMode),
742
775
  maxChargingPowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.inWRte !== undefined ? batteryData.wChaMax * batteryData.inWRte : undefined,
743
776
  maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
777
+ activeChargeLimitW: activeChargeLimitW,
778
+ activeDischargeLimitW: activeDischargeLimitW,
744
779
  features,
745
780
  gridChargingEnabled: batteryData?.chaGriSet === 1
746
781
  }
@@ -767,6 +802,8 @@ export class SunspecBattery extends BaseSunspecDevice {
767
802
  storageMode: this.mapToEnyoStorageMode(storageMode),
768
803
  maxChargingPowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.inWRte !== undefined ? batteryData.wChaMax * batteryData.inWRte : undefined,
769
804
  maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
805
+ activeChargeLimitW: activeChargeLimitW,
806
+ activeDischargeLimitW: activeDischargeLimitW,
770
807
  features,
771
808
  gridChargingEnabled: batteryData?.chaGriSet === 1
772
809
  }
@@ -782,7 +819,12 @@ export class SunspecBattery extends BaseSunspecDevice {
782
819
  async disconnect() {
783
820
  this.stopDataBusListening();
784
821
  if (this.applianceId) {
785
- await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
822
+ try {
823
+ await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
824
+ }
825
+ catch (error) {
826
+ console.error(`Failed to mark battery offline on disconnect: ${error}`);
827
+ }
786
828
  }
787
829
  }
788
830
  /**
@@ -873,6 +915,8 @@ export class SunspecBattery extends BaseSunspecDevice {
873
915
  storageMode: this.mapToEnyoStorageMode(storageMode),
874
916
  maxChargingPowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.inWRte !== undefined ? batteryData.wChaMax * batteryData.inWRte : undefined,
875
917
  maxDischargePowerW: batteryData && batteryData.wChaMax !== undefined && batteryData.outWRte !== undefined ? batteryData.wChaMax * batteryData.outWRte : undefined,
918
+ activeChargeLimitW: this.computeActiveChargeLimitW(batteryData),
919
+ activeDischargeLimitW: this.computeActiveDischargeLimitW(batteryData),
876
920
  gridChargingEnabled: batteryData?.chaGriSet === 1
877
921
  }
878
922
  });
@@ -1051,6 +1095,34 @@ export class SunspecBattery extends BaseSunspecDevice {
1051
1095
  const controls = await this.getBatteryControls();
1052
1096
  return this.determineStorageMode(controls);
1053
1097
  }
1098
+ /**
1099
+ * Compute the currently active charge limit in Watts from Model 124's
1100
+ * wChaMax and the charge rate setpoint inWRte (% of WChaMax). Returns
1101
+ * null when no limit is applied, when the registers cannot be read, or
1102
+ * when the rate is at 100% (no throttling) — null is returned (not
1103
+ * undefined) so the field is cleared on the appliance.
1104
+ */
1105
+ computeActiveChargeLimitW(batteryData) {
1106
+ if (!batteryData || batteryData.wChaMax === undefined || batteryData.inWRte === undefined) {
1107
+ return null;
1108
+ }
1109
+ if (batteryData.inWRte >= 100)
1110
+ return null;
1111
+ return (batteryData.wChaMax * batteryData.inWRte) / 100;
1112
+ }
1113
+ /**
1114
+ * Compute the currently active discharge limit in Watts from Model 124's
1115
+ * wChaMax and the discharge rate setpoint outWRte (% of WDisChaMax).
1116
+ * Returns null when no limit is applied or the registers cannot be read.
1117
+ */
1118
+ computeActiveDischargeLimitW(batteryData) {
1119
+ if (!batteryData || batteryData.wChaMax === undefined || batteryData.outWRte === undefined) {
1120
+ return null;
1121
+ }
1122
+ if (batteryData.outWRte >= 100)
1123
+ return null;
1124
+ return (batteryData.wChaMax * batteryData.outWRte) / 100;
1125
+ }
1054
1126
  determineStorageMode(controls) {
1055
1127
  if (!controls || controls.storCtlMod === undefined) {
1056
1128
  return null;
@@ -1325,7 +1397,12 @@ export class SunspecMeter extends BaseSunspecDevice {
1325
1397
  */
1326
1398
  async disconnect() {
1327
1399
  if (this.applianceId) {
1328
- await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
1400
+ try {
1401
+ await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
1402
+ }
1403
+ catch (error) {
1404
+ console.error(`Failed to mark meter offline on disconnect: ${error}`);
1405
+ }
1329
1406
  }
1330
1407
  // Close just this meter's unit; other devices on the same network device stay open.
1331
1408
  await this.sunspecClient.disconnectUnit(this.unitId);
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.67";
8
+ export declare const SDK_VERSION = "0.0.69";
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.67';
8
+ export const SDK_VERSION = '0.0.69';
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.67",
3
+ "version": "0.0.69",
4
4
  "description": "enyo Energy Sunspec SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,7 +37,7 @@
37
37
  "typescript": "^5.8.3"
38
38
  },
39
39
  "dependencies": {
40
- "@enyo-energy/energy-app-sdk": "^0.0.132"
40
+ "@enyo-energy/energy-app-sdk": "^0.0.135"
41
41
  },
42
42
  "volta": {
43
43
  "node": "22.17.0"