@enyo-energy/sunspec-sdk 0.0.71 → 0.0.73

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.
Files changed (34) hide show
  1. package/README.md +302 -0
  2. package/dist/cjs/index.cjs +30 -2
  3. package/dist/cjs/index.d.cts +6 -1
  4. package/dist/cjs/sunspec-battery-calibration-driver.cjs +158 -0
  5. package/dist/cjs/sunspec-battery-calibration-driver.d.cts +63 -0
  6. package/dist/cjs/sunspec-battery-feature-calibrator.cjs +350 -0
  7. package/dist/cjs/sunspec-battery-feature-calibrator.d.cts +89 -0
  8. package/dist/cjs/sunspec-battery-schedule-handler.cjs +92 -0
  9. package/dist/cjs/sunspec-battery-schedule-handler.d.cts +67 -0
  10. package/dist/cjs/sunspec-calibration-storage.cjs +47 -0
  11. package/dist/cjs/sunspec-calibration-storage.d.cts +24 -0
  12. package/dist/cjs/sunspec-devices.cjs +305 -215
  13. package/dist/cjs/sunspec-devices.d.cts +129 -19
  14. package/dist/cjs/sunspec-interfaces.cjs +42 -1
  15. package/dist/cjs/sunspec-interfaces.d.cts +66 -0
  16. package/dist/cjs/version.cjs +1 -1
  17. package/dist/cjs/version.d.cts +1 -1
  18. package/dist/index.d.ts +6 -1
  19. package/dist/index.js +12 -1
  20. package/dist/sunspec-battery-calibration-driver.d.ts +63 -0
  21. package/dist/sunspec-battery-calibration-driver.js +154 -0
  22. package/dist/sunspec-battery-feature-calibrator.d.ts +89 -0
  23. package/dist/sunspec-battery-feature-calibrator.js +345 -0
  24. package/dist/sunspec-battery-schedule-handler.d.ts +67 -0
  25. package/dist/sunspec-battery-schedule-handler.js +88 -0
  26. package/dist/sunspec-calibration-storage.d.ts +24 -0
  27. package/dist/sunspec-calibration-storage.js +42 -0
  28. package/dist/sunspec-devices.d.ts +129 -19
  29. package/dist/sunspec-devices.js +304 -216
  30. package/dist/sunspec-interfaces.d.ts +66 -0
  31. package/dist/sunspec-interfaces.js +41 -0
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/package.json +7 -3
@@ -1,11 +1,12 @@
1
- import { type IRetryConfig, type SunspecBatteryBaseData, SunspecBatteryCapability, type SunspecBatteryControls, type SunspecInverterData, SunspecInverterCapability, SunspecMeterCapability, SunspecStorageMode } from "./sunspec-interfaces.cjs";
1
+ import { type IRetryConfig, type SunspecBatteryBaseData, SunspecBatteryCapability, type SunspecBatteryControls, type SunspecBatteryData, type SunspecBatteryFeatureMode, type SunspecInverterData, SunspecInverterCapability, SunspecMeterCapability, SunspecStorageMode } from "./sunspec-interfaces.cjs";
2
2
  import { ApplianceManager, EnergyApp } from "@enyo-energy/energy-app-sdk";
3
3
  import { type EnyoAppliance, type EnyoApplianceErrorCode, EnyoApplianceName } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
4
4
  import { EnyoNetworkDevice } from "@enyo-energy/energy-app-sdk/dist/types/enyo-network-device.js";
5
5
  import { SunspecModbusClient } from "./sunspec-modbus-client.cjs";
6
6
  import { ConnectionRetryManager } from "./connection-retry-manager.cjs";
7
7
  import { EnyoCommandAcknowledgeAnswerEnum, EnyoDataBusMessage, EnyoDataBusMessageEnum, EnyoDataBusMessageResolution } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
8
- import { type CalibrationDeviceType, type CalibrationRestoreReason, type CalibrationSnapshot, CalibrationSnapshotService } from "./calibration-snapshot-service.cjs";
8
+ import { BatteryCalibrator, type BatteryCalibratorConfig, type CalibrationResult, type CalibrationResultStore, type CalibrationSnapshot, type RestoreReason, SnapshotService } from "@enyo-energy/appliance-calibration";
9
+ import { EnyoBatteryFeature } from "@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js";
9
10
  import { EnergyAppDataBus } from "@enyo-energy/energy-app-sdk/dist/packages/energy-app-data-bus.js";
10
11
  /**
11
12
  * Base abstract class for all Sunspec devices
@@ -26,7 +27,13 @@ export declare abstract class BaseSunspecDevice {
26
27
  protected dataBus?: EnergyAppDataBus;
27
28
  protected retryManager: ConnectionRetryManager;
28
29
  protected consecutiveReconnectFailures: number;
29
- protected calibrationService?: CalibrationSnapshotService;
30
+ /**
31
+ * Prefix used when persisting calibration snapshots via the library's
32
+ * {@link SnapshotService}. Kept identical to the key the SDK used before
33
+ * the migration to `@enyo-energy/appliance-calibration` so existing
34
+ * snapshots stored by older builds are still picked up on startup.
35
+ */
36
+ protected static readonly CALIBRATION_SNAPSHOT_KEY_PREFIX = "sunspec-calibration-snapshot-";
30
37
  constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number, retryConfig?: IRetryConfig, appliance?: EnyoAppliance, useTls?: boolean | undefined);
