@enyo-energy/energy-app-sdk 0.0.139 → 0.0.140

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 (65) hide show
  1. package/dist/cjs/implementations/storage/storage-schedule-handler.cjs +348 -0
  2. package/dist/cjs/implementations/storage/storage-schedule-handler.d.cts +300 -0
  3. package/dist/cjs/index.cjs +1 -0
  4. package/dist/cjs/index.d.cts +1 -0
  5. package/dist/cjs/integrations/storage-integration-energy-app.cjs +4 -12
  6. package/dist/cjs/integrations/storage-integration-energy-app.d.cts +16 -34
  7. package/dist/cjs/packages/eebus/eebus-cevc-client.cjs +2 -0
  8. package/dist/cjs/packages/eebus/eebus-cevc-client.d.cts +91 -0
  9. package/dist/cjs/packages/eebus/eebus-evcc-client.cjs +2 -0
  10. package/dist/cjs/packages/eebus/eebus-evcc-client.d.cts +84 -0
  11. package/dist/cjs/packages/eebus/eebus-evcem-client.cjs +2 -0
  12. package/dist/cjs/packages/eebus/eebus-evcem-client.d.cts +53 -0
  13. package/dist/cjs/packages/eebus/eebus-evsecc-client.cjs +2 -0
  14. package/dist/cjs/packages/eebus/eebus-evsecc-client.d.cts +70 -0
  15. package/dist/cjs/packages/eebus/eebus-evsoc-client.cjs +2 -0
  16. package/dist/cjs/packages/eebus/eebus-evsoc-client.d.cts +52 -0
  17. package/dist/cjs/packages/eebus/eebus-opev-client.cjs +2 -0
  18. package/dist/cjs/packages/eebus/eebus-opev-client.d.cts +78 -0
  19. package/dist/cjs/packages/eebus/eebus-oscev-client.cjs +2 -0
  20. package/dist/cjs/packages/eebus/eebus-oscev-client.d.cts +66 -0
  21. package/dist/cjs/packages/eebus/eebus-use-case-registry.d.cts +124 -0
  22. package/dist/cjs/packages/eebus/eebus-vabd-client.cjs +2 -0
  23. package/dist/cjs/packages/eebus/eebus-vabd-client.d.cts +70 -0
  24. package/dist/cjs/packages/eebus/eebus-vapd-client.cjs +2 -0
  25. package/dist/cjs/packages/eebus/eebus-vapd-client.d.cts +60 -0
  26. package/dist/cjs/packages/eebus/energy-app-eebus.d.cts +9 -0
  27. package/dist/cjs/types/enyo-data-bus-value.cjs +39 -1
  28. package/dist/cjs/types/enyo-data-bus-value.d.cts +149 -0
  29. package/dist/cjs/types/enyo-eebus-use-cases.cjs +22 -15
  30. package/dist/cjs/types/enyo-eebus-use-cases.d.cts +237 -0
  31. package/dist/cjs/version.cjs +1 -1
  32. package/dist/cjs/version.d.cts +1 -1
  33. package/dist/implementations/storage/storage-schedule-handler.d.ts +300 -0
  34. package/dist/implementations/storage/storage-schedule-handler.js +344 -0
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +1 -0
  37. package/dist/integrations/storage-integration-energy-app.d.ts +16 -34
  38. package/dist/integrations/storage-integration-energy-app.js +4 -12
  39. package/dist/packages/eebus/eebus-cevc-client.d.ts +91 -0
  40. package/dist/packages/eebus/eebus-cevc-client.js +1 -0
  41. package/dist/packages/eebus/eebus-evcc-client.d.ts +84 -0
  42. package/dist/packages/eebus/eebus-evcc-client.js +1 -0
  43. package/dist/packages/eebus/eebus-evcem-client.d.ts +53 -0
  44. package/dist/packages/eebus/eebus-evcem-client.js +1 -0
  45. package/dist/packages/eebus/eebus-evsecc-client.d.ts +70 -0
  46. package/dist/packages/eebus/eebus-evsecc-client.js +1 -0
  47. package/dist/packages/eebus/eebus-evsoc-client.d.ts +52 -0
  48. package/dist/packages/eebus/eebus-evsoc-client.js +1 -0
  49. package/dist/packages/eebus/eebus-opev-client.d.ts +78 -0
  50. package/dist/packages/eebus/eebus-opev-client.js +1 -0
  51. package/dist/packages/eebus/eebus-oscev-client.d.ts +66 -0
  52. package/dist/packages/eebus/eebus-oscev-client.js +1 -0
  53. package/dist/packages/eebus/eebus-use-case-registry.d.ts +124 -0
  54. package/dist/packages/eebus/eebus-vabd-client.d.ts +70 -0
  55. package/dist/packages/eebus/eebus-vabd-client.js +1 -0
  56. package/dist/packages/eebus/eebus-vapd-client.d.ts +60 -0
  57. package/dist/packages/eebus/eebus-vapd-client.js +1 -0
  58. package/dist/packages/eebus/energy-app-eebus.d.ts +9 -0
  59. package/dist/types/enyo-data-bus-value.d.ts +149 -0
  60. package/dist/types/enyo-data-bus-value.js +38 -0
  61. package/dist/types/enyo-eebus-use-cases.d.ts +237 -0
  62. package/dist/types/enyo-eebus-use-cases.js +20 -15
  63. package/dist/version.d.ts +1 -1
  64. package/dist/version.js +1 -1
  65. package/package.json +1 -1
