@enyo-energy/sunspec-sdk 0.0.70 → 0.0.72

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 (38) hide show
  1. package/README.md +302 -0
  2. package/dist/calibration-snapshot-service.d.ts +67 -0
  3. package/dist/calibration-snapshot-service.js +160 -0
  4. package/dist/cjs/calibration-snapshot-service.cjs +164 -0
  5. package/dist/cjs/calibration-snapshot-service.d.cts +67 -0
  6. package/dist/cjs/index.cjs +30 -1
  7. package/dist/cjs/index.d.cts +6 -0
  8. package/dist/cjs/sunspec-battery-calibration-driver.cjs +158 -0
  9. package/dist/cjs/sunspec-battery-calibration-driver.d.cts +63 -0
  10. package/dist/cjs/sunspec-battery-feature-calibrator.cjs +350 -0
  11. package/dist/cjs/sunspec-battery-feature-calibrator.d.cts +89 -0
  12. package/dist/cjs/sunspec-battery-schedule-handler.cjs +92 -0
  13. package/dist/cjs/sunspec-battery-schedule-handler.d.cts +67 -0
  14. package/dist/cjs/sunspec-calibration-storage.cjs +47 -0
  15. package/dist/cjs/sunspec-calibration-storage.d.cts +24 -0
  16. package/dist/cjs/sunspec-devices.cjs +407 -104
  17. package/dist/cjs/sunspec-devices.d.cts +112 -6
  18. package/dist/cjs/sunspec-interfaces.cjs +42 -1
  19. package/dist/cjs/sunspec-interfaces.d.cts +66 -0
  20. package/dist/cjs/version.cjs +1 -1
  21. package/dist/cjs/version.d.cts +1 -1
  22. package/dist/index.d.ts +6 -0
  23. package/dist/index.js +12 -0
  24. package/dist/sunspec-battery-calibration-driver.d.ts +63 -0
  25. package/dist/sunspec-battery-calibration-driver.js +154 -0
  26. package/dist/sunspec-battery-feature-calibrator.d.ts +89 -0
  27. package/dist/sunspec-battery-feature-calibrator.js +345 -0
  28. package/dist/sunspec-battery-schedule-handler.d.ts +67 -0
  29. package/dist/sunspec-battery-schedule-handler.js +88 -0
  30. package/dist/sunspec-calibration-storage.d.ts +24 -0
  31. package/dist/sunspec-calibration-storage.js +42 -0
  32. package/dist/sunspec-devices.d.ts +112 -6
  33. package/dist/sunspec-devices.js +408 -105
  34. package/dist/sunspec-interfaces.d.ts +66 -0
  35. package/dist/sunspec-interfaces.js +41 -0
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/package.json +7 -3
@@ -1,10 +1,11 @@
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 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 { BatteryCalibrator, type BatteryCalibratorConfig, type CalibrationResultStore, type CalibrationSnapshot, type RestoreReason, SnapshotService } from "@enyo-energy/appliance-calibration";
8
9
  import { EnergyAppDataBus } from "@enyo-energy/energy-app-sdk/dist/packages/energy-app-data-bus.js";
9
10
  /**
10
11
  * Base abstract class for all Sunspec devices
@@ -25,6 +26,13 @@ export declare abstract class BaseSunspecDevice {
25
26
  protected dataBus?: EnergyAppDataBus;
26
27
  protected retryManager: ConnectionRetryManager;
27
28
  protected consecutiveReconnectFailures: number;
29
+ /**
30
+ * Prefix used when persisting calibration snapshots via the library's
31
+ * {@link SnapshotService}. Kept identical to the key the SDK used before
32
+ * the migration to `@enyo-energy/appliance-calibration` so existing
33
+ * snapshots stored by older builds are still picked up on startup.
34
+ */
35
+ protected static readonly CALIBRATION_SNAPSHOT_KEY_PREFIX = "sunspec-calibration-snapshot-";
28
36
  constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number, port?: number, baseAddress?: number, retryConfig?: IRetryConfig, appliance?: EnyoAppliance, useTls?: boolean | undefined);