31
38
  /**
32
39
  * Connect to the device and create/update the appliance
@@ -80,16 +87,11 @@ export declare abstract class BaseSunspecDevice {
80
87
  protected markOffline(): Promise<void>;
81
88
  protected sendCommandAcknowledge(messageId: string, acknowledgeMessage: EnyoDataBusMessageEnum | string, answer: EnyoCommandAcknowledgeAnswerEnum, rejectionReason?: string): void;
82
89
  /**
83
- * Lazily construct and initialize the per-appliance CalibrationSnapshotService. Reloads
84
- * any persisted snapshot from storage so an in-flight calibration survives restarts.
85
- * Idempotent subsequent calls are no-ops once the service exists.
86
- */
87
- protected initCalibrationService(deviceType: CalibrationDeviceType): Promise<void>;
88
- /**
89
- * Subclasses implement how to write the modified fields of a snapshot back to the device.
90
- * Called both on explicit StopCalibrationV1 and when the 5-minute auto-stop fires.
90
+ * Build a typed {@link SnapshotService} bound to this appliance's calibration storage,
91
+ * using the legacy SDK storage key prefix so prior installs upgrade seamlessly. Returns
92
+ * undefined if the appliance has not been registered yet.
91
93
  */
92
- protected restoreFromCalibrationSnapshot(_snapshot: CalibrationSnapshot, _reason: CalibrationRestoreReason): Promise<void>;
94
+ protected buildSnapshotService<T>(onRestore: (snapshot: CalibrationSnapshot<T>, reason: RestoreReason) => Promise<void>): SnapshotService<T> | undefined;
93
95
  }
94
96
  /**
95
97
  * Sunspec Inverter implementation using dynamic model discovery
@@ -100,6 +102,7 @@ export declare class SunspecInverter extends BaseSunspecDevice {
100
102
  private static readonly CONNECTION_FAULT_THRESHOLD;
101
103
  private storage?;
102
104
  private errorState;
105
+ private snapshotService?;
103
106
  constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number, capabilities?: SunspecInverterCapability[], retryConfig?: IRetryConfig, appliance?: EnyoAppliance, useTls?: boolean);
104
107
  connect(): Promise<void>;
105
108
  disconnect(): Promise<void>;
@@ -169,16 +172,106 @@ export declare class SunspecInverter extends BaseSunspecDevice {
169
172
  stopDataBusListening(): void;
170
173
  private handleInverterCommand;
171
174
  private handleSetFeedInLimit;
175
+ /**
176
+ * Lazily construct the per-appliance {@link SnapshotService}. Reloads any
177
+ * persisted snapshot from storage so an in-flight calibration survives
178
+ * process restarts. Idempotent.
179
+ */
180
+ private initSnapshotService;
172
181
  private handleStartCalibration;
173
182
  private handleStopCalibration;
174
- protected restoreFromCalibrationSnapshot(snapshot: CalibrationSnapshot, reason: CalibrationRestoreReason): Promise<void>;
183
+ /**
184
+ * `onRestore` callback for the inverter's {@link SnapshotService}. Writes only the
185
+ * subset of writable inverter fields that other commands actually touched during the
186
+ * calibration. Catches and logs failures rather than throwing — the snapshot has
187
+ * already been removed from storage by the time this runs, so a throw is unrecoverable
188
+ * and is best logged for operator action.
189
+ */
190
+ private restoreInverterSnapshot;
175
191
  }
192
+ /**
193
+ * Pure register-presence detection — exported so it can be unit-tested
194
+ * without the full `SunspecBattery` scaffold. Maps each Model 124 writable
195
+ * register to the `EnyoBatteryFeature` it represents:
196
+ *
197
+ * - `chaGriSet` → `GridCharging` (charge source PV/GRID)
198
+ * - `wChaMax` → `ChargeLimitation` (max charge power)
199
+ * - `outWRte` → `DischargeLimitation` (discharge rate %)
200
+ * - `storCtlMod` → `GridDischarging` (closest signal that external discharge
201
+ * control is available; the inverter decides where it goes)
202
+ */
203
+ export declare function detectFeaturesFromRegisters(batteryData: SunspecBatteryData | null): EnyoBatteryFeature[];
204
+ /**
205
+ * Apply the per-feature calibration filter to a register-detected set.
206
+ * Exported for unit-test access to the legacy-result fallback branch.
207
+ *
208
+ * The contract:
209
+ * - No `result` or `result.state !== 'calibrated'` → strip every controllable
210
+ * feature (safe fallback while waiting for calibration).
211
+ * - `result.state === 'calibrated'` with a decodable per-feature payload →
212
+ * publish only the controllable features whose probes passed.
213
+ * - `result.state === 'calibrated'` with **no** decodable payload → legacy
214
+ * data (pre-feature-calibrator SDK). The new calibrator only marks
215
+ * `state=calibrated` when at least one probe passes, so an empty decoded
216
+ * set with `state=calibrated` unambiguously signals legacy results. Publish
217
+ * the full detected set to preserve the old all-or-nothing semantics on
218
+ * upgrade.
219
+ */
220
+ export declare function filterFeaturesByCalibrationResult(detected: EnyoBatteryFeature[], result: CalibrationResult | undefined, controllable: readonly EnyoBatteryFeature[]): EnyoBatteryFeature[];
176
221
  /**
177
222
  * Sunspec Battery implementation
178
223
  */