@@ -0,0 +1,348 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StorageScheduleHandler = void 0;
4
+ const enyo_data_bus_value_js_1 = require("../../types/enyo-data-bus-value.cjs");
5
+ /**
6
+ * Abstract base class that drives a battery / storage appliance
7
+ * through an {@link EnyoDataBusSetStorageScheduleV1}-style relative
8
+ * schedule on a 1-second tick.
9
+ *
10
+ * Subclasses **must** implement three lifecycle hooks; the handler
11
+ * cannot operate without them, so they are declared `abstract` rather
12
+ * than optional callbacks on the options bag:
13
+ *
14
+ * - {@link onInit} — an `async` setup hook called once per schedule
15
+ * install. Returns a `TRegisters` payload (a snapshot of the
16
+ * pre-schedule device state, the initial register values, anything
17
+ * the subclass needs to restore later) or throws to abort the
18
+ * install. The returned registers are persisted to
19
+ * {@link StorageScheduleHandlerOptions.storage} so a process that
20
+ * dies mid-schedule still gets a rollback next time it starts.
21
+ * - {@link onChange} — fires for every active-entry transition,
22
+ * including the first one (the entry at index 0, with
23
+ * `previous === undefined`). This is where subclasses apply the
24
+ * setpoint to the underlying battery driver.
25
+ * - {@link onRollback} — fires when a previously-running schedule is
26
+ * released (mode switched to auto, replaced, invalid input from the
27
+ * running state, handler disposed, or restart recovery). Receives
28
+ * the `TRegisters` originally returned by `onInit` so the subclass
29
+ * can restore the pre-schedule state.
30
+ *
31
+ * Subclasses **may** override {@link onError} to observe invalid
32
+ * input; the default implementation is a no-op.
33
+ *
34
+ * Wall-clock time is the source of truth for entry advancement; the
35
+ * `'1s'` interval is the sampling rate. Tests pump time
36
+ * deterministically via the injected `now` seam without
37
+ * `vi.useFakeTimers`.
38
+ *
39
+ * All public mutating methods serialise through an internal operation
40
+ * chain, so a rapid sequence of `applySchedule` calls observes them
41
+ * in order without races even though `onInit` and storage I/O are
42
+ * async.
43
+ *
44
+ * @typeParam TRegisters - The shape the subclass uses to remember
45
+ * pre-schedule state. Must be JSON-serialisable
46
+ * since it is round-tripped through
47
+ * {@link EnergyAppStorage}.
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * interface MyRegisters { previousMode: string; previousLimitW: number; }
52
+ *
53
+ * class MyStorage extends StorageScheduleHandler<MyRegisters> {
54
+ * protected async onInit(): Promise<MyRegisters> {
55
+ * return {
56
+ * previousMode: await this.driver.readMode(),
57
+ * previousLimitW: await this.driver.readLimit(),
58
+ * };
59
+ * }
60
+ * protected onChange(active: ActiveStorageScheduleEntry): void {
61
+ * this.driver.applySetpoint(active.entry.direction, active.entry.powerW);
62
+ * }
63
+ * protected onRollback(registers: MyRegisters): void {
64
+ * this.driver.applyMode(registers.previousMode);
65
+ * this.driver.applyLimit(registers.previousLimitW);
66
+ * }
67
+ * }
68
+ * ```
69
+ */
70
+ class StorageScheduleHandler {
71
+ options;
72
+ now;
73
+ storageKey;
74
+ listenerId;
75
+ state = { kind: 'idle' };
76
+ disposed = false;
77
+ operationChain = Promise.resolve();
78
+ /**
79
+ * @param options - {@link StorageScheduleHandlerOptions}. Required:
80
+ * `dataBus`, `interval`, `storage`. Optional:
81
+ * `applianceId`, `now`.
82
+ */
83
+ constructor(options) {
84
+ this.options = options;
85
+ this.now = options.now ?? (() => Date.now());
86
+ this.storageKey = `storage-schedule-handler:${options.applianceId ?? 'null'}`;
87
+ this.listenerId = options.dataBus.listenForMessages([enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.SetStorageScheduleV1],
88
+ // Returns the in-flight promise so a runtime / test that
89
+ // wants to await message processing can; the SDK contract
90
+ // is fire-and-forget so this is purely additive (TypeScript
91
+ // allows returning a Promise from a void-returning callback).
92
+ (msg) => this.applyMessage(msg).catch((err) => {
93
+ this.onError(err instanceof Error ? err : new Error(String(err)));
94
+ }));
95
+ this.enqueue(() => this.tryRecoverFromStorage());
96
+ }
97
+ /**
98
+ * Invoked when {@link applySchedule} receives invalid input
99
+ * (empty schedule while `mode === 'schedule'`, unsorted /
100
+ * duplicate / negative `seconds`, non-finite `powerW`, missing
101
+ * direction) **or** when {@link onInit} throws. The default
102
+ * implementation is a no-op; subclasses may override to log or
103
+ * surface the error.
104
+ *
105
+ * After `onError`, the handler always lands in a defined state
106
+ * (idle), regardless of whether `onError` is observed.
107
+ *
108
+ * @param _err - Description of what went wrong.
109
+ */
110
+ onError(_err) {
111
+ // Default: no-op. Subclasses may override to surface errors.
112
+ }
113
+ /**
114
+ * Apply a schedule directly, bypassing the data bus. Useful when
115
+ * the schedule comes from a UI control, an optimizer, or a replay
116
+ * of stored plans. Equivalent to receiving an
117
+ * {@link EnyoDataBusSetStorageScheduleV1} whose `data` matches
118
+ * `input`.
119
+ *
120
+ * Disposed handlers silently ignore the call. Concurrent calls
121
+ * are serialised internally — the returned promise resolves once
122
+ * the call's effects (including any preceding rollback and any
123
+ * `onInit` await) are complete.
124
+ *
125
+ * @param input - {@link StorageScheduleInput} carrying mode and
126
+ * optional schedule.
127
+ */
128
+ applySchedule(input) {
129
+ return this.enqueue(() => this.doApplySchedule(input));
130
+ }
131
+ /**
132
+ * Apply an incoming {@link EnyoDataBusSetStorageScheduleV1} message.
133
+ * Messages whose `applianceId` does not match the handler's bound
134
+ * appliance are silently ignored — the data-bus subscription
135
+ * registered in the constructor routes through this method.
136
+ *
137
+ * @param msg - The data-bus message.
138
+ */
139
+ applyMessage(msg) {
140
+ if (this.options.applianceId !== undefined && msg.applianceId !== this.options.applianceId) {
141
+ return Promise.resolve();
142
+ }
143
+ return this.applySchedule({
144
+ mode: msg.data.mode,
145
+ relativeSchedule: msg.data.relativeSchedule,
146
+ });
147
+ }
148
+ /**
149
+ * Snapshot of the currently-active entry, or `undefined` when the
150
+ * handler is idle (no schedule installed, or
151
+ * {@link EnyoStorageScheduleModeEnum.Auto} has been applied).
152
+ */
153
+ getActiveEntry() {
154
+ if (this.state.kind !== 'running') {
155
+ return undefined;
156
+ }
157
+ return this.makeActive(this.state, this.state.activeIndex);
158
+ }
159
+ /**
160
+ * Stop the interval, unsubscribe from the data bus, and — if a
161
+ * schedule was active — fire {@link onRollback} with the
162
+ * in-memory registers exactly once. The persisted copy is removed
163
+ * best-effort (fire-and-forget); restart recovery still works
164
+ * even if the removal does not land.
165
+ *
166
+ * Idempotent: subsequent calls are no-ops, and further
167
+ * `applySchedule` / `applyMessage` calls are silently ignored
168
+ * (they do not restart the interval).
169
+ */
170
+ dispose() {
171
+ if (this.disposed) {
172
+ return;
173
+ }
174
+ this.disposed = true;
175
+ this.options.dataBus.unsubscribe(this.listenerId);
176
+ if (this.state.kind === 'running') {
177
+ const state = this.state;
178
+ this.options.interval.stopInterval(state.intervalId);
179
+ const registers = state.registers;
180
+ this.state = { kind: 'idle' };
181
+ this.onRollback(registers);
182
+ // Best-effort persisted clear; rollback already fired in-process.
183
+ void this.options.storage.remove(this.storageKey).catch(() => { });
184
+ }
185
+ }
186
+ enqueue(op) {
187
+ const next = this.operationChain.then(() => op());
188
+ // The chain swallows errors so one failure does not freeze subsequent ops;
189
+ // the returned promise still propagates them to the caller.
190
+ this.operationChain = next.catch(() => { });
191
+ return next;
192
+ }
193
+ async doApplySchedule(input) {
194
+ if (this.disposed) {
195
+ return;
196
+ }
197
+ if (input.mode === enyo_data_bus_value_js_1.EnyoStorageScheduleModeEnum.Auto) {
198
+ await this.doReleaseSchedule();
199
+ return;
200
+ }
201
+ const validation = this.validate(input.relativeSchedule);
202
+ if (!validation.ok) {
203
+ this.onError(validation.error);
204
+ await this.doReleaseSchedule();
205
+ return;
206
+ }
207
+ await this.doInstallSchedule(validation.entries);
208
+ }
209
+ async doReleaseSchedule() {
210
+ if (this.state.kind !== 'running') {
211
+ return;
212
+ }
213
+ const state = this.state;
214
+ this.options.interval.stopInterval(state.intervalId);
215
+ const registers = state.registers;
216
+ this.state = { kind: 'idle' };
217
+ this.onRollback(registers);
218
+ await this.options.storage.remove(this.storageKey);
219
+ }
220
+ async doInstallSchedule(entries) {
221
+ if (this.state.kind === 'running') {
222
+ await this.doReleaseSchedule();
223
+ if (this.disposed) {
224
+ return;
225
+ }
226
+ }
227
+ const scheduleStartedAtMs = this.now();
228
+ const provisional = {
229
+ entry: entries[0],
230
+ indexInSchedule: 0,
231
+ totalEntries: entries.length,
232
+ scheduleStartedAtMs,
233
+ entryStartedAtMs: scheduleStartedAtMs,
234
+ };
235
+ let registers;
236
+ try {
237
+ registers = await this.onInit(provisional);
238
+ }
239
+ catch (err) {
240
+ if (this.disposed) {
241
+ return;
242
+ }
243
+ this.onError(err instanceof Error ? err : new Error(String(err)));
244
+ return;
245
+ }
246
+ if (this.disposed) {
247
+ // We received registers but the handler was disposed during the await.
248
+ // Roll back immediately so the appliance is not left in a half-set state.
249
+ this.onRollback(registers);
250
+ return;
251
+ }
252
+ const payload = { registers };
253
+ await this.options.storage.save(this.storageKey, payload);
254
+ if (this.disposed) {
255
+ this.onRollback(registers);
256
+ void this.options.storage.remove(this.storageKey).catch(() => { });
257
+ return;
258
+ }
259
+ const intervalId = this.options.interval.createInterval('1s', () => this.onTick());
260
+ const running = {
261
+ kind: 'running',
262
+ schedule: entries,
263
+ scheduleStartedAtMs,
264
+ activeIndex: 0,
265
+ entryStartedAtMs: scheduleStartedAtMs,
266
+ intervalId,
267
+ registers,
268
+ };
269
+ this.state = running;
270
+ this.onChange(this.makeActive(running, 0), undefined);
271
+ }
272
+ async tryRecoverFromStorage() {
273
+ if (this.disposed || this.state.kind !== 'idle') {
274
+ return;
275
+ }
276
+ const stored = await this.options.storage.load(this.storageKey);
277
+ if (!stored || this.disposed || this.state.kind !== 'idle') {
278
+ return;
279
+ }
280
+ this.onRollback(stored.registers);
281
+ await this.options.storage.remove(this.storageKey);
282
+ }
283
+ onTick() {
284
+ if (this.state.kind !== 'running') {
285
+ return;
286
+ }
287
+ const state = this.state;
288
+ const elapsedMs = this.now() - state.scheduleStartedAtMs;
289
+ const targetIndex = this.findTargetIndex(state.schedule, elapsedMs);
290
+ while (state.activeIndex < targetIndex) {
291
+ const previous = this.makeActive(state, state.activeIndex);
292
+ state.activeIndex += 1;
293
+ state.entryStartedAtMs = state.scheduleStartedAtMs
294
+ + state.schedule[state.activeIndex].seconds * 1000;
295
+ const active = this.makeActive(state, state.activeIndex);
296
+ this.onChange(active, previous);
297
+ }
298
+ }
299
+ findTargetIndex(entries, elapsedMs) {
300
+ let target = 0;
301
+ for (let i = 1; i < entries.length; i += 1) {
302
+ if (entries[i].seconds * 1000 <= elapsedMs) {
303
+ target = i;
304
+ }
305
+ else {
306
+ break;
307
+ }
308
+ }
309
+ return target;
310
+ }
311
+ makeActive(state, index) {
312
+ const entryStartedAtMs = index === state.activeIndex
313
+ ? state.entryStartedAtMs
314
+ : state.scheduleStartedAtMs + state.schedule[index].seconds * 1000;
315
+ return {
316
+ entry: state.schedule[index],
317
+ indexInSchedule: index,
318
+ totalEntries: state.schedule.length,
319
+ scheduleStartedAtMs: state.scheduleStartedAtMs,
320
+ entryStartedAtMs,
321
+ };
322
+ }
323
+ validate(entries) {
324
+ if (!entries || entries.length === 0) {
325
+ return { ok: false, error: new Error('relativeSchedule must contain at least one entry when mode is "schedule"') };
326
+ }
327
+ let previousSeconds = -Infinity;
328
+ for (let i = 0; i < entries.length; i += 1) {
329
+ const e = entries[i];
330
+ if (!Number.isFinite(e.seconds) || e.seconds < 0) {
331
+ return { ok: false, error: new Error(`relativeSchedule[${i}].seconds must be a non-negative finite number`) };
332
+ }
333
+ if (e.seconds <= previousSeconds) {
334
+ return { ok: false, error: new Error(`relativeSchedule must be strictly increasing by seconds (entry ${i} = ${e.seconds}, previous = ${previousSeconds})`) };
335
+ }
336
+ if (!Number.isFinite(e.powerW) || e.powerW < 0) {
337
+ return { ok: false, error: new Error(`relativeSchedule[${i}].powerW must be a non-negative finite number`) };
338
+ }
339
+ if (e.direction !== enyo_data_bus_value_js_1.EnyoStorageScheduleDirectionEnum.Charge
340
+ && e.direction !== enyo_data_bus_value_js_1.EnyoStorageScheduleDirectionEnum.Discharge) {
341
+ return { ok: false, error: new Error(`relativeSchedule[${i}].direction must be 'charge' or 'discharge'`) };
342
+ }
343
+ previousSeconds = e.seconds;
344
+ }
345
+ return { ok: true, entries };
346
+ }
347
+ }
348
+ exports.StorageScheduleHandler = StorageScheduleHandler;
@@ -0,0 +1,300 @@
1
+ import { EnergyAppDataBus } from '../../packages/energy-app-data-bus.cjs';
2
+ import { EnergyAppInterval } from '../../packages/energy-app-interval.cjs';
3
+ import { EnergyAppStorage } from '../../packages/energy-app-storage.cjs';
4
+ import { EnyoDataBusSetStorageScheduleV1, EnyoStorageScheduleEntry, EnyoStorageScheduleModeEnum } from '../../types/enyo-data-bus-value.cjs';
5
+ /**
6
+ * Snapshot of the currently-active entry within an installed storage
7
+ * schedule. Handed to {@link StorageScheduleHandler.onChange} every
8
+ * time the active entry changes — including the very first entry
9
+ * (which receives `previous === undefined`).
10
+ *
11
+ * The enriched timing fields let subclasses render UI ("entry 3 of 8,
12
+ * started 12 s ago") and compute remaining seconds against the next
13
+ * entry without re-deriving the timing context themselves.
14
+ */
15
+ export interface ActiveStorageScheduleEntry {
16
+ /** The raw schedule entry — direction, powerW, and the entry's own `seconds` offset. */
17
+ entry: EnyoStorageScheduleEntry;
18
+ /** Index of this entry within the installed schedule (0-based). */
19
+ indexInSchedule: number;
20
+ /** Total number of entries in the installed schedule. */
21
+ totalEntries: number;
22
+ /** Wall-clock milliseconds at which the carrying schedule was installed. */
23
+ scheduleStartedAtMs: number;
24
+ /** Wall-clock milliseconds at which this entry became the active one. */
25
+ entryStartedAtMs: number;
26
+ }
27
+ /**
28
+ * Unwrapped input accepted by
29
+ * {@link StorageScheduleHandler.applySchedule} — the same shape as
30
+ * {@link EnyoDataBusSetStorageScheduleV1.data} minus the optional
31
+ * `reason` field, so callers driving the handler from non-data-bus
32
+ * sources (a UI control, an optimizer output, a replay of stored plans)
33
+ * do not have to fabricate a reason.
34
+ */
35
+ export interface StorageScheduleInput {
36
+ /** Control mode: hand control back to the appliance, or follow a schedule. */
37
+ mode: EnyoStorageScheduleModeEnum;
38
+ /**
39
+ * Relative schedule the appliance should follow when {@link mode} is
40
+ * {@link EnyoStorageScheduleModeEnum.Schedule}. Sorted ascending by
41
+ * `seconds`; the first entry should be at `seconds = 0`. Omitted (or
42
+ * empty) when `mode` is {@link EnyoStorageScheduleModeEnum.Auto}.
43
+ */
44
+ relativeSchedule?: EnyoStorageScheduleEntry[];
45
+ }
46
+ /**
47
+ * Construction options for {@link StorageScheduleHandler}.
48
+ *
49
+ * The handler only takes runtime wiring — data bus, interval source,
50
+ * storage (for restart-safe persistence of the rollback registers),
51
+ * the appliance id it is bound to, and an optional `now` test seam.
52
+ * Lifecycle reactions (`onInit`, `onChange`, `onRollback`) are
53
+ * mandatory and live on the subclass as abstract methods.
54
+ */
55
+ export interface StorageScheduleHandlerOptions {
56
+ /**
57
+ * Data-bus service the handler subscribes to. On construction the
58
+ * handler registers a single listener for
59
+ * {@link EnyoDataBusMessageEnum.SetStorageScheduleV1} messages; the
60
+ * listener is removed in {@link StorageScheduleHandler.dispose}.
61
+ */
62
+ dataBus: EnergyAppDataBus;
63
+ /**
64
+ * Interval service driving the 1-second tick that advances the
65
+ * active entry. Inject a fake in tests; the production
66
+ * implementation uses `EnergyApp.useInterval()`.
67
+ */
68
+ interval: EnergyAppInterval;
69
+ /**
70
+ * Persistent key-value store the handler uses to make rollback
71
+ * restart-safe. The result of {@link StorageScheduleHandler.onInit}
72
+ * is written here when a schedule starts running and cleared after
73
+ * the matching {@link StorageScheduleHandler.onRollback}. On
74
+ * construction, the handler checks for a previously-persisted
75
+ * payload and invokes `onRollback` immediately if one is found —
76
+ * so a process that died mid-schedule still gets its rollback the
77
+ * next time around.
78
+ */
79
+ storage: EnergyAppStorage;
80
+ /**
81
+ * Optional appliance id this handler is bound to.
82
+ *
83
+ * When set, inbound `SetStorageScheduleV1` messages whose
84
+ * `applianceId` does not match are silently ignored — so multiple
85
+ * handlers can safely share one data bus.
86
+ *
87
+ * When omitted, the handler accepts every `SetStorageScheduleV1`
88
+ * message it sees. Useful for singletons that own the only
89
+ * battery / storage appliance on the device.
90
+ *
91
+ * The storage key under which the rollback registers are persisted
92
+ * incorporates the appliance id; an omitted id keys against the
93
+ * literal string `"null"`. This keeps per-appliance handlers and a
94
+ * single unbound handler from colliding in storage.
95
+ */
96
+ applianceId?: string;
97
+ /**
98
+ * Wall-clock source. Defaults to `() => Date.now()`. Override in
99
+ * tests to advance time deterministically without
100
+ * `vi.useFakeTimers`.
101
+ */
102
+ now?: () => number;
103
+ }
104
+ /**
105
+ * Abstract base class that drives a battery / storage appliance
106
+ * through an {@link EnyoDataBusSetStorageScheduleV1}-style relative
107
+ * schedule on a 1-second tick.
108
+ *
109
+ * Subclasses **must** implement three lifecycle hooks; the handler
110
+ * cannot operate without them, so they are declared `abstract` rather
111
+ * than optional callbacks on the options bag:
112
+ *
113
+ * - {@link onInit} — an `async` setup hook called once per schedule
114
+ * install. Returns a `TRegisters` payload (a snapshot of the
115
+ * pre-schedule device state, the initial register values, anything
116
+ * the subclass needs to restore later) or throws to abort the
117
+ * install. The returned registers are persisted to
118
+ * {@link StorageScheduleHandlerOptions.storage} so a process that
119
+ * dies mid-schedule still gets a rollback next time it starts.
120
+ * - {@link onChange} — fires for every active-entry transition,
121
+ * including the first one (the entry at index 0, with
122
+ * `previous === undefined`). This is where subclasses apply the
123
+ * setpoint to the underlying battery driver.
124
+ * - {@link onRollback} — fires when a previously-running schedule is
125
+ * released (mode switched to auto, replaced, invalid input from the
126
+ * running state, handler disposed, or restart recovery). Receives
127
+ * the `TRegisters` originally returned by `onInit` so the subclass
128
+ * can restore the pre-schedule state.
129
+ *
130
+ * Subclasses **may** override {@link onError} to observe invalid
131
+ * input; the default implementation is a no-op.
132
+ *
133
+ * Wall-clock time is the source of truth for entry advancement; the
134
+ * `'1s'` interval is the sampling rate. Tests pump time
135
+ * deterministically via the injected `now` seam without
136
+ * `vi.useFakeTimers`.
137
+ *
138
+ * All public mutating methods serialise through an internal operation
139
+ * chain, so a rapid sequence of `applySchedule` calls observes them
140
+ * in order without races even though `onInit` and storage I/O are
141
+ * async.
142
+ *
143
+ * @typeParam TRegisters - The shape the subclass uses to remember
144
+ * pre-schedule state. Must be JSON-serialisable
145
+ * since it is round-tripped through
146
+ * {@link EnergyAppStorage}.
147
+ *
148
+ * @example
149
+ * ```typescript
150
+ * interface MyRegisters { previousMode: string; previousLimitW: number; }
151
+ *
152
+ * class MyStorage extends StorageScheduleHandler<MyRegisters> {
153
+ * protected async onInit(): Promise<MyRegisters> {
154
+ * return {
155
+ * previousMode: await this.driver.readMode(),
156
+ * previousLimitW: await this.driver.readLimit(),
157
+ * };
158
+ * }
159
+ * protected onChange(active: ActiveStorageScheduleEntry): void {
160
+ * this.driver.applySetpoint(active.entry.direction, active.entry.powerW);
161
+ * }
162
+ * protected onRollback(registers: MyRegisters): void {
163
+ * this.driver.applyMode(registers.previousMode);
164
+ * this.driver.applyLimit(registers.previousLimitW);
165
+ * }
166
+ * }
167
+ * ```
168
+ */
169
+ export declare abstract class StorageScheduleHandler<TRegisters> {
170
+ private readonly options;
171
+ private readonly now;
172
+ private readonly storageKey;
173
+ private readonly listenerId;
174
+ private state;
175
+ private disposed;
176
+ private operationChain;
177
+ /**
178
+ * @param options - {@link StorageScheduleHandlerOptions}. Required:
179
+ * `dataBus`, `interval`, `storage`. Optional:
180
+ * `applianceId`, `now`.
181
+ */
182
+ protected constructor(options: StorageScheduleHandlerOptions);
183
+ /**
184
+ * Snapshot the pre-schedule state of the underlying appliance and
185
+ * return it as `TRegisters`. Called once per schedule install,
186
+ * before any {@link onChange} fires.
187
+ *
188
+ * The returned value is persisted to storage and handed back to
189
+ * {@link onRollback} when the schedule is released. Throwing from
190
+ * `onInit` aborts the install — no schedule is started, no
191
+ * registers are persisted, and {@link onError} is invoked with
192
+ * the thrown error.
193
+ *
194
+ * @param active - Snapshot of the entry that *will* become active
195
+ * if `onInit` succeeds. Subclasses do not need to
196
+ * apply this setpoint here — `onChange` fires
197
+ * immediately after a successful `onInit` with the
198
+ * same active entry.
199
+ * @returns Registers describing how to restore the pre-schedule
200
+ * state. Must be JSON-serialisable.
201
+ */
202
+ protected abstract onInit(active: ActiveStorageScheduleEntry): Promise<TRegisters>;
203
+ /**
204
+ * Apply a schedule setpoint to the underlying appliance. Fires:
205
+ *
206
+ * - Immediately after a successful {@link onInit} with
207
+ * `active = entries[0]` and `previous = undefined`.
208
+ * - On every subsequent active-entry transition with
209
+ * `previous` set to the entry that was active immediately before.
210
+ *
211
+ * Errors thrown from `onChange` are not caught by the handler —
212
+ * subclasses are responsible for their own error handling here.
213
+ *
214
+ * @param active - Snapshot of the now-active entry.
215
+ * @param previous - Snapshot of the previously-active entry, or
216
+ * `undefined` on the first call of a schedule.
217
+ */
218
+ protected abstract onChange(active: ActiveStorageScheduleEntry, previous: ActiveStorageScheduleEntry | undefined): void;
219
+ /**
220
+ * Restore the appliance to its pre-schedule state using the
221
+ * registers originally returned by {@link onInit}. Fires when a
222
+ * running schedule is released (mode switched to auto, replaced
223
+ * by another schedule, invalid input from the running state,
224
+ * handler disposed) **and** during restart recovery (a previous
225
+ * process saved registers but exited without rolling back).
226
+ *
227
+ * Errors thrown from `onRollback` are not caught by the handler.
228
+ *
229
+ * @param registers - The registers returned by the matching
230
+ * `onInit`, or — on restart recovery — the
231
+ * registers persisted by the previous process.
232
+ */
233
+ protected abstract onRollback(registers: TRegisters): void;
234
+ /**
235
+ * Invoked when {@link applySchedule} receives invalid input
236
+ * (empty schedule while `mode === 'schedule'`, unsorted /
237
+ * duplicate / negative `seconds`, non-finite `powerW`, missing
238
+ * direction) **or** when {@link onInit} throws. The default
239
+ * implementation is a no-op; subclasses may override to log or
240
+ * surface the error.
241
+ *
242
+ * After `onError`, the handler always lands in a defined state
243
+ * (idle), regardless of whether `onError` is observed.
244
+ *
245
+ * @param _err - Description of what went wrong.
246
+ */
247
+ protected onError(_err: Error): void;
248
+ /**
249
+ * Apply a schedule directly, bypassing the data bus. Useful when
250
+ * the schedule comes from a UI control, an optimizer, or a replay
251
+ * of stored plans. Equivalent to receiving an
252
+ * {@link EnyoDataBusSetStorageScheduleV1} whose `data` matches
253
+ * `input`.
254
+ *
255
+ * Disposed handlers silently ignore the call. Concurrent calls
256
+ * are serialised internally — the returned promise resolves once
257
+ * the call's effects (including any preceding rollback and any
258
+ * `onInit` await) are complete.
259
+ *
260
+ * @param input - {@link StorageScheduleInput} carrying mode and
261
+ * optional schedule.
262
+ */
263
+ applySchedule(input: StorageScheduleInput): Promise<void>;
264
+ /**
265
+ * Apply an incoming {@link EnyoDataBusSetStorageScheduleV1} message.
266
+ * Messages whose `applianceId` does not match the handler's bound
267
+ * appliance are silently ignored — the data-bus subscription
268
+ * registered in the constructor routes through this method.
269
+ *
270
+ * @param msg - The data-bus message.
271
+ */
272
+ applyMessage(msg: EnyoDataBusSetStorageScheduleV1): Promise<void>;
273
+ /**
274
+ * Snapshot of the currently-active entry, or `undefined` when the
275
+ * handler is idle (no schedule installed, or
276
+ * {@link EnyoStorageScheduleModeEnum.Auto} has been applied).
277
+ */
278
+ getActiveEntry(): ActiveStorageScheduleEntry | undefined;
279
+ /**
280
+ * Stop the interval, unsubscribe from the data bus, and — if a
281
+ * schedule was active — fire {@link onRollback} with the
282
+ * in-memory registers exactly once. The persisted copy is removed
283
+ * best-effort (fire-and-forget); restart recovery still works
284
+ * even if the removal does not land.
285
+ *
286
+ * Idempotent: subsequent calls are no-ops, and further
287
+ * `applySchedule` / `applyMessage` calls are silently ignored
288
+ * (they do not restart the interval).
289
+ */
290
+ dispose(): void;
291
+ private enqueue;
292
+ private doApplySchedule;
293
+ private doReleaseSchedule;
294
+ private doInstallSchedule;
295
+ private tryRecoverFromStorage;
296
+ private onTick;
297
+ private findTargetIndex;
298
+ private makeActive;
299
+ private validate;
300
+ }
@@ -32,6 +32,7 @@ __exportStar(require("./implementations/appliances/appliance-manager.cjs"), expo
32
32
  __exportStar(require("./implementations/appliances/identifier-strategies.cjs"), exports);
33
33
  __exportStar(require("./implementations/network-devices/network-access-guard.cjs"), exports);
34
34
  __exportStar(require("./implementations/network-devices/network-device-manager.cjs"), exports);
35
+ __exportStar(require("./implementations/storage/storage-schedule-handler.cjs"), exports);
35
36
  __exportStar(require("./enyo-package-channel.cjs"), exports);
36
37
  __exportStar(require("./types/enyo-timeseries.cjs"), exports);
37
38
  __exportStar(require("./types/enyo-energy-manager.cjs"), exports);
@@ -16,6 +16,7 @@ export * from './implementations/appliances/appliance-manager.cjs';
16
16
  export * from './implementations/appliances/identifier-strategies.cjs';
17
17
  export * from './implementations/network-devices/network-access-guard.cjs';
18
18
  export * from './implementations/network-devices/network-device-manager.cjs';
19
+ export * from './implementations/storage/storage-schedule-handler.cjs';
19
20
  export * from './enyo-package-channel.cjs';
20
21
  export * from './types/enyo-timeseries.cjs';
21
22
  export * from './types/enyo-energy-manager.cjs';