29
37
  /**
30
38
  * Connect to the device and create/update the appliance
@@ -77,6 +85,12 @@ export declare abstract class BaseSunspecDevice {
77
85
  */
78
86
  protected markOffline(): Promise<void>;
79
87
  protected sendCommandAcknowledge(messageId: string, acknowledgeMessage: EnyoDataBusMessageEnum | string, answer: EnyoCommandAcknowledgeAnswerEnum, rejectionReason?: string): void;
88
+ /**
89
+ * Build a typed {@link SnapshotService} bound to this appliance's calibration storage,
90
+ * using the legacy SDK storage key prefix so prior installs upgrade seamlessly. Returns
91
+ * undefined if the appliance has not been registered yet.
92
+ */
93
+ protected buildSnapshotService<T>(onRestore: (snapshot: CalibrationSnapshot<T>, reason: RestoreReason) => Promise<void>): SnapshotService<T> | undefined;
80
94
  }
81
95
  /**
82
96
  * Sunspec Inverter implementation using dynamic model discovery
@@ -87,6 +101,7 @@ export declare class SunspecInverter extends BaseSunspecDevice {
87
101
  private static readonly CONNECTION_FAULT_THRESHOLD;
88
102
  private storage?;
89
103
  private errorState;
104
+ private snapshotService?;
90
105
  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);
91
106
  connect(): Promise<void>;
92
107
  disconnect(): Promise<void>;
@@ -156,13 +171,77 @@ export declare class SunspecInverter extends BaseSunspecDevice {
156
171
  stopDataBusListening(): void;
157
172
  private handleInverterCommand;
158
173
  private handleSetFeedInLimit;
174
+ /**
175
+ * Lazily construct the per-appliance {@link SnapshotService}. Reloads any
176
+ * persisted snapshot from storage so an in-flight calibration survives
177
+ * process restarts. Idempotent.
178
+ */
179
+ private initSnapshotService;
180
+ private handleStartCalibration;
181
+ private handleStopCalibration;
182
+ /**
183
+ * `onRestore` callback for the inverter's {@link SnapshotService}. Writes only the
184
+ * subset of writable inverter fields that other commands actually touched during the
185
+ * calibration. Catches and logs failures rather than throwing — the snapshot has
186
+ * already been removed from storage by the time this runs, so a throw is unrecoverable
187
+ * and is best logged for operator action.
188
+ */
189
+ private restoreInverterSnapshot;
159
190
  }
160
191
  /**
161
192
  * Sunspec Battery implementation
162
193
  */
163
194
  export declare class SunspecBattery extends BaseSunspecDevice {
195
+ private readonly featureMode;
164
196
  private readonly capabilities;
165
- 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);
197
+ private snapshotService?;
198
+ private calibrationDriver?;
199
+ private batteryCalibrator?;
200
+ private calibrationResultStore?;
201
+ private scheduleHandler;
202
+ /**
203
+ * Battery features that imply outbound control writes from the host action-taker.
204
+ * These are stripped from `appliance.battery.features` until the calibration result
205
+ * store says this battery has been successfully calibrated — see
206
+ * {@link computeAdvertisedFeatures}.
207
+ */
208
+ private static readonly CONTROLLABLE_FEATURES;
209
+ 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);
210
+ /**
211
+ * Wire the battery into `@enyo-energy/appliance-calibration`'s
212
+ * `BatteryCalibrator` test-charge flow. Call once after {@link connect}.
213
+ *
214
+ * Consumers own the `CalibrationResultStore` (shared across appliances so
215
+ * the persisted result map doesn't get clobbered) and the
216
+ * `CalibrationTrigger` that drives `runCalibration()` on whatever schedule
217
+ * makes sense for the host. Pass overrides for the calibrator's defaults
218
+ * (`testPowerW`, response thresholds, etc.) via `opts.config`.
219
+ */
220
+ configureCalibration(opts: {
221
+ resultStore: CalibrationResultStore;
222
+ config?: Partial<BatteryCalibratorConfig>;
223
+ }): BatteryCalibrator<SunspecBatteryControls>;
224
+ /**
225
+ * Detect which battery features the device's registers expose. This is the
226
+ * raw, register-only view; the {@link featureMode} configured at
227
+ * construction decides what is actually published — see
228
+ * {@link resolveAdvertisedFeatures}.
229
+ */
230
+ private detectFromRegisters;
231
+ /**
232
+ * Resolve `appliance.battery.features` for the configured mode. Called on
233
+ * every connect() and readData() cycle so that, in calibration-based mode,
234
+ * the controllable features appear as soon as the calibrator flips
235
+ * `isCalibrated` (no synchronous push).
236
+ */
237
+ private resolveAdvertisedFeatures;
238
+ /**
239
+ * Returns the per-battery `BatteryCalibrator` created by
240
+ * {@link configureCalibration}, or `undefined` if calibration was never
241
+ * configured. Consumers register the returned instance with their own
242
+ * `CalibrationTrigger`.
243
+ */
244
+ getBatteryCalibrator(): BatteryCalibrator<SunspecBatteryControls> | undefined;
166
245
  /**
167
246
  * Connect to the battery and create/update the appliance
168
247
  */