179
224
  export declare class SunspecBattery extends BaseSunspecDevice {
225
+ private readonly featureMode;
180
226
  private readonly capabilities;
181
- constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number, capabilities?: SunspecBatteryCapability[], retryConfig?: IRetryConfig, appliance?: EnyoAppliance, useTls?: boolean);
227
+ private snapshotService?;
228
+ private calibrationDriver?;
229
+ private batteryCalibrator?;
230
+ private calibrationResultStore?;
231
+ private scheduleHandler;
232
+ /**
233
+ * Battery features that imply outbound control writes from the host action-taker.
234
+ * These are stripped from `appliance.battery.features` until the calibration result
235
+ * store says this battery has been successfully calibrated — see
236
+ * {@link computeAdvertisedFeatures}.
237
+ */
238
+ private static readonly CONTROLLABLE_FEATURES;
239
+ constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, featureMode: SunspecBatteryFeatureMode, unitId?: number, port?: number, baseAddress?: number, capabilities?: SunspecBatteryCapability[], retryConfig?: IRetryConfig, appliance?: EnyoAppliance, useTls?: boolean);
240
+ /**
241
+ * Wire the battery into `@enyo-energy/appliance-calibration`'s
242
+ * `BatteryCalibrator` test-charge flow. Call once after {@link connect}.
243
+ *
244
+ * Consumers own the `CalibrationResultStore` (shared across appliances so
245
+ * the persisted result map doesn't get clobbered) and the
246
+ * `CalibrationTrigger` that drives `runCalibration()` on whatever schedule
247
+ * makes sense for the host. Pass overrides for the calibrator's defaults
248
+ * (`testPowerW`, response thresholds, etc.) via `opts.config`.
249
+ */
250
+ configureCalibration(opts: {
251
+ resultStore: CalibrationResultStore;
252
+ config?: Partial<BatteryCalibratorConfig>;
253
+ }): BatteryCalibrator<SunspecBatteryControls>;
254
+ /**
255
+ * Detect which battery features the device's registers expose. This is the
256
+ * raw, register-only view; the {@link featureMode} configured at
257
+ * construction decides what is actually published — see
258
+ * {@link resolveAdvertisedFeatures}.
259
+ */
260
+ private detectFromRegisters;
261
+ /**
262
+ * Resolve `appliance.battery.features` for the configured mode. Called on
263
+ * every connect() and readData() cycle so that, in calibration-based mode,
264
+ * the controllable features appear as soon as the calibrator flips
265
+ * `isCalibrated` (no synchronous push).
266
+ */
267
+ private resolveAdvertisedFeatures;
268
+ /**
269
+ * Returns the per-battery `BatteryCalibrator` created by
270
+ * {@link configureCalibration}, or `undefined` if calibration was never
271
+ * configured. Consumers register the returned instance with their own
272
+ * `CalibrationTrigger`.
273
+ */
274
+ getBatteryCalibrator(): BatteryCalibrator<SunspecBatteryControls> | undefined;
182
275
  /**
183
276
  * Connect to the battery and create/update the appliance
184
277
  */
@@ -300,13 +393,30 @@ export declare class SunspecBattery extends BaseSunspecDevice {
300
393
  */
301
394
  stopDataBusListening(): void;
302
395
  private handleStorageCommand;
303
- private handleStartGridCharge;
304
- private handleStopGridCharge;
305
- private handleSetDischargeLimit;
306
- private handleSetChargeLimit;
396
+ /**
397
+ * Acknowledge a SetStorageScheduleV1 message. The actual schedule application
398
+ * is owned by `this.scheduleHandler` (subscribed independently via its own
399
+ * data-bus listener registered in its constructor). The ack reports "received
400
+ * & queued" rather than "applied" — schedule entries play out over time, and
401
+ * per-entry write failures land in `console.error` via the handler's
402
+ * onChange wrapper.
403
+ */
404
+ private handleSetStorageScheduleAck;
405
+ /**
406
+ * Lazily construct the per-appliance {@link SnapshotService}. Reloads any
407
+ * persisted snapshot from storage so an in-flight calibration survives
408
+ * process restarts. Idempotent.
409
+ */
410
+ private initSnapshotService;
307
411
  private handleStartCalibration;
308
412
  private handleStopCalibration;
309
- protected restoreFromCalibrationSnapshot(snapshot: CalibrationSnapshot, reason: CalibrationRestoreReason): Promise<void>;
413
+ /**
414
+ * `onRestore` callback for the battery's {@link SnapshotService}. Writes only the
415
+ * subset of writable battery fields touched during the calibration. Errors are
416
+ * caught and logged — by the time this fires the snapshot is gone from storage,
417
+ * so a throw would just propagate into the void.
418
+ */
419
+ private restoreBatterySnapshot;
310
420
  }
