@enyo-energy/sunspec-sdk 0.0.71 → 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 (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 +270 -215
  13. package/dist/cjs/sunspec-devices.d.cts +99 -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 +99 -19
  29. package/dist/sunspec-devices.js +271 -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
package/README.md CHANGED
@@ -5,6 +5,9 @@ SunSpec Modbus client for reading data from solar inverters, batteries, meters,
5
5
  ## Table of Contents
6
6
 
7
7
  - [Appliance Manager Integration](#appliance-manager-integration)
8
+ - [Battery Feature Modes](#battery-feature-modes)
9
+ - [Battery Calibration](#battery-calibration)
10
+ - [Storage Schedule Control](#storage-schedule-control)
8
11
  - [How Addressing Works](#how-addressing-works)
9
12
  - [Bulk Register Reading](#bulk-register-reading)
10
13
  - [Data Types](#data-types)
@@ -46,6 +49,305 @@ This SDK does not configure the strategy itself — it is the consumer app's res
46
49
 
47
50
  ---
48
51
 
52
+ ## Battery Feature Modes
53
+
54
+ `SunspecBattery` populates `appliance.battery.features` for every connected battery — the list the host's topology / UI reads to decide which control surfaces to render. **The consumer chooses how that list is computed** by passing a required `featureMode` argument to the `SunspecBattery` constructor. There is no default; the choice is explicit so a SunSpec device whose register surface lies (or whose responsiveness is unknown) cannot silently turn into an actionable battery from the host's point of view.
55
+
56
+ Three kinds are available, exposed via the `SunspecBatteryFeatureModeKind` enum:
57
+
58
+ | Kind | Calibration allowed? | What appears in `appliance.battery.features` |
59
+ |---|---|---|
60
+ | `Disabled` | No (`configureCalibration` throws) | Exactly the `allowedFeatures` array the consumer passes — register state is ignored. |
61
+ | `RegisterBased` | No (`configureCalibration` throws) | Features the SunSpec registers expose. Filtered down to `allowedFeatures` if that's set. |
62
+ | `CalibrationBased` | Yes (required for controllable features to ever appear) | Same as `RegisterBased` for read-only features. The four controllable features stay hidden until the calibration result store reports the appliance as `calibrated`. |
63
+
64
+ The four features the SDK treats as **controllable** (the ones calibration gates):
65
+
66
+ - `EnyoBatteryFeature.GridCharging`
67
+ - `EnyoBatteryFeature.GridDischarging`
68
+ - `EnyoBatteryFeature.ChargeLimitation`
69
+ - `EnyoBatteryFeature.DischargeLimitation`
70
+
71
+ Every other `EnyoBatteryFeature` is **read-only** from the SDK's perspective and is published as soon as the corresponding register is present (in `RegisterBased` and `CalibrationBased`) or as soon as the consumer lists it (in `Disabled`).
72
+
73
+ ### `Disabled`
74
+
75
+ Picks no registers, runs no calibration. Whatever the consumer lists in `allowedFeatures` becomes the published list; omit it (or pass `[]`) to publish nothing.
76
+
77
+ Use when the host has out-of-band knowledge of the battery's capabilities — e.g. a curated commissioning database — and doesn't want the SDK inferring anything from registers it may not trust. Also useful for read-only deployments: omit `allowedFeatures` and the host will treat the battery as observation-only.
78
+
79
+ ```ts
80
+ import { SunspecBattery, SunspecBatteryFeatureModeKind } from '@enyo-energy/sunspec-sdk';
81
+ import { EnyoBatteryFeature } from '@enyo-energy/energy-app-sdk';
82
+
83
+ const observationOnly = new SunspecBattery(
84
+ app, name, networkDevice, client, applianceManager,
85
+ { kind: SunspecBatteryFeatureModeKind.Disabled },
86
+ // → appliance.battery.features = []
87
+ );
88
+
89
+ const curated = new SunspecBattery(
90
+ app, name, networkDevice, client, applianceManager,
91
+ {
92
+ kind: SunspecBatteryFeatureModeKind.Disabled,
93
+ allowedFeatures: [EnyoBatteryFeature.GridCharging],
94
+ },
95
+ // → appliance.battery.features = ['grid-charging'], regardless of what the registers say
96
+ );
97
+ ```
98
+
99
+ Calling `configureCalibration(...)` on a `Disabled` battery throws.
100
+
101
+ ### `RegisterBased`
102
+
103
+ The SDK looks at the SunSpec Model 124 registers and publishes the features whose underlying registers are present:
104
+
105
+ - `chaGriSet` register present → `EnyoBatteryFeature.GridCharging`
106
+ - `wChaMax` register present → `EnyoBatteryFeature.ChargeLimitation`
107
+
108
+ Pass `allowedFeatures` to intersect the detected set with a whitelist — useful for hiding features the device exposes but the host doesn't want exercised. The allow-list **never** adds features that the registers don't expose; it is strictly a filter.
109
+
110
+ ```ts
111
+ const trustRegisters = new SunspecBattery(
112
+ app, name, networkDevice, client, applianceManager,
113
+ { kind: SunspecBatteryFeatureModeKind.RegisterBased },
114
+ // → appliance.battery.features reflects whichever Model 124 controls are exposed
115
+ );
116
+
117
+ const trustRegistersButHideGridCharging = new SunspecBattery(
118
+ app, name, networkDevice, client, applianceManager,
119
+ {
120
+ kind: SunspecBatteryFeatureModeKind.RegisterBased,
121
+ allowedFeatures: [EnyoBatteryFeature.ChargeLimitation],
122
+ },
123
+ // → publishes ChargeLimitation only (even if chaGriSet is present)
124
+ );
125
+ ```
126
+
127
+ Calling `configureCalibration(...)` on a `RegisterBased` battery throws.
128
+
129
+ ### `CalibrationBased`
130
+
131
+ Same register-driven detection as `RegisterBased` for read-only features, but the four controllable features stay stripped from the published list until `CalibrationResultStore.isCalibrated(applianceId)` returns `true` for this battery. Once the verdict flips, the next `readData()` cycle republishes the full set.
132
+
133
+ `configureCalibration(...)` must be called after `connect()` — without it, controllable features stay hidden forever (safe-fallback behaviour).
134
+
135
+ ```ts
136
+ const verified = new SunspecBattery(
137
+ app, name, networkDevice, client, applianceManager,
138
+ { kind: SunspecBatteryFeatureModeKind.CalibrationBased },
139
+ );
140
+ await verified.connect();
141
+ verified.configureCalibration({ resultStore }); // see "Battery Calibration" below
142
+ trigger.register(verified.getBatteryCalibrator()!);
143
+ // Until trigger.tick() runs a successful test charge, controllable features are absent.
144
+ // After it succeeds, they reappear within one readData() cycle.
145
+ ```
146
+
147
+ See [Battery Calibration](#battery-calibration) for the full wiring of `resultStore`, `trigger`, and `configureCalibration` config.
148
+
149
+ ### Choosing a mode
150
+
151
+ | Situation | Pick |
152
+ |---|---|
153
+ | Battery is observation-only, or the host already knows its capabilities | `Disabled` |
154
+ | Trust the SunSpec register surface; no responsiveness verification needed | `RegisterBased` |
155
+ | Want a test charge to confirm the battery actually reacts before exposing controls | `CalibrationBased` |
156
+
157
+ The mode is fixed for the lifetime of the `SunspecBattery` instance. To switch modes, construct a new instance.
158
+
159
+ ---
160
+
161
+ ## Battery Calibration
162
+
163
+ The SDK integrates [`@enyo-energy/appliance-calibration`](https://www.npmjs.com/package/@enyo-energy/appliance-calibration) to verify that a battery actually responds to control commands before the host action-taker is allowed to send it any. The verification is a short test charge wrapped in a snapshot/restore so a hung or crashed run never leaves writable registers in a half-modified state, and the verdict (`calibrated` / `not-supported` / `failed`) is persisted per appliance so it survives restarts.
164
+
165
+ The SDK owns the SunSpec-specific pieces — the snapshot service, the vendor driver, the per-battery `BatteryCalibrator`. The **consumer owns the trigger and the result store**, so scheduling, multi-tenant storage namespacing, and command gating stay configurable.
166
+
167
+ ### Architecture
168
+
169
+ | Piece | Lives in | Who builds it |
170
+ |---|---|---|
171
+ | `SunspecCalibrationStorage` (adapter over `EnergyAppStorage`) | this SDK | consumer (factory: `createSunspecCalibrationStorage(app)`) |
172
+ | `CalibrationResultStore` (persisted verdict map) | `appliance-calibration` | consumer — **one shared instance** across batteries |
173
+ | `SnapshotService<SunspecBatteryControls>` | `appliance-calibration` | SDK (per-battery, inside `SunspecBattery`) |
174
+ | `SunspecBatteryCalibrationDriver` | this SDK | SDK (per-battery, inside `SunspecBattery`) |
175
+ | `BatteryCalibrator<SunspecBatteryControls>` | `appliance-calibration` | SDK — built by `configureCalibration`, exposed via `getBatteryCalibrator` |
176
+ | `CalibrationTrigger` | `appliance-calibration` | consumer — drives `runCalibration()` on its own schedule |
177
+
178
+ ### Feature mode
179
+
180
+ Calibration is only relevant when `SunspecBattery` is constructed with `SunspecBatteryFeatureModeKind.CalibrationBased` — that's the mode under which the SDK gates controllable features on the calibration verdict. See [Battery Feature Modes](#battery-feature-modes) above for the full mode discussion; the rest of this section assumes the calibration-based mode has been selected.
181
+
182
+ ### Consumer wiring
183
+
184
+ ```ts
185
+ import {
186
+ SunspecBattery,
187
+ SunspecBatteryFeatureModeKind,
188
+ // re-exported from @enyo-energy/appliance-calibration so you don't need a second import:
189
+ CalibrationResultStore,
190
+ CalibrationTrigger,
191
+ createSunspecCalibrationStorage,
192
+ } from '@enyo-energy/sunspec-sdk';
193
+
194
+ // 1. One storage adapter + one result store for the whole app — sharing the result store
195
+ // across batteries keeps the persisted verdict map from being clobbered by per-instance writes.
196
+ const storage = createSunspecCalibrationStorage(app);
197
+ const resultStore = new CalibrationResultStore(storage);
198
+ await resultStore.initialize();
199
+
200
+ // 2. One trigger that fans out across every registered calibrator.
201
+ const trigger = new CalibrationTrigger({ resultStore });
202
+
203
+ // 3. Per battery: build it with the desired feature mode, then connect,
204
+ // then (only in calibration-based mode) configureCalibration + register.
205
+ for (const config of batteryConfigs) {
206
+ const battery = new SunspecBattery(
207
+ app, name, networkDevice, client, applianceManager,
208
+ { kind: SunspecBatteryFeatureModeKind.CalibrationBased },
209
+ // unitId, port, baseAddress, ...
210
+ );
211
+ await battery.connect();
212
+ battery.configureCalibration({
213
+ resultStore,
214
+ // Optional — override any BatteryCalibratorConfig defaults (see "Tuning" below).
215
+ config: { testPowerW: 300 },
216
+ });
217
+ const calibrator = battery.getBatteryCalibrator();
218
+ if (calibrator) {
219
+ trigger.register(calibrator);
220
+ }
221
+ }
222
+
223
+ // 4. Drive the trigger on your own cadence. tick() runs at most one calibration per call
224
+ // and is a no-op for any battery that already has a stored result.
225
+ client.useInterval().createInterval('30m', () => trigger.tick());
226
+ ```
227
+
228
+ ### Gate commands on the calibration result
229
+
230
+ In whatever code emits battery commands, check the store synchronously before sending:
231
+
232
+ ```ts
233
+ if (!resultStore.isCalibrated(applianceId)) {
234
+ console.log(`Skipping ${applianceId}: not calibrated yet`);
235
+ return;
236
+ }
237
+ // ...send the command
238
+ ```
239
+
240
+ ### Per-feature probes
241
+
242
+ In `calibration-based` mode the SDK runs `SunspecBatteryFeatureCalibrator` (a subclass of the library's `BatteryCalibrator`) which **exercises each of the four controllable features individually** and records the outcomes in `CalibrationResult.notes` as JSON. Only features whose probes pass land in `appliance.battery.features` after calibration.
243
+
244
+ The session is one snapshot → four probes → one restore. Probes run in this order so the battery has energy to discharge with by the end:
245
+
246
+ | # | Probe | What it writes | Pass condition |
247
+ |---|---|---|---|
248
+ | 1 | `GridCharging` | `chaGriSet=GRID`, `wChaMax=testPowerW`, `inWRte=100`, `storCtlMod=CHARGE` | Battery power AND grid power both rise above `responseThresholdW` |
249
+ | 2 | `ChargeLimitation` | same as above with `inWRte=50` | Charge power settles within `±responseThresholdW` of `testPowerW × 0.5` |
250
+ | 3 | `DischargeLimitation` | `chaGriSet=PV`, `outWRte=50`, `storCtlMod=DISCHARGE` | Discharge power settles within `±responseThresholdW` of `testPowerW × 0.5` |
251
+ | 4 | `GridDischarging` | `chaGriSet=PV`, `outWRte=100`, `storCtlMod=DISCHARGE` | Battery discharges AND meter shows reverse flow above `responseThresholdW` |
252
+
253
+ Each probe runs a `writeBatteryControls(...)` then polls the driver's cached battery/grid power until either the predicate passes or `responseTimeoutMs` elapses. Between probes the SDK resets to a neutral state (`storCtlMod=AUTO`, `chaGriSet=PV`, `inWRte/outWRte=100`, `wChaMax=baseline`) so each probe starts cleanly. The final `restoreFromSnapshot` (run by the `SnapshotService` on `stopCalibration`) writes the original pre-calibration register set back regardless of outcome.
254
+
255
+ #### SoC preconditions
256
+
257
+ Probes refuse to run outside a useful SoC band and return `not-supported` instead — distinct from `failed` so the host can tell "device won't" apart from "we couldn't even try":
258
+
259
+ - Charge probes (`GridCharging`, `ChargeLimitation`) need headroom: SoC ≥ 90% → `not-supported`.
260
+ - Discharge probes (`DischargeLimitation`, `GridDischarging`) need energy: SoC ≤ 20% → `not-supported`.
261
+
262
+ #### Aggregate verdict
263
+
264
+ The library's `CalibrationResult.state` (one value per appliance) is computed from the four per-feature verdicts:
265
+
266
+ | Per-feature verdicts | `CalibrationResult.state` |
267
+ |---|---|
268
+ | At least one `passed` | `calibrated` |
269
+ | All `not-supported` | `not-supported` |
270
+ | Otherwise (mix of `failed` / `not-supported`) | `failed` |
271
+
272
+ `isCalibrated(applianceId)` therefore reads as **"the SDK probed this device and at least something is controllable"** — keep using it as the broad command-emission gate. The per-feature breakdown stored in `notes` is what `resolveAdvertisedFeatures` consults to decide *which* controllable features to publish; `decodeFeatureResults(result.notes)` returns the passing set and is exported for consumers that want to inspect it directly.
273
+
274
+ #### Other operational notes
275
+
276
+ - **Republishing cadence**: the controllable features reappear on the next `readData()` cycle after the calibrator persists its result — not synchronously when calibration completes. With a typical 10s/30s/1m poll cadence the appliance metadata follows within one cycle.
277
+ - **Defence in depth**: the feature filter is complementary to the [`isCalibrated` command-gate](#gate-commands-on-the-calibration-result), not a substitute. The consumer's action-taker should still check `resultStore.isCalibrated(applianceId)` synchronously before every command write — the filter keeps the controllable surface off the appliance until the device is verified, but a race window exists between calibration completing and the next readData cycle.
278
+ - **GridDischarge ambiguity**: SunSpec Model 124 has no register to *force* battery output toward the grid versus toward local load. The probe drives the battery into discharge and then watches the meter for export. If the host's local load eats the discharge there will be no export and the probe records `failed` with a note explaining the likely cause. Hosts that know their grid topology can map this to "controllable, but couldn't be verified".
279
+
280
+ ### What `configureCalibration` does under the hood
281
+
282
+ For each battery it builds:
283
+
284
+ - A `SunspecBatteryCalibrationDriver` that:
285
+ - Snapshots writable controls via `readBatteryControls()`.
286
+ - Restores them via `writeBatteryControls()` (whitelist: `storCtlMod`, `chaGriSet`, `wChaMax`, `inWRte`, `outWRte`, `minRsvPct`).
287
+ - Runs the test charge with the same 3-step sequence as `handleStartGridCharge` (`enableGridCharging(true)`, `wChaMax = powerW`, `setStorageMode(CHARGE)`).
288
+ - Reads battery power from a `LatestValueCache` fed by `SunspecBattery.readData()` each cycle.
289
+ - Reads grid power from a `LatestValueCache` fed by a `MeterValuesUpdateV1` data-bus subscription that's live for the driver's lifetime (so the grid signal works regardless of where the meter actually comes from).
290
+ - A `BatteryCalibrator<SunspecBatteryControls>` wired to the driver, the per-battery `SnapshotService`, and the shared `resultStore`.
291
+
292
+ Battery `disconnect()` tears the driver subscription down.
293
+
294
+ ### What still happens through the data bus
295
+
296
+ The SDK also keeps listening for `StartCalibrationV1` / `StopCalibrationV1` data-bus messages so an external orchestrator (e.g. the action-taker) can begin and end a manual calibration session. Those handlers now drive the same `SnapshotService<T>` instance from `@enyo-energy/appliance-calibration`. While a manual session is active, any concurrent `SetInverterFeedInLimitV1` / `SetStorageChargeLimitV1` / etc. call records the field it touched into `modifiedFields`, and only those fields are written back on stop (or on the 5-minute auto-stop). This works exactly as before — the change is purely the implementation underneath.
297
+
298
+ ### Re-running a calibration
299
+
300
+ The trigger runs each battery **once** — appliances already marked `calibrated`, `not-supported`, or `failed` are skipped on every subsequent `tick()`. To force a re-run, drop the persisted result for that key:
301
+
302
+ ```ts
303
+ await app.useStorage().remove('battery-calibration'); // wipes ALL results (the default key)
304
+ ```
305
+
306
+ If you need targeted invalidation, snapshot the store via `resultStore.snapshot()`, mutate the map, and re-save the survivors. Periodic re-calibration is a host concern — build it on top of `calibrator.runCalibration()` directly rather than going through `CalibrationTrigger`.
307
+
308
+ ### Tuning
309
+
310
+ Override defaults via `configureCalibration({ resultStore, config })`:
311
+
312
+ | Key | Default | When to change |
313
+ |---|---|---|
314
+ | `testPowerW` | 500 W | Smaller batteries; PV-only sites where 500 W is visible. |
315
+ | `responseThresholdW` | 200 W | Noisier meters; tighter tolerance for known-good devices. |
316
+ | `ackTimeoutMs` | 10 s | Slow Modbus links. |
317
+ | `responseTimeoutMs` | 20 s | Devices that take longer to ramp. |
318
+ | `pollIntervalMs` | 2 s | Faster polling for short-window tests. |
319
+
320
+ See [`@enyo-energy/appliance-calibration`'s README](https://www.npmjs.com/package/@enyo-energy/appliance-calibration) for the full crash-safety contract and every result-store / snapshot-service knob.
321
+
322
+ ---
323
+
324
+ ## Storage Schedule Control
325
+
326
+ `SunspecBattery` reacts to a single data-bus command for control: `EnyoDataBusSetStorageScheduleV1`. The message carries a `mode` (`auto` or `schedule`) and, when `mode` is `schedule`, a sorted `relativeSchedule` of `{seconds, direction, powerW}` entries. Subscription is automatic — each battery wires its own `SunspecBatteryScheduleHandler` (extending the SDK's `StorageScheduleHandler` from `@enyo-energy/energy-app-sdk`) during `connect()`. There is no consumer setup beyond connecting the battery.
327
+
328
+ ### What the SDK does on receipt
329
+
330
+ 1. Acks the message with `Accepted` immediately ("received & queued" — schedule entries play out over time).
331
+ 2. Snapshots the writable Model 124 registers (`storCtlMod`, `chaGriSet`, `wChaMax`, `inWRte`, `outWRte`) and persists them to `EnergyAppStorage` for restart-safe rollback.
332
+ 3. Activates the first entry immediately, then advances to subsequent entries as their `seconds` offsets elapse on a 1-second tick (driven by `EnergyApp.useInterval()`).
333
+ 4. Each `Charge` entry writes `chaGriSet=GRID`, `inWRte = powerW / installedWChaMax × 100`, `storCtlMod=CHARGE`.
334
+ 5. Each `Discharge` entry writes `chaGriSet=PV`, `outWRte = powerW / installedWChaMax × 100`, `storCtlMod=DISCHARGE`.
335
+ 6. On `mode: auto`, a replacement schedule, `disconnect()`, or process restart with an active schedule still persisted, the SDK restores the snapshotted pre-schedule registers via `onRollback`.
336
+
337
+ ### Power cap
338
+
339
+ Scheduled `powerW` is clamped to the device's installed `wChaMax` because the handler holds that value stable as the denominator for the percentage math throughout the schedule. To request more than the installed maximum, raise `wChaMax` out-of-band before sending the schedule.
340
+
341
+ ### Calibration coexistence
342
+
343
+ Each per-entry write is recorded into the calibration `SnapshotService` (when calibration is configured), so a calibration that starts mid-schedule still restores the right register set on stop / auto-stop.
344
+
345
+ ### Legacy messages
346
+
347
+ The earlier `StartStorageGridChargeV1` / `StopStorageGridChargeV1` / `SetStorageChargeLimitV1` / `SetStorageDischargeLimitV1` family is **no longer handled** by this SDK. Their type defs in `@enyo-energy/energy-app-sdk` are flagged `@deprecated` with pointers to `SetStorageScheduleV1`. Migrate any callers that still emit them.
348
+
349
+ ---
350
+
49
351
  ## How Addressing Works
50
352
 
51
353
  ### Base Address Detection
@@ -14,13 +14,41 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.getSdkVersion = exports.SDK_VERSION = exports.ConnectionRetryManager = void 0;
17
+ exports.CalibrationTrigger = exports.CalibrationResultStore = exports.DEFAULT_BATTERY_CALIBRATOR_CONFIG = exports.BatteryCalibrator = exports.AbstractBatteryCalibrationDriver = exports.DEFAULT_AUTO_STOP_MS = exports.SnapshotService = exports.InMemoryCalibrationStorage = exports.AbstractCalibrationStorage = exports.EnyoStorageScheduleDirectionEnum = exports.EnyoStorageScheduleModeEnum = exports.SunspecBatteryScheduleHandler = exports.decodeFeatureResults = exports.SunspecBatteryFeatureCalibrator = exports.SunspecBatteryCalibrationDriver = exports.createSunspecCalibrationStorage = exports.SunspecCalibrationStorage = exports.getSdkVersion = exports.SDK_VERSION = exports.ConnectionRetryManager = void 0;
18
18
  __exportStar(require("./sunspec-interfaces.cjs"), exports);
19
19
  __exportStar(require("./sunspec-devices.cjs"), exports);
20
20
  __exportStar(require("./sunspec-modbus-client.cjs"), exports);
21
- __exportStar(require("./calibration-snapshot-service.cjs"), exports);
22
21
  var connection_retry_manager_js_1 = require("./connection-retry-manager.cjs");
23
22
  Object.defineProperty(exports, "ConnectionRetryManager", { enumerable: true, get: function () { return connection_retry_manager_js_1.ConnectionRetryManager; } });
24
23
  var version_js_1 = require("./version.cjs");
25
24
  Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return version_js_1.SDK_VERSION; } });
26
25
  Object.defineProperty(exports, "getSdkVersion", { enumerable: true, get: function () { return version_js_1.getSdkVersion; } });
26
+ // New calibration integration with @enyo-energy/appliance-calibration
27
+ var sunspec_calibration_storage_js_1 = require("./sunspec-calibration-storage.cjs");
28
+ Object.defineProperty(exports, "SunspecCalibrationStorage", { enumerable: true, get: function () { return sunspec_calibration_storage_js_1.SunspecCalibrationStorage; } });
29
+ Object.defineProperty(exports, "createSunspecCalibrationStorage", { enumerable: true, get: function () { return sunspec_calibration_storage_js_1.createSunspecCalibrationStorage; } });
30
+ var sunspec_battery_calibration_driver_js_1 = require("./sunspec-battery-calibration-driver.cjs");
31
+ Object.defineProperty(exports, "SunspecBatteryCalibrationDriver", { enumerable: true, get: function () { return sunspec_battery_calibration_driver_js_1.SunspecBatteryCalibrationDriver; } });
32
+ var sunspec_battery_feature_calibrator_js_1 = require("./sunspec-battery-feature-calibrator.cjs");
33
+ Object.defineProperty(exports, "SunspecBatteryFeatureCalibrator", { enumerable: true, get: function () { return sunspec_battery_feature_calibrator_js_1.SunspecBatteryFeatureCalibrator; } });
34
+ Object.defineProperty(exports, "decodeFeatureResults", { enumerable: true, get: function () { return sunspec_battery_feature_calibrator_js_1.decodeFeatureResults; } });
35
+ var sunspec_battery_schedule_handler_js_1 = require("./sunspec-battery-schedule-handler.cjs");
36
+ Object.defineProperty(exports, "SunspecBatteryScheduleHandler", { enumerable: true, get: function () { return sunspec_battery_schedule_handler_js_1.SunspecBatteryScheduleHandler; } });
37
+ // Re-export the schedule message types from energy-app-sdk so consumers can
38
+ // build/inspect SetStorageScheduleV1 messages without a second import.
39
+ var enyo_data_bus_value_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js");
40
+ Object.defineProperty(exports, "EnyoStorageScheduleModeEnum", { enumerable: true, get: function () { return enyo_data_bus_value_js_1.EnyoStorageScheduleModeEnum; } });
41
+ Object.defineProperty(exports, "EnyoStorageScheduleDirectionEnum", { enumerable: true, get: function () { return enyo_data_bus_value_js_1.EnyoStorageScheduleDirectionEnum; } });
42
+ // Re-export the library symbols consumers need to wire up the trigger and gate
43
+ // commands. Avoids a second peer dependency on @enyo-energy/appliance-calibration
44
+ // while keeping the consumer wiring concise.
45
+ var appliance_calibration_1 = require("@enyo-energy/appliance-calibration");
46
+ Object.defineProperty(exports, "AbstractCalibrationStorage", { enumerable: true, get: function () { return appliance_calibration_1.AbstractCalibrationStorage; } });
47
+ Object.defineProperty(exports, "InMemoryCalibrationStorage", { enumerable: true, get: function () { return appliance_calibration_1.InMemoryCalibrationStorage; } });
48
+ Object.defineProperty(exports, "SnapshotService", { enumerable: true, get: function () { return appliance_calibration_1.SnapshotService; } });
49
+ Object.defineProperty(exports, "DEFAULT_AUTO_STOP_MS", { enumerable: true, get: function () { return appliance_calibration_1.DEFAULT_AUTO_STOP_MS; } });
50
+ Object.defineProperty(exports, "AbstractBatteryCalibrationDriver", { enumerable: true, get: function () { return appliance_calibration_1.AbstractBatteryCalibrationDriver; } });
51
+ Object.defineProperty(exports, "BatteryCalibrator", { enumerable: true, get: function () { return appliance_calibration_1.BatteryCalibrator; } });
52
+ Object.defineProperty(exports, "DEFAULT_BATTERY_CALIBRATOR_CONFIG", { enumerable: true, get: function () { return appliance_calibration_1.DEFAULT_BATTERY_CALIBRATOR_CONFIG; } });
53
+ Object.defineProperty(exports, "CalibrationResultStore", { enumerable: true, get: function () { return appliance_calibration_1.CalibrationResultStore; } });
54
+ Object.defineProperty(exports, "CalibrationTrigger", { enumerable: true, get: function () { return appliance_calibration_1.CalibrationTrigger; } });
@@ -1,6 +1,11 @@
1
1
  export * from './sunspec-interfaces.cjs';
2
2
  export * from './sunspec-devices.cjs';
3
3
  export * from './sunspec-modbus-client.cjs';
4
- export * from './calibration-snapshot-service.cjs';
5
4
  export { ConnectionRetryManager } from './connection-retry-manager.cjs';
6
5
  export { SDK_VERSION, getSdkVersion } from './version.cjs';
6
+ export { SunspecCalibrationStorage, createSunspecCalibrationStorage } from './sunspec-calibration-storage.cjs';
7
+ export { SunspecBatteryCalibrationDriver } from './sunspec-battery-calibration-driver.cjs';
8
+ export { SunspecBatteryFeatureCalibrator, type SunspecBatteryFeatureCalibratorOptions, decodeFeatureResults, } from './sunspec-battery-feature-calibrator.cjs';
9
+ export { SunspecBatteryScheduleHandler, type SunspecScheduleRegisters, type SunspecBatteryScheduleHandlerOptions, } from './sunspec-battery-schedule-handler.cjs';
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';
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SunspecBatteryCalibrationDriver = void 0;
4
+ const appliance_calibration_1 = require("@enyo-energy/appliance-calibration");
5
+ const enyo_data_bus_value_js_1 = require("@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js");
6
+ const sunspec_interfaces_js_1 = require("./sunspec-interfaces.cjs");
7
+ /**
8
+ * Subset of {@link SunspecBatteryControls} that `writeBatteryControls` accepts.
9
+ * Used both as the calibrator's snapshot payload and as the restore whitelist.
10
+ */
11
+ const WRITABLE_BATTERY_FIELDS = [
12
+ "storCtlMod",
13
+ "chaGriSet",
14
+ "wChaMax",
15
+ "inWRte",
16
+ "outWRte",
17
+ "minRsvPct",
18
+ ];
19
+ /**
20
+ * Vendor seam between `@enyo-energy/appliance-calibration`'s `BatteryCalibrator`
21
+ * and a SunSpec battery exposed via {@link SunspecModbusClient}.
22
+ *
23
+ * Battery power is fed in by the owning {@link SunspecBattery} on every readData
24
+ * cycle (call {@link updateBatteryPowerCache}). Grid power is sourced from the
25
+ * data bus: the driver subscribes to `MeterValuesUpdateV1` for the lifetime of
26
+ * the calibrator and caches the latest reading. Call {@link stop} when the
27
+ * battery disconnects to unsubscribe.
28
+ */
29
+ class SunspecBatteryCalibrationDriver extends appliance_calibration_1.AbstractBatteryCalibrationDriver {
30
+ sunspecClient;
31
+ unitId;
32
+ dataBus;
33
+ batteryPowerCache = new appliance_calibration_1.LatestValueCache();
34
+ gridPowerCache = new appliance_calibration_1.LatestValueCache();
35
+ meterListenerId;
36
+ constructor(sunspecClient, unitId, dataBus) {
37
+ super();
38
+ this.sunspecClient = sunspecClient;
39
+ this.unitId = unitId;
40
+ this.dataBus = dataBus;
41
+ this.subscribeMeterUpdates();
42
+ }
43
+ subscribeMeterUpdates() {
44
+ this.meterListenerId = this.dataBus.listenForMessages([enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.MeterValuesUpdateV1], (entry) => {
45
+ if (entry.message !== enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.MeterValuesUpdateV1) {
46
+ return;
47
+ }
48
+ const meter = entry;
49
+ if (meter.data.gridPowerW !== undefined) {
50
+ this.gridPowerCache.set(meter.data.gridPowerW);
51
+ }
52
+ });
53
+ }
54
+ /**
55
+ * Tear down the meter subscription. Idempotent — call from
56
+ * `SunspecBattery.disconnect()`.
57
+ */
58
+ stop() {
59
+ if (this.meterListenerId) {
60
+ this.dataBus.unsubscribe(this.meterListenerId);
61
+ this.meterListenerId = undefined;
62
+ }
63
+ }
64
+ /**
65
+ * Push the latest computed battery power (W, positive = charging into the
66
+ * battery) from the owning device. Called from the readData loop.
67
+ */
68
+ updateBatteryPowerCache(powerW) {
69
+ this.batteryPowerCache.set(powerW);
70
+ }
71
+ async captureSnapshot() {
72
+ const controls = await this.sunspecClient.readBatteryControls(this.unitId);
73
+ if (!controls) {
74
+ throw new Error(`SunspecBatteryCalibrationDriver: readBatteryControls returned null for unit ${this.unitId}`);
75
+ }
76
+ return controls;
77
+ }
78
+ /**
79
+ * Write only the fields present in the snapshot AND in the writable whitelist.
80
+ * Per the library's contract this is best-effort: log on failure rather than
81
+ * throw, because the persisted snapshot has already been removed by the time
82
+ * we run and any throw would silently swallow what just happened.
83
+ */
84
+ async restoreFromSnapshot(snapshot, reason) {
85
+ const partial = {};
86
+ for (const field of WRITABLE_BATTERY_FIELDS) {
87
+ const value = snapshot[field];
88
+ if (value !== undefined) {
89
+ partial[field] = value;
90
+ }
91
+ }
92
+ if (Object.keys(partial).length === 0) {
93
+ console.log(`SunspecBatteryCalibrationDriver ${this.unitId}: restore (${reason}) — nothing to write`);
94
+ return;
95
+ }
96
+ try {
97
+ const ok = await this.sunspecClient.writeBatteryControls(this.unitId, partial);
98
+ if (!ok) {
99
+ console.error(`SunspecBatteryCalibrationDriver ${this.unitId}: restore (${reason}) writeBatteryControls returned false for [${Object.keys(partial).join(", ")}]`);
100
+ }
101
+ }
102
+ catch (error) {
103
+ console.error(`SunspecBatteryCalibrationDriver ${this.unitId}: restore (${reason}) threw: ${error}`);
104
+ }
105
+ }
106
+ /**
107
+ * SunSpec model 124 has no explicit calibration-mode register — the
108
+ * snapshot / restore + storCtlMod sequence *is* the calibration protocol.
109
+ * Resolve immediately so the orchestrator advances to the test charge.
110
+ */
111
+ async enterCalibrationMode() {
112
+ return "accepted";
113
+ }
114
+ async exitCalibrationMode() {
115
+ return "accepted";
116
+ }
117
+ /**
118
+ * Drive the same 3-step sequence proven by `handleStartGridCharge` in
119
+ * {@link SunspecBattery}: charge cap, grid-charging enable, storage mode.
120
+ * Throws on any step failure so the calibrator records a `failed` result.
121
+ */
122
+ async startTestCharge(powerW) {
123
+ const enabledGrid = await this.sunspecClient.enableGridCharging(this.unitId, true);
124
+ if (!enabledGrid) {
125
+ throw new Error("startTestCharge: failed to enable grid charging");
126
+ }
127
+ const wroteChaMax = await this.sunspecClient.writeBatteryControls(this.unitId, { wChaMax: powerW });
128
+ if (!wroteChaMax) {
129
+ throw new Error("startTestCharge: failed to set wChaMax");
130
+ }
131
+ const setMode = await this.sunspecClient.setStorageMode(this.unitId, sunspec_interfaces_js_1.SunspecStorageMode.CHARGE);
132
+ if (!setMode) {
133
+ throw new Error("startTestCharge: failed to set storage mode CHARGE");
134
+ }
135
+ }
136
+ /**
137
+ * Hand the battery back to its normal AUTO / PV state. The snapshot
138
+ * service's restore takes care of returning wChaMax / inWRte / outWRte
139
+ * to baseline.
140
+ */
141
+ async stopTestCharge() {
142
+ const setMode = await this.sunspecClient.setStorageMode(this.unitId, sunspec_interfaces_js_1.SunspecStorageMode.AUTO);
143
+ if (!setMode) {
144
+ throw new Error("stopTestCharge: failed to set storage mode AUTO");
145
+ }
146
+ const disabledGrid = await this.sunspecClient.enableGridCharging(this.unitId, false);
147
+ if (!disabledGrid) {
148
+ throw new Error("stopTestCharge: failed to disable grid charging");
149
+ }
150
+ }
151
+ readBatteryPowerW() {
152
+ return this.batteryPowerCache.get();
153
+ }
154
+ readGridPowerW() {
155
+ return this.gridPowerCache.get();
156
+ }
157
+ }
158
+ exports.SunspecBatteryCalibrationDriver = SunspecBatteryCalibrationDriver;
@@ -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.cjs";
4
+ import { type SunspecBatteryControls } from "./sunspec-interfaces.cjs";
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
+ }