@@ -284,10 +363,30 @@ export declare class SunspecBattery extends BaseSunspecDevice {
284
363
  */
285
364
  stopDataBusListening(): void;
286
365
  private handleStorageCommand;
287
- private handleStartGridCharge;
288
- private handleStopGridCharge;
289
- private handleSetDischargeLimit;
290
- private handleSetChargeLimit;
366
+ /**
367
+ * Acknowledge a SetStorageScheduleV1 message. The actual schedule application
368
+ * is owned by `this.scheduleHandler` (subscribed independently via its own
369
+ * data-bus listener registered in its constructor). The ack reports "received
370
+ * & queued" rather than "applied" — schedule entries play out over time, and
371
+ * per-entry write failures land in `console.error` via the handler's
372
+ * onChange wrapper.
373
+ */
374
+ private handleSetStorageScheduleAck;
375
+ /**
376
+ * Lazily construct the per-appliance {@link SnapshotService}. Reloads any
377
+ * persisted snapshot from storage so an in-flight calibration survives
378
+ * process restarts. Idempotent.
379
+ */
380
+ private initSnapshotService;
381
+ private handleStartCalibration;
382
+ private handleStopCalibration;
383
+ /**
384
+ * `onRestore` callback for the battery's {@link SnapshotService}. Writes only the
385
+ * subset of writable battery fields touched during the calibration. Errors are
386
+ * caught and logged — by the time this fires the snapshot is gone from storage,
387
+ * so a throw would just propagate into the void.
388
+ */
389
+ private restoreBatterySnapshot;
291
390
  }
292
391
  /**
293
392
  * Sunspec Meter implementation
@@ -303,6 +402,13 @@ export declare class SunspecMeter extends BaseSunspecDevice {
303
402
  * Disconnect from the meter and update appliance state
304
403
  */
305
404
  disconnect(): Promise<void>;
405
+ /**
406
+ * Meter does not implement calibration; it only subscribes to Start/StopCalibrationV1 to
407
+ * answer NotSupported (per the data-bus contract that every command must be acknowledged).
408
+ */
409
+ startDataBusListening(): void;
410
+ stopDataBusListening(): void;
411
+ private handleMeterCommand;
306
412
  /**
307
413
  * Update meter data and return data bus messages
308
414
  */
@@ -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.70';
12
+ exports.SDK_VERSION = '0.0.72';
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.70";
8
+ export declare const SDK_VERSION = "0.0.72";
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
@@ -3,3 +3,9 @@ export * from './sunspec-devices.js';
3
3
  export * from './sunspec-modbus-client.js';
4
4
  export { ConnectionRetryManager } from './connection-retry-manager.js';
5
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
@@ -3,3 +3,15 @@ export * from './sunspec-devices.js';
3
3
  export * from './sunspec-modbus-client.js';
4
4
  export { ConnectionRetryManager } from './connection-retry-manager.js';
5
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
+ }