311
421
  /**
312
422
  * Sunspec Meter implementation
@@ -3,7 +3,8 @@
3
3
  * SunSpec block interfaces with block numbers
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SunspecMeterCapability = exports.SunspecBatteryCapability = exports.SunspecInverterCapability = exports.SunspecStorageMode = exports.SunspecChargeSource = exports.SunspecVArPctMode = exports.SunspecEnableControl = exports.SunspecConnectionControl = exports.SunspecStorageControlMode = exports.SunspecBatteryEvent1 = exports.SunspecBatteryBankState = exports.SunspecBatteryType = exports.SunspecBatteryControlMode = exports.SunspecBatteryChargeState = exports.SunspecMPPTOperatingState = exports.SUNSPEC_CONNECTION_LOST_CODE = exports.SunspecInverterEvent1 = exports.SunspecModelId = exports.DEFAULT_RETRY_CONFIG = void 0;
6
+ exports.SUNSPEC_CONTROLLABLE_FEATURES = exports.SunspecMeterCapability = exports.SunspecBatteryFeatureModeKind = exports.SunspecBatteryCapability = exports.SunspecInverterCapability = exports.SunspecStorageMode = exports.SunspecChargeSource = exports.SunspecVArPctMode = exports.SunspecEnableControl = exports.SunspecConnectionControl = exports.SunspecStorageControlMode = exports.SunspecBatteryEvent1 = exports.SunspecBatteryBankState = exports.SunspecBatteryType = exports.SunspecBatteryControlMode = exports.SunspecBatteryChargeState = exports.SunspecMPPTOperatingState = exports.SUNSPEC_CONNECTION_LOST_CODE = exports.SunspecInverterEvent1 = exports.SunspecModelId = exports.DEFAULT_RETRY_CONFIG = void 0;
7
+ const enyo_battery_appliance_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js");
7
8
  exports.DEFAULT_RETRY_CONFIG = {
8
9
  phases: [
9
10
  { intervalMs: 10_000, durationMs: 60_000 }, // Phase 1: every 10s for 1 minute
@@ -257,6 +258,46 @@ var SunspecBatteryCapability;
257
258
  SunspecBatteryCapability["DischargeLimit"] = "discharge-limit";
258
259
  SunspecBatteryCapability["ChargeLimit"] = "charge-limit";
259
260
  })(SunspecBatteryCapability || (exports.SunspecBatteryCapability = SunspecBatteryCapability = {}));
261
+ /**
262
+ * Selects how `SunspecBattery` populates `appliance.battery.features`.
263
+ *
264
+ * - `Disabled`: ignore the SunSpec registers and ignore calibration. The SDK
265
+ * publishes whatever the consumer puts in `allowedFeatures` (omit or pass
266
+ * `[]` to advertise nothing). Useful when the host already knows what the
267
+ * battery can do and doesn't want the SDK to make any inference.
268
+ * - `RegisterBased`: publish whatever the device's registers expose. If
269
+ * `allowedFeatures` is set, the published list is the intersection — the
270
+ * register-detected set filtered to the allow-list. Lets the consumer hide
271
+ * features that the hardware exposes but they don't want to use.
272
+ * - `CalibrationBased`: same as `RegisterBased`, but controllable features
273
+ * (grid charging, grid discharging, charge limitation, discharge
274
+ * limitation) stay hidden until `CalibrationResultStore.isCalibrated(...)`
275
+ * returns true for this appliance. Requires `configureCalibration(...)` to
276
+ * actually run a calibration; without it the controllable features remain
277
+ * hidden forever (safe-fallback behaviour).
278
+ */
279
+ var SunspecBatteryFeatureModeKind;
280
+ (function (SunspecBatteryFeatureModeKind) {
281
+ SunspecBatteryFeatureModeKind["Disabled"] = "disabled";
282
+ SunspecBatteryFeatureModeKind["RegisterBased"] = "register-based";
283
+ SunspecBatteryFeatureModeKind["CalibrationBased"] = "calibration-based";
284
+ })(SunspecBatteryFeatureModeKind || (exports.SunspecBatteryFeatureModeKind = SunspecBatteryFeatureModeKind = {}));
260
285
  var SunspecMeterCapability;
261
286
  (function (SunspecMeterCapability) {
262
287
  })(SunspecMeterCapability || (exports.SunspecMeterCapability = SunspecMeterCapability = {}));
