@enyo-energy/energy-app-sdk 1.5.0 → 1.7.0

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 (56) hide show
  1. package/README.md +45 -0
  2. package/dist/cjs/energy-app-permission.type.cjs +1 -0
  3. package/dist/cjs/energy-app-permission.type.d.cts +2 -1
  4. package/dist/cjs/energy-app.cjs +16 -0
  5. package/dist/cjs/energy-app.d.cts +15 -0
  6. package/dist/cjs/enyo-energy-app-sdk.d.cts +3 -0
  7. package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.cjs +90 -0
  8. package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.d.cts +78 -1
  9. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.cjs +119 -0
  10. package/dist/cjs/index.cjs +4 -0
  11. package/dist/cjs/index.d.cts +4 -0
  12. package/dist/cjs/packages/energy-app-command-log.cjs +2 -0
  13. package/dist/cjs/packages/energy-app-command-log.d.cts +108 -0
  14. package/dist/cjs/packages/energy-app-interval.d.cts +1 -1
  15. package/dist/cjs/packages/energy-app-onboarding-v2.d.cts +54 -0
  16. package/dist/cjs/types/enyo-battery-appliance.d.cts +23 -0
  17. package/dist/cjs/types/enyo-command-log.cjs +50 -0
  18. package/dist/cjs/types/enyo-command-log.d.cts +151 -0
  19. package/dist/cjs/types/enyo-onboarding-v2-device-select.cjs +29 -0
  20. package/dist/cjs/types/enyo-onboarding-v2-device-select.d.cts +105 -0
  21. package/dist/cjs/types/enyo-onboarding-v2-validation.cjs +29 -0
  22. package/dist/cjs/types/enyo-onboarding-v2-validation.d.cts +108 -0
  23. package/dist/cjs/types/enyo-onboarding-v2.cjs +66 -1
  24. package/dist/cjs/types/enyo-onboarding-v2.d.cts +206 -8
  25. package/dist/cjs/types/enyo-onboarding.cjs +5 -3
  26. package/dist/cjs/types/enyo-onboarding.d.cts +5 -3
  27. package/dist/cjs/version.cjs +1 -1
  28. package/dist/cjs/version.d.cts +1 -1
  29. package/dist/energy-app-permission.type.d.ts +2 -1
  30. package/dist/energy-app-permission.type.js +1 -0
  31. package/dist/energy-app.d.ts +15 -0
  32. package/dist/energy-app.js +16 -0
  33. package/dist/enyo-energy-app-sdk.d.ts +3 -0
  34. package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.d.ts +78 -1
  35. package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.js +90 -0
  36. package/dist/implementations/onboarding-v2/onboarding-v2-validators.js +120 -1
  37. package/dist/index.d.ts +4 -0
  38. package/dist/index.js +4 -0
  39. package/dist/packages/energy-app-command-log.d.ts +108 -0
  40. package/dist/packages/energy-app-command-log.js +1 -0
  41. package/dist/packages/energy-app-interval.d.ts +1 -1
  42. package/dist/packages/energy-app-onboarding-v2.d.ts +54 -0
  43. package/dist/types/enyo-battery-appliance.d.ts +23 -0
  44. package/dist/types/enyo-command-log.d.ts +151 -0
  45. package/dist/types/enyo-command-log.js +47 -0
  46. package/dist/types/enyo-onboarding-v2-device-select.d.ts +105 -0
  47. package/dist/types/enyo-onboarding-v2-device-select.js +28 -0
  48. package/dist/types/enyo-onboarding-v2-validation.d.ts +108 -0
  49. package/dist/types/enyo-onboarding-v2-validation.js +28 -0
  50. package/dist/types/enyo-onboarding-v2.d.ts +206 -8
  51. package/dist/types/enyo-onboarding-v2.js +65 -0
  52. package/dist/types/enyo-onboarding.d.ts +5 -3
  53. package/dist/types/enyo-onboarding.js +5 -3
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/package.json +1 -1
package/README.md CHANGED
@@ -367,6 +367,7 @@ Energy Apps use a granular permissions system to control access to system resour
367
367
  - **`Savings`**: Publish and read back day-scoped savings reports
368
368
  - **`EpexSpotPrices`**: Read EPEX SPOT day-ahead wholesale market prices
369
369
  - **`GridFeeRegister`** / **`GridFeeUse`**: Publish / resolve dynamic grid fees (time-variable network charges)
370
+ - **`CommandLog`**: Record and read back the commands apps issued to appliances
370
371
 
371
372
  #### Site & Identity Permissions
372
373
 
@@ -1500,6 +1501,50 @@ Notes worth respecting when producing a report:
1500
1501
 
1501
1502
  Requires the `Savings` permission.
1502
1503
 