288
+ /**
289
+ * The four `EnyoBatteryFeature` values that imply outbound control writes from
290
+ * the host action-taker. `SunspecBattery` calibration probes each one
291
+ * individually so `appliance.battery.features` reflects only what the device
292
+ * actually honours.
293
+ *
294
+ * Exposed as `as const` (not a plain `EnyoBatteryFeature[]`) so consumers can
295
+ * iterate the tuple with full type narrowing — see the `WRITABLE_BATTERY_FIELDS`
296
+ * pattern in `sunspec-battery-calibration-driver.ts`.
297
+ */
298
+ exports.SUNSPEC_CONTROLLABLE_FEATURES = [
299
+ enyo_battery_appliance_js_1.EnyoBatteryFeature.GridCharging,
300
+ enyo_battery_appliance_js_1.EnyoBatteryFeature.GridDischarging,
301
+ enyo_battery_appliance_js_1.EnyoBatteryFeature.ChargeLimitation,
302
+ enyo_battery_appliance_js_1.EnyoBatteryFeature.DischargeLimitation,
303
+ ];
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * SunSpec block interfaces with block numbers
3
3
  */
4
+ import { EnyoBatteryFeature } from "@enyo-energy/energy-app-sdk/dist/types/enyo-battery-appliance.js";
4
5
  /**
5
6
  * A single phase in the tiered retry schedule
6
7
  */
@@ -616,6 +617,39 @@ export declare enum SunspecBatteryCapability {
616
617
  DischargeLimit = "discharge-limit",
617
618
  ChargeLimit = "charge-limit"
618
619
  }
620
+ /**
621
+ * Selects how `SunspecBattery` populates `appliance.battery.features`.
622
+ *
623
+ * - `Disabled`: ignore the SunSpec registers and ignore calibration. The SDK
624
+ * publishes whatever the consumer puts in `allowedFeatures` (omit or pass
625
+ * `[]` to advertise nothing). Useful when the host already knows what the
626
+ * battery can do and doesn't want the SDK to make any inference.
627
+ * - `RegisterBased`: publish whatever the device's registers expose. If
628
+ * `allowedFeatures` is set, the published list is the intersection — the
629
+ * register-detected set filtered to the allow-list. Lets the consumer hide
630
+ * features that the hardware exposes but they don't want to use.
631
+ * - `CalibrationBased`: same as `RegisterBased`, but controllable features
632
+ * (grid charging, grid discharging, charge limitation, discharge
633
+ * limitation) stay hidden until `CalibrationResultStore.isCalibrated(...)`
634
+ * returns true for this appliance. Requires `configureCalibration(...)` to
635
+ * actually run a calibration; without it the controllable features remain
636
+ * hidden forever (safe-fallback behaviour).
637
+ */
638
+ export declare enum SunspecBatteryFeatureModeKind {
639
+ Disabled = "disabled",
640
+ RegisterBased = "register-based",
641
+ CalibrationBased = "calibration-based"
642
+ }
643
+ export type SunspecBatteryFeatureMode = {
644
+ kind: SunspecBatteryFeatureModeKind.Disabled;
645
+ allowedFeatures?: EnyoBatteryFeature[];
646
+ } | {
647
+ kind: SunspecBatteryFeatureModeKind.RegisterBased;
648
+ allowedFeatures?: EnyoBatteryFeature[];
649
+ } | {
650
+ kind: SunspecBatteryFeatureModeKind.CalibrationBased;
651
+ allowedFeatures?: EnyoBatteryFeature[];
652
+ };
619
653
  export declare enum SunspecMeterCapability {
620
654
  }