1504
+ #### `useCommandLog(): EnergyAppCommandLog`
1505
+
1506
+ Record which register, configuration key or message your app wrote to an appliance — and read those
1507
+ writes back. This is the answer to *"why did the device do that?"*: an appliance's state shows what it
1508
+ is doing now, not that you wrote holding register 40023 four seconds ago because the energy manager cut
1509
+ available power, nor that the write was rejected.
1510
+
1511
+ ```typescript
1512
+ const commandLog = energyApp.useCommandLog();
1513
+
1514
+ await commandLog.logCommand({
1515
+ applianceId,
1516
+ protocol: EnyoCommandProtocolEnum.Modbus,
1517
+ operation: 'write-single-register',
1518
+ target: '40023',
1519
+ previousValue: String(before),
1520
+ newValue: String(limitW),
1521
+ outcome: EnyoCommandOutcomeEnum.Success,
1522
+ reason: 'energy manager reduced available power',
1523
+ });
1524
+
1525
+ // Everything we failed to write to this appliance today.
1526
+ const failures = await commandLog.getCommands({
1527
+ applianceId,
1528
+ outcome: EnyoCommandOutcomeEnum.Failed,
1529
+ fromIso: startOfDay,
1530
+ });
1531
+ ```
1532
+
1533
+ - **Writes, not reads.** Polling a register every two seconds does not belong here; recording reads
1534
+ would bury the writes the log exists for.
1535
+ - **Log the attempt, not only the success.** `Failed`, `TimedOut` and `Rejected` entries are the ones
1536
+ someone goes looking for. `Rejected` is separate from `Failed` on purpose — a device that answered and
1537
+ refused is a different diagnosis from one that could not be reached.
1538
+ - **Fill `reason`.** *What* was written can be reconstructed from the device; *why* cannot.
1539
+ - **Never gate the command on the log.** Log after the write is attempted; a failure to log must not
1540
+ surface as a failure to control the appliance.
1541
+ - **Append-only.** No update, no delete — a log the app can rewrite cannot explain an incident.
1542
+ Retention is the host's.
1543
+ - **Reads span every app** unless you filter by `packageName`: two packages fighting over the same
1544
+ register are invisible from one app's own entries.
1545
+
1546
+ Requires the `CommandLog` permission.
1547
+
1503
1548
  ### Operational Utilities
1504
1549
 
1505
1550
  #### `useOnboardingV2(): EnergyAppOnboardingV2`
@@ -62,4 +62,5 @@ var EnergyAppPermissionTypeEnum;
62
62
  EnergyAppPermissionTypeEnum["FirmwareRegistry"] = "FirmwareRegistry";
63
63
  EnergyAppPermissionTypeEnum["GridFeeRegister"] = "GridFeeRegister";
64
64
  EnergyAppPermissionTypeEnum["GridFeeUse"] = "GridFeeUse";
65
+ EnergyAppPermissionTypeEnum["CommandLog"] = "CommandLog";
65
66
  })(EnergyAppPermissionTypeEnum || (exports.EnergyAppPermissionTypeEnum = EnergyAppPermissionTypeEnum = {}));
@@ -57,7 +57,8 @@ export declare enum EnergyAppPermissionTypeEnum {
57
57
  EpexSpotPrices = "EpexSpotPrices",
58
58
  FirmwareRegistry = "FirmwareRegistry",
59
59
  GridFeeRegister = "GridFeeRegister",
60
- GridFeeUse = "GridFeeUse"
60
+ GridFeeUse = "GridFeeUse",
61
+ CommandLog = "CommandLog"
61
62
  }
62
63
  /**
63
64
  * String union of every permission name, derived from
@@ -465,6 +465,22 @@ class EnergyApp {
465
465
  useGridFee() {
466
466
  return this.energyAppSdk.useGridFee();
467
467
  }
468
+ /**
469
+ * Gets the Command Log API for recording the writes this app issued to an
470
+ * appliance — which Modbus register, which OCPP configuration key or
471
+ * message, with what it was before and whether the device took it.
472
+ *
473
+ * An appliance's state shows what it is doing; the command log shows what
474
+ * was done to it and why, which is what an incident investigation actually
475
+ * needs. Entries are append-only, and reads span every app so that two
476
+ * packages fighting over the same register are visible.
477
+ * @returns The Command Log API instance
478
+ * @throws {EnergyAppPermissionNotGrantedError} If the `CommandLog`
479
+ * permission is not granted.
480
+ */
481
+ useCommandLog() {
482
+ return this.energyAppSdk.useCommandLog();
483
+ }
468
484
  /**
469
485
  * Gets the current SDK version.
470
486
  * @returns The semantic version string of the SDK
@@ -46,6 +46,7 @@ import { EnergyAppSavings } from "./packages/energy-app-savings.cjs";
46
46
  import { EnergyAppDeviceTest } from "./packages/energy-app-device-test.cjs";
47
47
  import { EnergyAppEpexSpotPrice } from "./packages/energy-app-epex-spot-price.cjs";
48
48
  import { EnergyAppGridFee } from "./packages/energy-app-grid-fee.cjs";
49
+ import { EnergyAppCommandLog } from "./packages/energy-app-command-log.cjs";
49
50
  import { UseFetchOptions } from "./types/enyo-fetch.cjs";
50
51
  /**
51
52
  * Concrete implementation of {@link EnyoEnergyAppSdk} that delegates every call
@@ -387,6 +388,20 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
387
388
  * `GridFeeUse` permission required by the called method is not granted.
388
389
  */
389
390
  useGridFee(): EnergyAppGridFee;
391
+ /**
392
+ * Gets the Command Log API for recording the writes this app issued to an
393
+ * appliance — which Modbus register, which OCPP configuration key or
394
+ * message, with what it was before and whether the device took it.
395
+ *
396
+ * An appliance's state shows what it is doing; the command log shows what
397
+ * was done to it and why, which is what an incident investigation actually
398
+ * needs. Entries are append-only, and reads span every app so that two
399
+ * packages fighting over the same register are visible.
400
+ * @returns The Command Log API instance
401
+ * @throws {EnergyAppPermissionNotGrantedError} If the `CommandLog`
402
+ * permission is not granted.
403
+ */
404
+ useCommandLog(): EnergyAppCommandLog;
390
405
  /**
391
406
  * Gets the current SDK version.
392
407
  * @returns The semantic version string of the SDK
@@ -45,6 +45,7 @@ import { EnergyAppSavings } from "./packages/energy-app-savings.cjs";
45
45
  import { EnergyAppDeviceTest } from "./packages/energy-app-device-test.cjs";
46
46
  import { EnergyAppEpexSpotPrice } from "./packages/energy-app-epex-spot-price.cjs";
47
47
  import { EnergyAppGridFee } from "./packages/energy-app-grid-fee.cjs";
48
+ import { EnergyAppCommandLog } from "./packages/energy-app-command-log.cjs";
48
49
  import { UseFetchOptions } from "./types/enyo-fetch.cjs";
49
50
  export declare enum EnergyAppStateEnum {
50
51
  Launching = "launching",
@@ -166,4 +167,6 @@ export interface EnyoEnergyAppSdk {
166
167
  useEpexSpotPrices: () => EnergyAppEpexSpotPrice;
167
168
  /** Get the Grid Fee API for publishing time-dependent network charges and resolving them to a 15-minute series */
168
169
  useGridFee: () => EnergyAppGridFee;
170
+ /** Get the Command Log API for recording which register, configuration key or message this app wrote to an appliance, and reading those commands back */
171
+ useCommandLog: () => EnergyAppCommandLog;
169
172
  }