621
655
  export interface SunspecBatteryControls {
@@ -626,3 +660,35 @@ export interface SunspecBatteryControls {
626
660
  outWRte?: number;
627
661
  minRsvPct?: number;
628
662
  }
663
+ /**
664
+ * The four `EnyoBatteryFeature` values that imply outbound control writes from
665
+ * the host action-taker. `SunspecBattery` calibration probes each one
666
+ * individually so `appliance.battery.features` reflects only what the device
667
+ * actually honours.
668
+ *
669
+ * Exposed as `as const` (not a plain `EnyoBatteryFeature[]`) so consumers can
670
+ * iterate the tuple with full type narrowing — see the `WRITABLE_BATTERY_FIELDS`
671
+ * pattern in `sunspec-battery-calibration-driver.ts`.
672
+ */
673
+ export declare const SUNSPEC_CONTROLLABLE_FEATURES: readonly [EnyoBatteryFeature.GridCharging, EnyoBatteryFeature.GridDischarging, EnyoBatteryFeature.ChargeLimitation, EnyoBatteryFeature.DischargeLimitation];
674
+ export type SunspecControllableFeature = typeof SUNSPEC_CONTROLLABLE_FEATURES[number];
675
+ /**
676
+ * Outcome of a single per-feature calibration probe.
677
+ *
678
+ * - `passed`: the SDK observed the expected register/meter response.
679
+ * - `failed`: the SDK wrote the registers but the response was absent or wrong.
680
+ * - `not-supported`: a precondition stopped the probe (battery SoC out of
681
+ * range, missing telemetry, register not exposed). Distinct from `failed`
682
+ * so the host can distinguish "device won't" from "we couldn't even try".
683
+ */
684
+ export type SunspecFeatureVerdict = 'passed' | 'failed' | 'not-supported';
685
+ /**
686
+ * Per-feature outcomes captured during one calibration session. Serialised
687
+ * into `CalibrationResult.notes` as JSON; decoded with runtime guards by
688
+ * `decodeFeatureResults` (no `as` casts at the use site).
689
+ */
690
+ export interface SunspecCalibrationFeatureResults {
691
+ featureResults: Partial<Record<SunspecControllableFeature, SunspecFeatureVerdict>>;
692
+ /** Free-form per-probe diagnostics (SoC at start, observed deltas, timeouts). */
693
+ diagnostics?: Partial<Record<SunspecControllableFeature, string>>;
694
+ }
@@ -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.71';
12
+ exports.SDK_VERSION = '0.0.73';
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.71";
8
+ export declare const SDK_VERSION = "0.0.73";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/dist/index.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  export * from './sunspec-interfaces.js';
2
2
  export * from './sunspec-devices.js';
3
3
  export * from './sunspec-modbus-client.js';
4
- export * from './calibration-snapshot-service.js';
5
4
  export { ConnectionRetryManager } from './connection-retry-manager.js';
6
5
  export { SDK_VERSION, getSdkVersion } from './version.js';
6
+ export { SunspecCalibrationStorage, createSunspecCalibrationStorage } from './sunspec-calibration-storage.js';
7
+ export { SunspecBatteryCalibrationDriver } from './sunspec-battery-calibration-driver.js';
8
+ export { SunspecBatteryFeatureCalibrator, type SunspecBatteryFeatureCalibratorOptions, decodeFeatureResults, } from './sunspec-battery-feature-calibrator.js';
9
+ export { SunspecBatteryScheduleHandler, type SunspecScheduleRegisters, type SunspecBatteryScheduleHandlerOptions, } from './sunspec-battery-schedule-handler.js';
10
+ export { type EnyoDataBusSetStorageScheduleV1, type EnyoStorageScheduleEntry, EnyoStorageScheduleModeEnum, EnyoStorageScheduleDirectionEnum, } from '@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js';
11
+ export { AbstractCalibrationStorage, InMemoryCalibrationStorage, type AckResult, type CalibrationResult, type CalibrationState, type CalibrationSnapshot, type CalibrationRestoreCallback, type RestoreReason, SnapshotService, type SnapshotServiceOptions, DEFAULT_AUTO_STOP_MS, AbstractBatteryCalibrationDriver, BatteryCalibrator, type BatteryCalibratorOptions, type BatteryCalibratorConfig, DEFAULT_BATTERY_CALIBRATOR_CONFIG, CalibrationResultStore, type CalibrationResultStoreData, CalibrationTrigger, type CalibrationTriggerOptions, } from '@enyo-energy/appliance-calibration';
package/dist/index.js CHANGED
@@ -1,6 +1,17 @@
1
1
  export * from './sunspec-interfaces.js';
2
2
  export * from './sunspec-devices.js';
3
3
  export * from './sunspec-modbus-client.js';
4
- export * from './calibration-snapshot-service.js';
5
4
  export { ConnectionRetryManager } from './connection-retry-manager.js';
6
5
  export { SDK_VERSION, getSdkVersion } from './version.js';
6
+ // New calibration integration with @enyo-energy/appliance-calibration
7
+ export { SunspecCalibrationStorage, createSunspecCalibrationStorage } from './sunspec-calibration-storage.js';
8
+ export { SunspecBatteryCalibrationDriver } from './sunspec-battery-calibration-driver.js';
9
+ export { SunspecBatteryFeatureCalibrator, decodeFeatureResults, } from './sunspec-battery-feature-calibrator.js';
10
+ export { SunspecBatteryScheduleHandler, } from './sunspec-battery-schedule-handler.js';
11
+ // Re-export the schedule message types from energy-app-sdk so consumers can
12
+ // build/inspect SetStorageScheduleV1 messages without a second import.
13
+ export { EnyoStorageScheduleModeEnum, EnyoStorageScheduleDirectionEnum, } from '@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js';
14
+ // Re-export the library symbols consumers need to wire up the trigger and gate
15
+ // commands. Avoids a second peer dependency on @enyo-energy/appliance-calibration
16
+ // while keeping the consumer wiring concise.
17
+ export { AbstractCalibrationStorage, InMemoryCalibrationStorage, SnapshotService, DEFAULT_AUTO_STOP_MS, AbstractBatteryCalibrationDriver, BatteryCalibrator, DEFAULT_BATTERY_CALIBRATOR_CONFIG, CalibrationResultStore, CalibrationTrigger, } from '@enyo-energy/appliance-calibration';
@@ -0,0 +1,63 @@
1
+ import { AbstractBatteryCalibrationDriver, type AckResult, type RestoreReason } from "@enyo-energy/appliance-calibration";
2
+ import type { EnergyAppDataBus } from "@enyo-energy/energy-app-sdk/dist/packages/energy-app-data-bus.js";
3
+ import type { SunspecModbusClient } from "./sunspec-modbus-client.js";
4
+ import { type SunspecBatteryControls } from "./sunspec-interfaces.js";
5
+ /**
6
+ * Vendor seam between `@enyo-energy/appliance-calibration`'s `BatteryCalibrator`
7
+ * and a SunSpec battery exposed via {@link SunspecModbusClient}.
8
+ *
9
+ * Battery power is fed in by the owning {@link SunspecBattery} on every readData
10
+ * cycle (call {@link updateBatteryPowerCache}). Grid power is sourced from the
11
+ * data bus: the driver subscribes to `MeterValuesUpdateV1` for the lifetime of
12
+ * the calibrator and caches the latest reading. Call {@link stop} when the
13
+ * battery disconnects to unsubscribe.
14
+ */
15
+ export declare class SunspecBatteryCalibrationDriver extends AbstractBatteryCalibrationDriver<SunspecBatteryControls> {
16
+ private readonly sunspecClient;
17
+ private readonly unitId;
18
+ private readonly dataBus;
19
+ private readonly batteryPowerCache;
20
+ private readonly gridPowerCache;
21
+ private meterListenerId?;
22
+ constructor(sunspecClient: SunspecModbusClient, unitId: number, dataBus: EnergyAppDataBus);
23
+ private subscribeMeterUpdates;
24
+ /**
25
+ * Tear down the meter subscription. Idempotent — call from
26
+ * `SunspecBattery.disconnect()`.
27
+ */
28
+ stop(): void;
29
+ /**
30
+ * Push the latest computed battery power (W, positive = charging into the
31
+ * battery) from the owning device. Called from the readData loop.
32
+ */
33
+ updateBatteryPowerCache(powerW: number): void;
34
+ captureSnapshot(): Promise<SunspecBatteryControls>;
35
+ /**
36
+ * Write only the fields present in the snapshot AND in the writable whitelist.
37
+ * Per the library's contract this is best-effort: log on failure rather than
38
+ * throw, because the persisted snapshot has already been removed by the time
39
+ * we run and any throw would silently swallow what just happened.
40
+ */
41
+ restoreFromSnapshot(snapshot: SunspecBatteryControls, reason: RestoreReason): Promise<void>;
42
+ /**
43
+ * SunSpec model 124 has no explicit calibration-mode register — the
44
+ * snapshot / restore + storCtlMod sequence *is* the calibration protocol.
45
+ * Resolve immediately so the orchestrator advances to the test charge.
46
+ */
47
+ enterCalibrationMode(): Promise<AckResult>;
48
+ exitCalibrationMode(): Promise<AckResult>;
49
+ /**
50
+ * Drive the same 3-step sequence proven by `handleStartGridCharge` in
51
+ * {@link SunspecBattery}: charge cap, grid-charging enable, storage mode.
52
+ * Throws on any step failure so the calibrator records a `failed` result.
53
+ */
54
+ startTestCharge(powerW: number): Promise<void>;
55
+ /**
56
+ * Hand the battery back to its normal AUTO / PV state. The snapshot
57
+ * service's restore takes care of returning wChaMax / inWRte / outWRte
58
+ * to baseline.
59
+ */
60
+ stopTestCharge(): Promise<void>;
61
+ readBatteryPowerW(): number | undefined;
62
+ readGridPowerW(): number | undefined;
63
+ }
@@ -0,0 +1,154 @@
1
+ import { AbstractBatteryCalibrationDriver, LatestValueCache, } from "@enyo-energy/appliance-calibration";
2
+ import { EnyoDataBusMessageEnum, } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
3
+ import { SunspecStorageMode } from "./sunspec-interfaces.js";
4
+ /**
5
+ * Subset of {@link SunspecBatteryControls} that `writeBatteryControls` accepts.
6
+ * Used both as the calibrator's snapshot payload and as the restore whitelist.
7
+ */
8
+ const WRITABLE_BATTERY_FIELDS = [
9
+ "storCtlMod",
10
+ "chaGriSet",
11
+ "wChaMax",
12
+ "inWRte",
13
+ "outWRte",
14
+ "minRsvPct",
15
+ ];
16
+ /**
17
+ * Vendor seam between `@enyo-energy/appliance-calibration`'s `BatteryCalibrator`
18
+ * and a SunSpec battery exposed via {@link SunspecModbusClient}.
19
+ *
20
+ * Battery power is fed in by the owning {@link SunspecBattery} on every readData
21
+ * cycle (call {@link updateBatteryPowerCache}). Grid power is sourced from the
22
+ * data bus: the driver subscribes to `MeterValuesUpdateV1` for the lifetime of
23
+ * the calibrator and caches the latest reading. Call {@link stop} when the
24
+ * battery disconnects to unsubscribe.
25
+ */
26
+ export class SunspecBatteryCalibrationDriver extends AbstractBatteryCalibrationDriver {
27
+ sunspecClient;
28
+ unitId;
29
+ dataBus;
30
+ batteryPowerCache = new LatestValueCache();
31
+ gridPowerCache = new LatestValueCache();
32
+ meterListenerId;
33
+ constructor(sunspecClient, unitId, dataBus) {
34
+ super();
35
+ this.sunspecClient = sunspecClient;
36
+ this.unitId = unitId;
37
+ this.dataBus = dataBus;
38
+ this.subscribeMeterUpdates();
39
+ }
40
+ subscribeMeterUpdates() {
41
+ this.meterListenerId = this.dataBus.listenForMessages([EnyoDataBusMessageEnum.MeterValuesUpdateV1], (entry) => {
42
+ if (entry.message !== EnyoDataBusMessageEnum.MeterValuesUpdateV1) {
43
+ return;
44
+ }
45
+ const meter = entry;
46
+ if (meter.data.gridPowerW !== undefined) {
47
+ this.gridPowerCache.set(meter.data.gridPowerW);
48
+ }
49
+ });
50
+ }
51
+ /**
52
+ * Tear down the meter subscription. Idempotent — call from
53
+ * `SunspecBattery.disconnect()`.
54
+ */
55
+ stop() {
56
+ if (this.meterListenerId) {
57
+ this.dataBus.unsubscribe(this.meterListenerId);
58
+ this.meterListenerId = undefined;
59
+ }
60
+ }
61
+ /**
62
+ * Push the latest computed battery power (W, positive = charging into the
63
+ * battery) from the owning device. Called from the readData loop.
64
+ */
65
+ updateBatteryPowerCache(powerW) {
66
+ this.batteryPowerCache.set(powerW);
67
+ }
68
+ async captureSnapshot() {
69
+ const controls = await this.sunspecClient.readBatteryControls(this.unitId);
70
+ if (!controls) {
71
+ throw new Error(`SunspecBatteryCalibrationDriver: readBatteryControls returned null for unit ${this.unitId}`);
72
+ }
73
+ return controls;
74
+ }
75
+ /**
76
+ * Write only the fields present in the snapshot AND in the writable whitelist.
77
+ * Per the library's contract this is best-effort: log on failure rather than
78
+ * throw, because the persisted snapshot has already been removed by the time
79
+ * we run and any throw would silently swallow what just happened.
80
+ */
81
+ async restoreFromSnapshot(snapshot, reason) {
82
+ const partial = {};
83
+ for (const field of WRITABLE_BATTERY_FIELDS) {
84
+ const value = snapshot[field];
85
+ if (value !== undefined) {
86
+ partial[field] = value;
87
+ }
88
+ }
89
+ if (Object.keys(partial).length === 0) {
90
+ console.log(`SunspecBatteryCalibrationDriver ${this.unitId}: restore (${reason}) — nothing to write`);
91
+ return;
92
+ }
93
+ try {
94
+ const ok = await this.sunspecClient.writeBatteryControls(this.unitId, partial);
95
+ if (!ok) {
96
+ console.error(`SunspecBatteryCalibrationDriver ${this.unitId}: restore (${reason}) writeBatteryControls returned false for [${Object.keys(partial).join(", ")}]`);
97
+ }
98
+ }
99
+ catch (error) {
100
+ console.error(`SunspecBatteryCalibrationDriver ${this.unitId}: restore (${reason}) threw: ${error}`);
101
+ }
102
+ }
103
+ /**
104
+ * SunSpec model 124 has no explicit calibration-mode register — the
105
+ * snapshot / restore + storCtlMod sequence *is* the calibration protocol.
106
+ * Resolve immediately so the orchestrator advances to the test charge.
107
+ */
108
+ async enterCalibrationMode() {
109
+ return "accepted";
110
+ }
111
+ async exitCalibrationMode() {
112
+ return "accepted";
113
+ }
114
+ /**
115
+ * Drive the same 3-step sequence proven by `handleStartGridCharge` in
116
+ * {@link SunspecBattery}: charge cap, grid-charging enable, storage mode.
117
+ * Throws on any step failure so the calibrator records a `failed` result.
118
+ */
119
+ async startTestCharge(powerW) {
120
+ const enabledGrid = await this.sunspecClient.enableGridCharging(this.unitId, true);
121
+ if (!enabledGrid) {
122
+ throw new Error("startTestCharge: failed to enable grid charging");
123
+ }
124
+ const wroteChaMax = await this.sunspecClient.writeBatteryControls(this.unitId, { wChaMax: powerW });
125
+ if (!wroteChaMax) {
126
+ throw new Error("startTestCharge: failed to set wChaMax");
127
+ }
128
+ const setMode = await this.sunspecClient.setStorageMode(this.unitId, SunspecStorageMode.CHARGE);
129
+ if (!setMode) {
130
+ throw new Error("startTestCharge: failed to set storage mode CHARGE");
131
+ }
132
+ }
133
+ /**
134
+ * Hand the battery back to its normal AUTO / PV state. The snapshot
135
+ * service's restore takes care of returning wChaMax / inWRte / outWRte
136
+ * to baseline.
137
+ */
138
+ async stopTestCharge() {
139
+ const setMode = await this.sunspecClient.setStorageMode(this.unitId, SunspecStorageMode.AUTO);
140
+ if (!setMode) {
141
+ throw new Error("stopTestCharge: failed to set storage mode AUTO");
142
+ }
143
+ const disabledGrid = await this.sunspecClient.enableGridCharging(this.unitId, false);
144
+ if (!disabledGrid) {
145
+ throw new Error("stopTestCharge: failed to disable grid charging");
146
+ }
147
+ }
148
+ readBatteryPowerW() {
149
+ return this.batteryPowerCache.get();
150
+ }
151
+ readGridPowerW() {
152
+ return this.gridPowerCache.get();
153
+ }
154
+ }