@@ -67,6 +67,62 @@ exports.onboardingV2Block = {
67
67
  * @param items - One translated entry per bullet (de/en).
68
68
  */
69
69
  bullets: (id, items) => ({ id, type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Bullets, items }),
70
+ /**
71
+ * A select block — a dropdown whose answer is recorded, not branched on.
72
+ *
73
+ * Use this, not {@link block.choice}, when the answer is data rather than a
74
+ * route: a choice needs one outgoing transition per option, so forty models
75
+ * would mean forty transitions to the same step.
76
+ *
77
+ * @param id - Stable block id, unique within the guide.
78
+ * @param label - Translated field label (de/en).
79
+ * @param options - The selectable options; unique values, at least one.
80
+ * @param opts - Optional `defaultValue`, `help` and `required`.
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * block.select('model', t('Modell', 'Model'), [
85
+ * {value: 'sb-3-0', label: t('Sunny Boy 3.0', 'Sunny Boy 3.0')},
86
+ * {value: 'sb-5-0', label: t('Sunny Boy 5.0', 'Sunny Boy 5.0')},
87
+ * ], {required: true})
88
+ * ```
89
+ */
90
+ select: (id, label, options, opts) => ({
91
+ id,
92
+ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Select,
93
+ label,
94
+ options,
95
+ ...(opts?.defaultValue !== undefined ? { defaultValue: opts.defaultValue } : {}),
96
+ ...(opts?.help ? { help: opts.help } : {}),
97
+ ...(opts?.required !== undefined ? { required: opts.required } : {}),
98
+ }),
99
+ /**
100
+ * A credentials block — label/value pairs the installer transcribes into the
101
+ * device or a vendor portal.
102
+ *
103
+ * Passive content: it routes nothing, so a step whose only non-content block
104
+ * is this one still leaves through its single `continue` handle.
105
+ *
106
+ * @param id - Stable block id, unique within the guide.
107
+ * @param credentials - The label/value pairs. At least one.
108
+ * @param options - Optional heading, note, and copy-button toggle.
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * block.credentials('creds', [
113
+ * {label: t('Benutzername', 'Username'), value: 'enyo'},
114
+ * {label: t('Passwort', 'Password'), value: generated, secret: true},
115
+ * ], {title: t('Zugangsdaten', 'Credentials')})
116
+ * ```
117
+ */
118
+ credentials: (id, credentials, options) => ({
119
+ id,
120
+ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Credentials,
121
+ credentials,
122
+ copyable: options?.copyable ?? true,
123
+ ...(options?.title ? { title: options.title } : {}),
124
+ ...(options?.description ? { description: options.description } : {}),
125
+ }),
70
126
  /**
71
127
  * An image block addressing an externally hosted image by URL.
72
128
  *
@@ -217,6 +273,40 @@ exports.onboardingV2Block = {
217
273
  label,
218
274
  outcomes,
219
275
  }),
276
+ /**
277
+ * A device-select block: the installer picks the device being onboarded from
278
+ * everything the run has found.
279
+ *
280
+ * Use it whenever a scan can turn up more than one candidate. A
281
+ * {@link block.networkScan} branches on found/not-found but binds nothing,
282
+ * so without this the run does not know *which* device it is working on —
283
+ * and {@link EnyoOnboardingV2DeviceSelection.Current} and
284
+ * {@link EnyoOnboardingV2DynamicKind.DeviceIp} have nothing to resolve
285
+ * against.
286
+ *
287
+ * The picker renders what discovery found, so the guide must have scanned:
288
+ * keep {@link EnyoOnboardingV2Guide.requiresNetworkScan} at its default, or
289
+ * place a {@link block.networkScan} ahead of this one.
290
+ *
291
+ * Outcome `value`s must be {@link EnyoOnboardingV2DeviceSelectOutcome}
292
+ * members; route `not-found` to troubleshooting rather than to a step that
293
+ * assumes a device exists.
294
+ *
295
+ * Register an {@link EnyoOnboardingV2DeviceSelectHandler} to turn the pick
296
+ * into appliances — the host awaits it and binds the run to the ids it
297
+ * returns. Without one the pick binds an address and nothing more.
298
+ *
299
+ * @param id - Stable block id, unique within the guide.
300
+ * @param label - Translated trigger button text (de/en).
301
+ * @param outcomes - The `selected` / `not-found` results; each is a routing handle.
302
+ */
303
+ deviceSelect: (id, label, outcomes) => ({
304
+ id,
305
+ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Action,
306
+ action: enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.DeviceSelect,
307
+ label,
308
+ outcomes,
309
+ }),
220
310
  /**
221
311
  * An auth block: the installer signs into the energy app's own account
222
312
  * system (OAuth / vendor portal).
@@ -10,7 +10,7 @@
10
10
  * before publishing.
11
11
  */
12
12
  import type { EnyoOnboardingTranslatedContent } from '../../types/enyo-onboarding.cjs';
13
- import { EnyoOnboardingV2ActionKind, EnyoOnboardingV2ChoiceLayout, EnyoOnboardingV2DeviceSelection, EnyoOnboardingV2PauseReason } from '../../types/enyo-onboarding-v2.cjs';
13
+ import { EnyoOnboardingV2ActionKind, EnyoOnboardingV2Credential, EnyoOnboardingV2SelectOption, EnyoOnboardingV2ChoiceLayout, EnyoOnboardingV2DeviceSelection, EnyoOnboardingV2PauseReason } from '../../types/enyo-onboarding-v2.cjs';
14
14
  import type { EnyoOnboardingV2ActionOutcome, EnyoOnboardingV2AuthOutcome, EnyoOnboardingV2SetupField, EnyoOnboardingV2SetupOutcome, EnyoOnboardingV2SetupSkipHandle, EnyoOnboardingV2Block, EnyoOnboardingV2ChoiceOption, EnyoOnboardingV2DynamicKind, EnyoOnboardingV2Guide, EnyoOnboardingV2HintVariant, EnyoOnboardingV2InputOutcome, EnyoOnboardingV2InputValueType, EnyoOnboardingV2StartVariant, EnyoOnboardingV2Target, EnyoOnboardingV2Transition } from '../../types/enyo-onboarding-v2.cjs';
15
15
  /**
16
16
  * Identity helper that type-checks a v2 guide literal at definition time
@@ -45,6 +45,55 @@ export declare const onboardingV2Block: {
45
45
  * @param items - One translated entry per bullet (de/en).
46
46
  */
47
47
  bullets: (id: string, items: EnyoOnboardingTranslatedContent[][]) => EnyoOnboardingV2Block;
48
+ /**
49
+ * A select block — a dropdown whose answer is recorded, not branched on.
50
+ *
51
+ * Use this, not {@link block.choice}, when the answer is data rather than a
52
+ * route: a choice needs one outgoing transition per option, so forty models
53
+ * would mean forty transitions to the same step.
54
+ *
55
+ * @param id - Stable block id, unique within the guide.
56
+ * @param label - Translated field label (de/en).
57
+ * @param options - The selectable options; unique values, at least one.
58
+ * @param opts - Optional `defaultValue`, `help` and `required`.
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * block.select('model', t('Modell', 'Model'), [
63
+ * {value: 'sb-3-0', label: t('Sunny Boy 3.0', 'Sunny Boy 3.0')},
64
+ * {value: 'sb-5-0', label: t('Sunny Boy 5.0', 'Sunny Boy 5.0')},
65
+ * ], {required: true})
66
+ * ```
67
+ */
68
+ select: (id: string, label: EnyoOnboardingTranslatedContent[], options: EnyoOnboardingV2SelectOption[], opts?: {
69
+ defaultValue?: string;
70
+ help?: EnyoOnboardingTranslatedContent[];
71
+ required?: boolean;
72
+ }) => EnyoOnboardingV2Block;
73
+ /**
74
+ * A credentials block — label/value pairs the installer transcribes into the
75
+ * device or a vendor portal.
76
+ *
77
+ * Passive content: it routes nothing, so a step whose only non-content block
78
+ * is this one still leaves through its single `continue` handle.
79
+ *
80
+ * @param id - Stable block id, unique within the guide.
81
+ * @param credentials - The label/value pairs. At least one.
82
+ * @param options - Optional heading, note, and copy-button toggle.
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * block.credentials('creds', [
87
+ * {label: t('Benutzername', 'Username'), value: 'enyo'},
88
+ * {label: t('Passwort', 'Password'), value: generated, secret: true},
89
+ * ], {title: t('Zugangsdaten', 'Credentials')})
90
+ * ```
91
+ */
92
+ credentials: (id: string, credentials: EnyoOnboardingV2Credential[], options?: {
93
+ title?: EnyoOnboardingTranslatedContent[];
94
+ description?: EnyoOnboardingTranslatedContent[];
95
+ copyable?: boolean;
96
+ }) => EnyoOnboardingV2Block;
48
97
  /**
49
98
  * An image block addressing an externally hosted image by URL.
50
99
  *
@@ -169,6 +218,34 @@ export declare const onboardingV2Block: {
169
218
  * @param outcomes - The `paired` / `not-found` / `failure` results; each is a routing handle.
170
219
  */
171
220
  eebusPair: (id: string, label: EnyoOnboardingTranslatedContent[], outcomes: EnyoOnboardingV2ActionOutcome[]) => EnyoOnboardingV2Block;
221
+ /**
222
+ * A device-select block: the installer picks the device being onboarded from
223
+ * everything the run has found.
224
+ *
225
+ * Use it whenever a scan can turn up more than one candidate. A
226
+ * {@link block.networkScan} branches on found/not-found but binds nothing,
227
+ * so without this the run does not know *which* device it is working on —
228
+ * and {@link EnyoOnboardingV2DeviceSelection.Current} and
229
+ * {@link EnyoOnboardingV2DynamicKind.DeviceIp} have nothing to resolve
230
+ * against.
231
+ *
232
+ * The picker renders what discovery found, so the guide must have scanned:
233
+ * keep {@link EnyoOnboardingV2Guide.requiresNetworkScan} at its default, or
234
+ * place a {@link block.networkScan} ahead of this one.
235
+ *
236
+ * Outcome `value`s must be {@link EnyoOnboardingV2DeviceSelectOutcome}
237
+ * members; route `not-found` to troubleshooting rather than to a step that
238
+ * assumes a device exists.
239
+ *
240
+ * Register an {@link EnyoOnboardingV2DeviceSelectHandler} to turn the pick
241
+ * into appliances — the host awaits it and binds the run to the ids it
242
+ * returns. Without one the pick binds an address and nothing more.
243
+ *
244
+ * @param id - Stable block id, unique within the guide.
245
+ * @param label - Translated trigger button text (de/en).
246
+ * @param outcomes - The `selected` / `not-found` results; each is a routing handle.
247
+ */
248
+ deviceSelect: (id: string, label: EnyoOnboardingTranslatedContent[], outcomes: EnyoOnboardingV2ActionOutcome[]) => EnyoOnboardingV2Block;
172
249
  /**
173
250
  * An auth block: the installer signs into the energy app's own account
174
251
  * system (OAuth / vendor portal).
@@ -83,6 +83,8 @@ function validateOnboardingGuideV2(guide, context) {
83
83
  warnings.push(`${at}: no content blocks.`);
84
84
  validateActionBlocks(step, at, errors, warnings);
85
85
  validateLinkBlocks(step, at, errors, warnings);
86
+ validateCredentialsBlocks(step, at, errors, warnings);
87
+ validateSelectBlocks(step, at, errors, warnings);
86
88
  validateImageBlocks(step, at, errors, warnings, context);
87
89
  validateInputBlocks(step, at, errors, warnings);
88
90
  validateAuthBlocks(step, at, errors, warnings);
@@ -191,6 +193,9 @@ function validateActionBlocks(step, at, errors, warnings) {
191
193
  if (block.action === enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.OcppConnect) {
192
194
  validateOcppConnectOutcomes(block, at, errors);
193
195
  }
196
+ if (block.action === enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.DeviceSelect) {
197
+ validateDeviceSelectOutcomes(block, at, errors, warnings);
198
+ }
194
199
  if (block.action === enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.EebusPair) {
195
200
  validateEebusPairOutcomes(block, at, errors, warnings);
196
201
  }
@@ -247,6 +252,76 @@ function validateLinkBlocks(step, at, errors, warnings) {
247
252
  }
248
253
  }
249
254
  }
255
+ /**
256
+ * Validates the select blocks of a step.
257
+ *
258
+ * A select records an answer rather than routing on one, so the failures that
259
+ * matter are the ones that make the recorded value ambiguous or unreachable:
260
+ * duplicate option values (the app cannot tell which was picked) and a
261
+ * `defaultValue` naming no option (nothing is pre-selected, silently).
262
+ */
263
+ function validateSelectBlocks(step, at, errors, warnings) {
264
+ for (const block of step.blocks ?? []) {
265
+ if (block.type !== enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Select)
266
+ continue;
267
+ if (!block.label?.length) {
268
+ warnings.push(`${at}: select block "${block.id}" has no label.`);
269
+ }
270
+ if (!block.options?.length) {
271
+ errors.push(`${at}: select block "${block.id}" has no options.`);
272
+ continue;
273
+ }
274
+ if (block.options.length === 1) {
275
+ warnings.push(`${at}: select block "${block.id}" offers a single option — ` +
276
+ `there is nothing for the installer to choose.`);
277
+ }
278
+ const seen = new Set();
279
+ block.options.forEach((option, index) => {
280
+ if (!option.value?.trim()) {
281
+ errors.push(`${at}: select block "${block.id}" option ${index} has no value.`);
282
+ return;
283
+ }
284
+ if (seen.has(option.value)) {
285
+ errors.push(`${at}: select block "${block.id}" uses the option value ` +
286
+ `"${option.value}" more than once.`);
287
+ }
288
+ seen.add(option.value);
289
+ if (!option.label?.length) {
290
+ warnings.push(`${at}: select block "${block.id}" option "${option.value}" has no label.`);
291
+ }
292
+ });
293
+ if (block.defaultValue !== undefined && !seen.has(block.defaultValue)) {
294
+ errors.push(`${at}: select block "${block.id}" defaultValue "${block.defaultValue}" ` +
295
+ `matches no option.`);
296
+ }
297
+ }
298
+ }
299
+ /**
300
+ * Validates the credentials blocks of a step.
301
+ *
302
+ * A pair with no value is the failure that matters: it renders as an empty row,
303
+ * and the installer has nothing to type into the device. An untranslated label
304
+ * only degrades the display, so it is a warning.
305
+ */
306
+ function validateCredentialsBlocks(step, at, errors, warnings) {
307
+ for (const block of step.blocks ?? []) {
308
+ if (block.type !== enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Credentials)
309
+ continue;
310
+ if (!block.credentials?.length) {
311
+ errors.push(`${at}: credentials block "${block.id}" has no credentials.`);
312
+ continue;
313
+ }
314
+ block.credentials.forEach((credential, index) => {
315
+ if (!credential.value?.trim()) {
316
+ errors.push(`${at}: credentials block "${block.id}" entry ${index} has no value — ` +
317
+ `it would render as an empty row.`);
318
+ }
319
+ if (!credential.label?.length) {
320
+ warnings.push(`${at}: credentials block "${block.id}" entry ${index} has no label.`);
321
+ }
322
+ });
323
+ }
324
+ }
250
325
  /** An image-block `file` reference — the slug shape a package file is named with. */
251
326
  const FILE_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
252
327
  /**
@@ -366,6 +441,10 @@ function validateInputBlocks(step, at, errors, warnings) {
366
441
  if (!block.submitLabel?.length) {
367
442
  errors.push(`${at}: input block "${block.id}" has no submitLabel.`);
368
443
  }
444
+ if (block.validated && block.valueType === enyo_onboarding_v2_js_1.EnyoOnboardingV2InputValueType.IpAddress) {
445
+ errors.push(`${at}: input block "${block.id}" sets validated on an ip-address input — ` +
446
+ `the device test already produces the app's own verdict.`);
447
+ }
369
448
  const outcomes = block.outcomes ?? [];
370
449
  if (outcomes.length < 2) {
371
450
  errors.push(`${at}: input block "${block.id}" needs at least 2 outcomes.`);
@@ -432,6 +511,46 @@ function validateOcppConnectOutcomes(block, at, errors) {
432
511
  }
433
512
  }
434
513
  }
514
+ /** Every {@link EnyoOnboardingV2DeviceSelectOutcome} value. */
515
+ const DEVICE_SELECT_OUTCOMES = new Set(Object.values(enyo_onboarding_v2_js_1.EnyoOnboardingV2DeviceSelectOutcome));
516
+ /**
517
+ * Validates the outcomes of an {@link EnyoOnboardingV2ActionKind.DeviceSelect}
518
+ * block.
519
+ *
520
+ * The block reports one of two things — the installer picked a device, or the
521
+ * run came away without one — so its outcome `value`s are closed over
522
+ * {@link EnyoOnboardingV2DeviceSelectOutcome}. Anything else is an outcome that
523
+ * can never fire.
524
+ *
525
+ * A missing `selected` branch strands every successful pick, and a missing
526
+ * `not-found` branch strands the installer whose device is not in the list —
527
+ * both warnings rather than errors, so a guide can be staged step by step.
528
+ *
529
+ * @param block - The device-select action block being checked.
530
+ * @param at - Human-readable location prefix for messages.
531
+ * @param errors - Collector for blocking problems.
532
+ * @param warnings - Collector for advisory problems.
533
+ */
534
+ function validateDeviceSelectOutcomes(block, at, errors, warnings) {
535
+ const values = new Set();
536
+ for (const outcome of block.outcomes ?? []) {
537
+ if (!DEVICE_SELECT_OUTCOMES.has(outcome.value)) {
538
+ errors.push(`${at}: device-select block "${block.id}" has outcome value "${outcome.value}", which is not an EnyoOnboardingV2DeviceSelectOutcome member.`);
539
+ }
540
+ else if (values.has(outcome.value)) {
541
+ errors.push(`${at}: device-select block "${block.id}" wires outcome value "${outcome.value}" more than once.`);
542
+ }
543
+ values.add(outcome.value);
544
+ }
545
+ if (!values.has(enyo_onboarding_v2_js_1.EnyoOnboardingV2DeviceSelectOutcome.Selected)) {
546
+ warnings.push(`${at}: device-select block "${block.id}" has no "${enyo_onboarding_v2_js_1.EnyoOnboardingV2DeviceSelectOutcome.Selected}" outcome — ` +
547
+ 'a picked device would have nowhere to go.');
548
+ }
549
+ if (!values.has(enyo_onboarding_v2_js_1.EnyoOnboardingV2DeviceSelectOutcome.NotFound)) {
550
+ warnings.push(`${at}: device-select block "${block.id}" has no "${enyo_onboarding_v2_js_1.EnyoOnboardingV2DeviceSelectOutcome.NotFound}" outcome — ` +
551
+ 'an installer whose device is not in the list would be stranded.');
552
+ }
553
+ }
435
554
  /** Every {@link EnyoOnboardingV2EebusPairOutcome} value. */
436
555
  const EEBUS_PAIR_OUTCOMES = new Set(Object.values(enyo_onboarding_v2_js_1.EnyoOnboardingV2EebusPairOutcome));
437
556
  /**
@@ -103,6 +103,8 @@ __exportStar(require("./packages/energy-app-onboarding-v2.cjs"), exports);
103
103
  __exportStar(require("./implementations/onboarding-v2/onboarding-v2-provider-validators.cjs"), exports);
104
104
  __exportStar(require("./types/enyo-onboarding-v2-dynamic.cjs"), exports);
105
105
  __exportStar(require("./types/enyo-onboarding-v2-additional-setup.cjs"), exports);
106
+ __exportStar(require("./types/enyo-onboarding-v2-validation.cjs"), exports);
107
+ __exportStar(require("./types/enyo-onboarding-v2-device-select.cjs"), exports);
106
108
  __exportStar(require("./implementations/onboarding-v2/onboarding-v2-dynamic-validators.cjs"), exports);
107
109
  __exportStar(require("./implementations/files/define-public-file.cjs"), exports);
108
110
  __exportStar(require("./implementations/files/public-file-validators.cjs"), exports);
@@ -147,6 +149,8 @@ __exportStar(require("./packages/energy-app-device-test.cjs"), exports);
147
149
  __exportStar(require("./implementations/device-test/device-test-validators.cjs"), exports);
148
150
  __exportStar(require("./types/enyo-savings.cjs"), exports);
149
151
  __exportStar(require("./packages/energy-app-savings.cjs"), exports);
152
+ __exportStar(require("./types/enyo-command-log.cjs"), exports);
153
+ __exportStar(require("./packages/energy-app-command-log.cjs"), exports);
150
154
  __exportStar(require("./types/enyo-epex-spot-price.cjs"), exports);
151
155
  __exportStar(require("./packages/energy-app-epex-spot-price.cjs"), exports);
152
156
  __exportStar(require("./types/enyo-automation.cjs"), exports);
@@ -87,6 +87,8 @@ export * from './packages/energy-app-onboarding-v2.cjs';
87
87
  export * from './implementations/onboarding-v2/onboarding-v2-provider-validators.cjs';
88
88
  export * from './types/enyo-onboarding-v2-dynamic.cjs';
89
89
  export * from './types/enyo-onboarding-v2-additional-setup.cjs';
90
+ export * from './types/enyo-onboarding-v2-validation.cjs';
91
+ export * from './types/enyo-onboarding-v2-device-select.cjs';
90
92
  export * from './implementations/onboarding-v2/onboarding-v2-dynamic-validators.cjs';
91
93
  export * from './implementations/files/define-public-file.cjs';
92
94
  export * from './implementations/files/public-file-validators.cjs';
@@ -131,6 +133,8 @@ export * from './packages/energy-app-device-test.cjs';
131
133
  export * from './implementations/device-test/device-test-validators.cjs';
132
134
  export * from './types/enyo-savings.cjs';
133
135
  export * from './packages/energy-app-savings.cjs';
136
+ export * from './types/enyo-command-log.cjs';
137
+ export * from './packages/energy-app-command-log.cjs';
134
138
  export * from './types/enyo-epex-spot-price.cjs';
135
139
  export * from './packages/energy-app-epex-spot-price.cjs';
136
140
  export * from './types/enyo-automation.cjs';
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,108 @@
1
+ import { EnyoCommandLogEntry, EnyoCommandLogEntryInput, EnyoCommandLogFilter } from "../types/enyo-command-log.cjs";
2
+ /**
3
+ * Interface for recording and reading back the commands an energy app issued to
4
+ * an appliance.
5
+ *
6
+ * This is the answer to "why did the device do that?". An appliance's own state
7
+ * shows what it is doing now; it does not show that this app wrote holding
8
+ * register 40023 four seconds ago because the energy manager cut the available
9
+ * power, nor that the write was rejected. Without a record of the writes, that
10
+ * has to be reconstructed from log files that may already have rotated away.
11
+ *
12
+ * The unit is a **write, not a read**. Polling a register every two seconds does
13
+ * not belong here — an entry is a command that changed, or tried to change,
14
+ * something on the device. Recording reads would bury the writes.
15
+ *
16
+ * Entries are append-only: there is no update and no delete. A log an app can
17
+ * rewrite cannot be trusted to explain an incident, and retention is the host's
18
+ * to enforce.
19
+ *
20
+ * Access to this API requires the `CommandLog` permission
21
+ * ({@link EnergyAppPermissionType}); {@link EnergyApp.useCommandLog} throws when
22
+ * it has not been granted.
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const commandLog = energyApp.useCommandLog();
27
+ *
28
+ * const before = await modbus.readHoldingRegisters(40023, 1);
29
+ * try {
30
+ * await modbus.writeSingleRegister(40023, limitW);
31
+ * await commandLog.logCommand({
32
+ * applianceId,
33
+ * protocol: EnyoCommandProtocolEnum.Modbus,
34
+ * operation: 'write-single-register',
35
+ * target: '40023',
36
+ * previousValue: String(before.readUInt16BE(0)),
37
+ * newValue: String(limitW),
38
+ * outcome: EnyoCommandOutcomeEnum.Success,
39
+ * reason: 'energy manager reduced available power',
40
+ * });
41
+ * } catch (error) {
42
+ * await commandLog.logCommand({
43
+ * applianceId,
44
+ * protocol: EnyoCommandProtocolEnum.Modbus,
45
+ * operation: 'write-single-register',
46
+ * target: '40023',
47
+ * newValue: String(limitW),
48
+ * outcome: EnyoCommandOutcomeEnum.Failed,
49
+ * errorMessage: String(error),
50
+ * reason: 'energy manager reduced available power',
51
+ * });
52
+ * }
53
+ * ```
54
+ */
55
+ export interface EnergyAppCommandLog {
56
+ /**
57
+ * Appends one command to the log.
58
+ *
59
+ * Log the attempt, not only the success: a command that timed out or was
60
+ * rejected is the entry someone will actually go looking for, and an app
61
+ * that only records its successes produces a log where nothing ever went
62
+ * wrong. Set {@link EnyoCommandLogEntry.outcome} accordingly and put the
63
+ * detail in `errorMessage`.
64
+ *
65
+ * Fill {@link EnyoCommandLogEntry.reason} whenever the app knows it. What was
66
+ * written can be reconstructed from the device; why it was written cannot.
67
+ *
68
+ * This call is for auditing and must never gate the command itself — log
69
+ * after the write has been attempted, and do not let a failure to log
70
+ * surface as a failure to control the appliance.
71
+ *
72
+ * Requires the `CommandLog` permission.
73
+ *
74
+ * @param entry - The command to record. `id`, `packageName` and (unless
75
+ * supplied) `timestampIso` are assigned by the host.
76
+ * @returns Promise resolving to the stored entry, including its assigned id
77
+ * @throws {EnergyAppPermissionNotGrantedError} If the `CommandLog` permission
78
+ * is not granted.
79
+ */
80
+ logCommand(entry: EnyoCommandLogEntryInput): Promise<EnyoCommandLogEntry>;
81
+ /**
82
+ * Reads recorded commands back, newest first.
83
+ *
84
+ * Returns entries from **every** app, not just this one, unless
85
+ * {@link EnyoCommandLogFilter.packageName} narrows it. That is deliberate:
86
+ * the failure this log exists to explain is often two apps writing the same
87
+ * register, which is invisible from either app's own entries.
88
+ *
89
+ * Requires the `CommandLog` permission.
90
+ *
91
+ * @param filter - Optional narrowing by appliance, protocol, outcome, time
92
+ * range or package. Fields combine with AND.
93
+ * @returns Promise resolving to the matching entries, newest first
94
+ * @throws {EnergyAppPermissionNotGrantedError} If the `CommandLog` permission
95
+ * is not granted.
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * // Everything we failed to write to this appliance today.
100
+ * const failures = await commandLog.getCommands({
101
+ * applianceId,
102
+ * outcome: EnyoCommandOutcomeEnum.Failed,
103
+ * fromIso: startOfDay,
104
+ * });
105
+ * ```
106
+ */
107
+ getCommands(filter?: EnyoCommandLogFilter): Promise<EnyoCommandLogEntry[]>;
108
+ }
@@ -2,7 +2,7 @@
2
2
  * Supported interval durations for scheduled tasks.
3
3
  * Provides predefined time intervals from 1 second to 1 hour.
4
4
  */
5
- export type IntervalDuration = '1s' | '5s' | '10s' | '30s' | '1m' | '5m' | '1hr';
5
+ export type IntervalDuration = '1s' | '2s' | '3s' | '5s' | '10s' | '30s' | '1m' | '5m' | '1hr';
6
6
  /**
7
7
  * Interface for managing scheduled intervals in enyo packages.
8
8
  * Provides functionality to create and manage recurring tasks.