@enyo-energy/energy-app-sdk 0.0.191 → 0.0.193

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 (30) hide show
  1. package/README.md +20 -5
  2. package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.cjs +74 -2
  3. package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.d.cts +60 -4
  4. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.cjs +142 -12
  5. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.d.cts +10 -4
  6. package/dist/cjs/index.cjs +2 -0
  7. package/dist/cjs/index.d.cts +2 -0
  8. package/dist/cjs/packages/energy-app-charging-card.d.cts +47 -2
  9. package/dist/cjs/types/enyo-charging-card.d.cts +21 -0
  10. package/dist/cjs/types/enyo-data-bus-value.cjs +9 -0
  11. package/dist/cjs/types/enyo-data-bus-value.d.cts +19 -0
  12. package/dist/cjs/types/enyo-onboarding-v2.cjs +54 -7
  13. package/dist/cjs/types/enyo-onboarding-v2.d.cts +128 -12
  14. package/dist/cjs/version.cjs +1 -1
  15. package/dist/cjs/version.d.cts +1 -1
  16. package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.d.ts +60 -4
  17. package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.js +75 -3
  18. package/dist/implementations/onboarding-v2/onboarding-v2-validators.d.ts +10 -4
  19. package/dist/implementations/onboarding-v2/onboarding-v2-validators.js +143 -13
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.js +2 -0
  22. package/dist/packages/energy-app-charging-card.d.ts +47 -2
  23. package/dist/types/enyo-charging-card.d.ts +21 -0
  24. package/dist/types/enyo-data-bus-value.d.ts +19 -0
  25. package/dist/types/enyo-data-bus-value.js +9 -0
  26. package/dist/types/enyo-onboarding-v2.d.ts +128 -12
  27. package/dist/types/enyo-onboarding-v2.js +53 -6
  28. package/dist/version.d.ts +1 -1
  29. package/dist/version.js +1 -1
  30. package/package.json +1 -1
package/README.md CHANGED
@@ -757,18 +757,33 @@ the update callback cheap.
757
757
 
758
758
  #### `useChargingCard(): EnergyAppChargingCard`
759
759
 
760
- Handle charging authentication:
760
+ Read registered charging cards and pair new RFID cards:
761
761
 
762
762
  ```typescript
763
763
  const chargingCards = energyApp.useChargingCard();
764
764
 
765
- // Validate charging card
766
- const isValid = await chargingCards.validateCard('RFID-12345');
765
+ // List all registered charging cards
766
+ const cards = await chargingCards.list();
767
767
 
768
- // Get card information
769
- const cardInfo = await chargingCards.getCardInfo('RFID-12345');
768
+ // Get a single card
769
+ const card = await chargingCards.getById('card-id');
770
+
771
+ // Handle pairing requests coming from the app
772
+ const listenerId = chargingCards.listenForPairingStarted(async (request) => {
773
+ // Put the reader into pairing mode and wait for a card to be presented.
774
+ // Resolve with the RFID read from the card - the host assigns it to
775
+ // request.chargingCardId and clears its pendingRegistration flag.
776
+ return await charger.enterPairingMode(request.applianceId, request.timeoutMs);
777
+ });
778
+
779
+ chargingCards.removeListener(listenerId);
770
780
  ```
771
781
 
782
+ Reject the promise returned by the pairing listener when no card was presented or
783
+ the charger refused to enter pairing mode - the host then reports the attempt as
784
+ failed and leaves the card pending. A package managing several chargers should
785
+ check `request.applianceId` and reject requests for appliances it does not own.
786
+
772
787
  ### User Features
773
788
 
774
789
  #### `useAuthentication(): EnergyAppAuthentication`
@@ -133,6 +133,52 @@ exports.onboardingV2Block = {
133
133
  outcomes,
134
134
  deviceSelection,
135
135
  }),
136
+ /**
137
+ * An OCPP-connect action block: wait for the charger to dial into enyo's
138
+ * CSMS after the installer has entered the dynamic OCPP URL in it.
139
+ *
140
+ * A convenience wrapper over {@link onboardingV2Block.action} that pins the
141
+ * action kind. Nothing is searched — an OCPP wallbox is never on the LAN, so
142
+ * {@link EnyoOnboardingV2ActionKind.NetworkScan} is not a substitute. Pair it
143
+ * with an {@link onboardingV2Block.dynamic} `ocpp-url` block on the same or a
144
+ * preceding step.
145
+ *
146
+ * Outcome `value`s must be {@link EnyoOnboardingV2OcppConnectOutcome} members
147
+ * and both must be wired — the validator enforces that, since a charger that
148
+ * never calls home is the common case.
149
+ *
150
+ * @param id - Stable block id, unique within the guide.
151
+ * @param label - Translated trigger button text (de/en).
152
+ * @param outcomes - The `connected` / `timeout` results; each is a routing handle.
153
+ */
154
+ ocppConnect: (id, label, outcomes) => ({
155
+ id,
156
+ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Action,
157
+ action: enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.OcppConnect,
158
+ label,
159
+ outcomes,
160
+ }),
161
+ /**
162
+ * An auth block: the installer signs into the energy app's own account
163
+ * system (OAuth / vendor portal).
164
+ *
165
+ * Exactly one routing handle, and it means "the login succeeded". The server
166
+ * decides when it fires, so the installer cannot skip it; there is no failure
167
+ * branch to author — a failed attempt simply keeps them on the step. Route
168
+ * the handle with {@link onOutcomeV2}, passing `outcome.id`.
169
+ *
170
+ * @param id - Stable block id, unique within the guide.
171
+ * @param label - Translated sign-in button text (de/en).
172
+ * @param outcome - The single success handle (`{id, label}`).
173
+ * @param opts - Optional translated `help` naming the account that is needed.
174
+ */
175
+ auth: (id, label, outcome, opts) => ({
176
+ id,
177
+ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Auth,
178
+ label,
179
+ outcome,
180
+ help: opts?.help,
181
+ }),
136
182
  /**
137
183
  * A link block: a fixed URL the installer opens or copies.
138
184
  *
@@ -191,14 +237,40 @@ exports.onboardingV2Target = {
191
237
  step: (stepId) => ({ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Step, stepId }),
192
238
  /** Exit: onboarding succeeded (hand back to the app). */
193
239
  success: () => ({ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Success }),
194
- /** Exit: escalate to enyo support. */
195
- support: () => ({ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Support }),
240
+ /**
241
+ * Exit: escalate to enyo support.
242
+ * @param reason - Optional short internal key describing what failed, e.g.
243
+ * `firmware-too-old`. Never shown to the installer; it travels with the
244
+ * hand-off so support knows why it arrived.
245
+ */
246
+ support: (reason) => ({
247
+ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Support,
248
+ reason,
249
+ }),
196
250
  /**
197
251
  * Exit: pause the run (resumable) with a reason.
252
+ *
253
+ * For {@link EnyoOnboardingV2PauseReason.EnyoTodo} prefer
254
+ * {@link onboardingV2Target.enyoTakeover} — that reason is a terminal
255
+ * hand-off, not a park, and `resumeStepName` does not apply to it.
256
+ *
198
257
  * @param reason - Why the run is parked.
199
258
  * @param resumeStepName - Optional step `name` to resume at.
200
259
  */
201
260
  pause: (reason, resumeStepName) => ({ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Pause, reason, resumeStepName }),
261
+ /**
262
+ * Exit: **enyo übernimmt** — the installer is done and enyo finishes the
263
+ * setup. A terminal exit alongside `success` and `support`: the app shows the
264
+ * takeover screen rather than returning to the cockpit.
265
+ *
266
+ * Emits the unchanged wire shape (`pause` with reason `enyo-todo`), and
267
+ * counts as a completing exit for {@link validateOnboardingGuideV2} — a guide
268
+ * that only ends here needs no `success` branch.
269
+ */
270
+ enyoTakeover: () => ({
271
+ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Pause,
272
+ reason: enyo_onboarding_v2_js_1.EnyoOnboardingV2PauseReason.EnyoTodo,
273
+ }),
202
274
  /**
203
275
  * Jump into another start variant's flow for the same vendor/model.
204
276
  * @param variant - The start variant to hand off to.
@@ -10,8 +10,8 @@
10
10
  * before publishing.
11
11
  */
12
12
  import type { EnyoOnboardingTranslatedContent } from '../../types/enyo-onboarding.cjs';
13
- import { EnyoOnboardingV2ActionKind, EnyoOnboardingV2ChoiceLayout, EnyoOnboardingV2DeviceSelection } from '../../types/enyo-onboarding-v2.cjs';
14
- import type { EnyoOnboardingV2ActionOutcome, EnyoOnboardingV2Block, EnyoOnboardingV2ChoiceOption, EnyoOnboardingV2DynamicKind, EnyoOnboardingV2Guide, EnyoOnboardingV2HintVariant, EnyoOnboardingV2InputOutcome, EnyoOnboardingV2InputValueType, EnyoOnboardingV2PauseReason, EnyoOnboardingV2StartVariant, EnyoOnboardingV2Target, EnyoOnboardingV2Transition } from '../../types/enyo-onboarding-v2.cjs';
13
+ import { EnyoOnboardingV2ActionKind, EnyoOnboardingV2ChoiceLayout, EnyoOnboardingV2DeviceSelection, EnyoOnboardingV2PauseReason } from '../../types/enyo-onboarding-v2.cjs';
14
+ import type { EnyoOnboardingV2ActionOutcome, EnyoOnboardingV2AuthOutcome, 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
17
17
  * (mirrors `defineEnergyAppPackage`). Prefer this over a bare object literal so
@@ -98,6 +98,42 @@ export declare const onboardingV2Block: {
98
98
  * @param deviceSelection - Which devices to test (defaults to `Detected`).
99
99
  */
100
100
  deviceTest: (id: string, label: EnyoOnboardingTranslatedContent[], outcomes: EnyoOnboardingV2ActionOutcome[], deviceSelection?: EnyoOnboardingV2DeviceSelection) => EnyoOnboardingV2Block;
101
+ /**
102
+ * An OCPP-connect action block: wait for the charger to dial into enyo's
103
+ * CSMS after the installer has entered the dynamic OCPP URL in it.
104
+ *
105
+ * A convenience wrapper over {@link onboardingV2Block.action} that pins the
106
+ * action kind. Nothing is searched — an OCPP wallbox is never on the LAN, so
107
+ * {@link EnyoOnboardingV2ActionKind.NetworkScan} is not a substitute. Pair it
108
+ * with an {@link onboardingV2Block.dynamic} `ocpp-url` block on the same or a
109
+ * preceding step.
110
+ *
111
+ * Outcome `value`s must be {@link EnyoOnboardingV2OcppConnectOutcome} members
112
+ * and both must be wired — the validator enforces that, since a charger that
113
+ * never calls home is the common case.
114
+ *
115
+ * @param id - Stable block id, unique within the guide.
116
+ * @param label - Translated trigger button text (de/en).
117
+ * @param outcomes - The `connected` / `timeout` results; each is a routing handle.
118
+ */
119
+ ocppConnect: (id: string, label: EnyoOnboardingTranslatedContent[], outcomes: EnyoOnboardingV2ActionOutcome[]) => EnyoOnboardingV2Block;
120
+ /**
121
+ * An auth block: the installer signs into the energy app's own account
122
+ * system (OAuth / vendor portal).
123
+ *
124
+ * Exactly one routing handle, and it means "the login succeeded". The server
125
+ * decides when it fires, so the installer cannot skip it; there is no failure
126
+ * branch to author — a failed attempt simply keeps them on the step. Route
127
+ * the handle with {@link onOutcomeV2}, passing `outcome.id`.
128
+ *
129
+ * @param id - Stable block id, unique within the guide.
130
+ * @param label - Translated sign-in button text (de/en).
131
+ * @param outcome - The single success handle (`{id, label}`).
132
+ * @param opts - Optional translated `help` naming the account that is needed.
133
+ */
134
+ auth: (id: string, label: EnyoOnboardingTranslatedContent[], outcome: EnyoOnboardingV2AuthOutcome, opts?: {
135
+ help?: EnyoOnboardingTranslatedContent[];
136
+ }) => EnyoOnboardingV2Block;
101
137
  /**
102
138
  * A link block: a fixed URL the installer opens or copies.
103
139
  *
@@ -143,14 +179,34 @@ export declare const onboardingV2Target: {
143
179
  step: (stepId: string) => EnyoOnboardingV2Target;
144
180
  /** Exit: onboarding succeeded (hand back to the app). */
145
181
  success: () => EnyoOnboardingV2Target;
146
- /** Exit: escalate to enyo support. */
147
- support: () => EnyoOnboardingV2Target;
182
+ /**
183
+ * Exit: escalate to enyo support.
184
+ * @param reason - Optional short internal key describing what failed, e.g.
185
+ * `firmware-too-old`. Never shown to the installer; it travels with the
186
+ * hand-off so support knows why it arrived.
187
+ */
188
+ support: (reason?: string) => EnyoOnboardingV2Target;
148
189
  /**
149
190
  * Exit: pause the run (resumable) with a reason.
191
+ *
192
+ * For {@link EnyoOnboardingV2PauseReason.EnyoTodo} prefer
193
+ * {@link onboardingV2Target.enyoTakeover} — that reason is a terminal
194
+ * hand-off, not a park, and `resumeStepName` does not apply to it.
195
+ *
150
196
  * @param reason - Why the run is parked.
151
197
  * @param resumeStepName - Optional step `name` to resume at.
152
198
  */
153
199
  pause: (reason: EnyoOnboardingV2PauseReason, resumeStepName?: string) => EnyoOnboardingV2Target;
200
+ /**
201
+ * Exit: **enyo übernimmt** — the installer is done and enyo finishes the
202
+ * setup. A terminal exit alongside `success` and `support`: the app shows the
203
+ * takeover screen rather than returning to the cockpit.
204
+ *
205
+ * Emits the unchanged wire shape (`pause` with reason `enyo-todo`), and
206
+ * counts as a completing exit for {@link validateOnboardingGuideV2} — a guide
207
+ * that only ends here needs no `success` branch.
208
+ */
209
+ enyoTakeover: () => EnyoOnboardingV2Target;
154
210
  /**
155
211
  * Jump into another start variant's flow for the same vendor/model.
156
212
  * @param variant - The start variant to hand off to.
@@ -5,8 +5,14 @@
5
5
  * energy app can fail fast locally before publishing.
6
6
  *
7
7
  * `errors` block publishing; `warnings` are advisory (e.g. unreachable step, no
8
- * path to success). Use {@link validateOnboardingGuideV2} for the non-throwing
9
- * result, or {@link assertValidOnboardingGuideV2} to throw on the first failure.
8
+ * path to a completing exit). Use {@link validateOnboardingGuideV2} for the
9
+ * non-throwing result, or {@link assertValidOnboardingGuideV2} to throw on the
10
+ * first failure.
11
+ *
12
+ * A run completes either through a `success` target **or** through the
13
+ * "enyo übernimmt" hand-off (`pause` with reason
14
+ * {@link EnyoOnboardingV2PauseReason.EnyoTodo}) — both count, so a guide that
15
+ * legitimately ends in a hand-off is not nagged about a missing success path.
10
16
  */
11
17
  Object.defineProperty(exports, "__esModule", { value: true });
12
18
  exports.OnboardingV2ValidationError = void 0;
@@ -34,8 +40,8 @@ exports.OnboardingV2ValidationError = OnboardingV2ValidationError;
34
40
  const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
35
41
  /**
36
42
  * Structural + plausibility validation of a v2 guide graph. `errors` block
37
- * publishing; `warnings` are advisory (e.g. unreachable step, no path to
38
- * success).
43
+ * publishing; `warnings` are advisory (e.g. unreachable step, no path to a
44
+ * completing exit — `success` or the `enyo-todo` hand-off).
39
45
  *
40
46
  * @param guide - The v2 guide to validate.
41
47
  * @returns The {@link OnboardingV2ValidationResult}.
@@ -72,11 +78,12 @@ function validateOnboardingGuideV2(guide) {
72
78
  validateActionBlocks(step, at, errors, warnings);
73
79
  validateLinkBlocks(step, at, errors, warnings);
74
80
  validateInputBlocks(step, at, errors, warnings);
81
+ validateAuthBlocks(step, at, errors, warnings);
75
82
  }
76
83
  if (!guide.startStepId || !stepIds.has(guide.startStepId)) {
77
84
  errors.push('`startStepId` must reference an existing step.');
78
85
  }
79
- let hasSuccess = false;
86
+ let hasCompletingExit = false;
80
87
  for (const [i, step] of guide.steps.entries()) {
81
88
  const at = `steps[${i}] (${step.name})`;
82
89
  const required = requiredHandleKeys(step);
@@ -90,14 +97,24 @@ function validateOnboardingGuideV2(guide) {
90
97
  wired.add(key);
91
98
  const tg = t.target;
92
99
  if (tg.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Success)
93
- hasSuccess = true;
100
+ hasCompletingExit = true;
94
101
  else if (tg.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Step && !stepIds.has(tg.stepId)) {
95
102
  errors.push(`${at}: transition targets missing step "${tg.stepId}".`);
96
103
  }
97
- else if (tg.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Pause &&
98
- tg.resumeStepName &&
99
- !stepNames.has(tg.resumeStepName)) {
100
- errors.push(`${at}: pause resumeStepName "${tg.resumeStepName}" is unknown.`);
104
+ else if (tg.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Pause) {
105
+ // "enyo übernimmt" is a terminal hand-off, not a park: it completes
106
+ // the run for the installer, so it satisfies the completion check
107
+ // and has nothing to resume at.
108
+ if (tg.reason === enyo_onboarding_v2_js_1.EnyoOnboardingV2PauseReason.EnyoTodo) {
109
+ hasCompletingExit = true;
110
+ if (tg.resumeStepName) {
111
+ warnings.push(`${at}: pause reason "${enyo_onboarding_v2_js_1.EnyoOnboardingV2PauseReason.EnyoTodo}" is a hand-off to enyo, ` +
112
+ `not a resumable park — resumeStepName "${tg.resumeStepName}" is ignored.`);
113
+ }
114
+ }
115
+ else if (tg.resumeStepName && !stepNames.has(tg.resumeStepName)) {
116
+ errors.push(`${at}: pause resumeStepName "${tg.resumeStepName}" is unknown.`);
117
+ }
101
118
  }
102
119
  else if (tg.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.StartVariant &&
103
120
  tg.variant === guide.startVariant) {
@@ -117,8 +134,11 @@ function validateOnboardingGuideV2(guide) {
117
134
  warnings.push(`step "${step.name}" is unreachable from the start.`);
118
135
  }
119
136
  }
120
- if (!hasSuccess)
121
- warnings.push('No branch reaches a `success` exit.');
137
+ if (!hasCompletingExit) {
138
+ warnings.push('No branch reaches a completing exit — wire a `success` target, or a `pause` with reason ' +
139
+ `"${enyo_onboarding_v2_js_1.EnyoOnboardingV2PauseReason.EnyoTodo}" when enyo takes the setup over.`);
140
+ }
141
+ validateNetworkScanFlag(guide, warnings);
122
142
  return { ok: errors.length === 0, errors, warnings };
123
143
  }
124
144
  /**
@@ -155,6 +175,9 @@ function validateActionBlocks(step, at, errors, warnings) {
155
175
  if (block.deviceSelection) {
156
176
  warnings.push(`${at}: block "${block.id}" sets deviceSelection but is not a device-test action; it is ignored.`);
157
177
  }
178
+ if (block.action === enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.OcppConnect) {
179
+ validateOcppConnectOutcomes(block, at, errors);
180
+ }
158
181
  continue;
159
182
  }
160
183
  const values = new Set();
@@ -284,6 +307,110 @@ function validateInputBlocks(step, at, errors, warnings) {
284
307
  }
285
308
  }
286
309
  }
310
+ /** Every {@link EnyoOnboardingV2OcppConnectOutcome} value. */
311
+ const OCPP_CONNECT_OUTCOMES = new Set(Object.values(enyo_onboarding_v2_js_1.EnyoOnboardingV2OcppConnectOutcome));
312
+ /**
313
+ * Validates the outcomes of an {@link EnyoOnboardingV2ActionKind.OcppConnect}
314
+ * block.
315
+ *
316
+ * The block waits for the charger to dial into our CSMS; it can only ever report
317
+ * that the connection arrived or that it did not, so its outcome `value`s are
318
+ * closed over {@link EnyoOnboardingV2OcppConnectOutcome}. Both must be wired: a
319
+ * charger that never calls home — wrong URL typed, no mobile coverage in the
320
+ * garage — is the *common* case, and a guide without a `timeout` branch strands
321
+ * the installer on a spinner.
322
+ *
323
+ * @param block - The ocpp-connect action block being checked.
324
+ * @param at - Human-readable location prefix for messages.
325
+ * @param errors - Collector for blocking problems.
326
+ */
327
+ function validateOcppConnectOutcomes(block, at, errors) {
328
+ const values = new Set();
329
+ for (const outcome of block.outcomes ?? []) {
330
+ if (!OCPP_CONNECT_OUTCOMES.has(outcome.value)) {
331
+ errors.push(`${at}: ocpp-connect block "${block.id}" has outcome value "${outcome.value}", which is not an EnyoOnboardingV2OcppConnectOutcome member.`);
332
+ }
333
+ else if (values.has(outcome.value)) {
334
+ errors.push(`${at}: ocpp-connect block "${block.id}" wires outcome value "${outcome.value}" more than once.`);
335
+ }
336
+ values.add(outcome.value);
337
+ }
338
+ for (const required of OCPP_CONNECT_OUTCOMES) {
339
+ if (!values.has(required)) {
340
+ errors.push(`${at}: ocpp-connect block "${block.id}" has no "${required}" outcome; both results must be routed.`);
341
+ }
342
+ }
343
+ }
344
+ /**
345
+ * Validates the auth blocks of a step.
346
+ *
347
+ * An auth block has exactly one handle, and the **server** decides when it
348
+ * fires — there is no failure branch to author, because a failed login keeps the
349
+ * installer on the step to retry. So the checks are about the handle existing and
350
+ * being routable at all, and about the step not pretending the login is optional:
351
+ * a second decision block next to it would offer a way past a gate the client is
352
+ * not allowed to skip.
353
+ *
354
+ * @param step - The step whose blocks are checked.
355
+ * @param at - Human-readable location prefix for messages.
356
+ * @param errors - Collector for blocking problems.
357
+ * @param warnings - Collector for advisory problems.
358
+ */
359
+ function validateAuthBlocks(step, at, errors, warnings) {
360
+ const authBlocks = (step.blocks ?? []).filter((b) => b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Auth);
361
+ for (const block of authBlocks) {
362
+ if (!block.label?.length)
363
+ errors.push(`${at}: auth block "${block.id}" has no label.`);
364
+ if (!block.outcome?.id) {
365
+ errors.push(`${at}: auth block "${block.id}" has no outcome id — its success handle cannot be routed.`);
366
+ }
367
+ if (!block.outcome?.label?.length) {
368
+ warnings.push(`${at}: auth block "${block.id}" outcome has no label.`);
369
+ }
370
+ }
371
+ if (authBlocks.length > 1) {
372
+ errors.push(`${at}: more than one auth block; a step can hold at most one login.`);
373
+ }
374
+ if (authBlocks.length === 1) {
375
+ const others = (step.blocks ?? []).filter((b) => b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Choice ||
376
+ b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Action ||
377
+ b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Input);
378
+ if (others.length) {
379
+ warnings.push(`${at}: auth block "${authBlocks[0].id}" shares the step with ${others.length} other decision block(s) — ` +
380
+ 'those give the installer a way past a login the server gates.');
381
+ }
382
+ }
383
+ }
384
+ /**
385
+ * Checks {@link EnyoOnboardingV2Guide.requiresNetworkScan} against what the guide
386
+ * actually does.
387
+ *
388
+ * Opting out means "don't search, start here" — right for a device that is never
389
+ * on the LAN (an OCPP wallbox), wrong if the guide then relies on scan results.
390
+ * Both mismatches are warnings, not errors: the flag describes the host's
391
+ * behaviour before the guide runs, and an author may have a reason.
392
+ *
393
+ * @param guide - The guide being validated.
394
+ * @param warnings - Collector for advisory problems.
395
+ */
396
+ function validateNetworkScanFlag(guide, warnings) {
397
+ if (guide.requiresNetworkScan !== false)
398
+ return;
399
+ const blocks = guide.steps.flatMap((s) => s.blocks ?? []);
400
+ const scansItself = blocks.some((b) => b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Action &&
401
+ b.action === enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.NetworkScan);
402
+ if (scansItself)
403
+ return;
404
+ const dependsOnDetected = blocks.some((b) => b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Action &&
405
+ b.action === enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.DeviceTest &&
406
+ (b.deviceSelection ?? enyo_onboarding_v2_js_1.EnyoOnboardingV2DeviceSelection.Detected) !==
407
+ enyo_onboarding_v2_js_1.EnyoOnboardingV2DeviceSelection.Current);
408
+ if (dependsOnDetected) {
409
+ warnings.push('requiresNetworkScan is false, but a device-test block selects from detected devices — ' +
410
+ 'nothing was scanned, so it has nothing to test. Use deviceSelection "current", or run a ' +
411
+ 'network-scan action inside the guide.');
412
+ }
413
+ }
287
414
  /**
288
415
  * The set of routing-handle keys a step must wire exactly once: one per
289
416
  * choice option / action outcome / input outcome, or the single `continue`
@@ -303,6 +430,9 @@ function requiredHandleKeys(step) {
303
430
  for (const o of b.outcomes)
304
431
  keys.add(`outcome:${b.id}:${o.id}`);
305
432
  }
433
+ else if (b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Auth && b.outcome?.id) {
434
+ keys.add(`outcome:${b.id}:${b.outcome.id}`);
435
+ }
306
436
  }
307
437
  if (keys.size === 0)
308
438
  keys.add('continue');
@@ -4,8 +4,14 @@
4
4
  * energy app can fail fast locally before publishing.
5
5
  *
6
6
  * `errors` block publishing; `warnings` are advisory (e.g. unreachable step, no
7
- * path to success). Use {@link validateOnboardingGuideV2} for the non-throwing
8
- * result, or {@link assertValidOnboardingGuideV2} to throw on the first failure.
7
+ * path to a completing exit). Use {@link validateOnboardingGuideV2} for the
8
+ * non-throwing result, or {@link assertValidOnboardingGuideV2} to throw on the
9
+ * first failure.
10
+ *
11
+ * A run completes either through a `success` target **or** through the
12
+ * "enyo übernimmt" hand-off (`pause` with reason
13
+ * {@link EnyoOnboardingV2PauseReason.EnyoTodo}) — both count, so a guide that
14
+ * legitimately ends in a hand-off is not nagged about a missing success path.
9
15
  */
10
16
  import type { EnyoOnboardingV2Guide } from '../../types/enyo-onboarding-v2.cjs';
11
17
  /**
@@ -31,8 +37,8 @@ export interface OnboardingV2ValidationResult {
31
37
  }
32
38
  /**
33
39
  * Structural + plausibility validation of a v2 guide graph. `errors` block
34
- * publishing; `warnings` are advisory (e.g. unreachable step, no path to
35
- * success).
40
+ * publishing; `warnings` are advisory (e.g. unreachable step, no path to a
41
+ * completing exit — `success` or the `enyo-todo` hand-off).
36
42
  *
37
43
  * @param guide - The v2 guide to validate.
38
44
  * @returns The {@link OnboardingV2ValidationResult}.
@@ -83,6 +83,8 @@ __exportStar(require("./packages/energy-app-configuration-manager.cjs"), exports
83
83
  __exportStar(require("./types/enyo-air-conditioning-appliance.cjs"), exports);
84
84
  __exportStar(require("./types/enyo-heating-rod-appliance.cjs"), exports);
85
85
  __exportStar(require("./types/enyo-charger-appliance.cjs"), exports);
86
+ __exportStar(require("./types/enyo-charging-card.cjs"), exports);
87
+ __exportStar(require("./packages/energy-app-charging-card.cjs"), exports);
86
88
  __exportStar(require("./types/enyo-battery-appliance.cjs"), exports);
87
89
  __exportStar(require("./types/enyo-heatpump-appliance.cjs"), exports);
88
90
  __exportStar(require("./types/enyo-inverter-appliance.cjs"), exports);
@@ -67,6 +67,8 @@ export * from './packages/energy-app-configuration-manager.cjs';
67
67
  export * from './types/enyo-air-conditioning-appliance.cjs';
68
68
  export * from './types/enyo-heating-rod-appliance.cjs';
69
69
  export * from './types/enyo-charger-appliance.cjs';
70
+ export * from './types/enyo-charging-card.cjs';
71
+ export * from './packages/energy-app-charging-card.cjs';
70
72
  export * from './types/enyo-battery-appliance.cjs';
71
73
  export * from './types/enyo-heatpump-appliance.cjs';
72
74
  export * from './types/enyo-inverter-appliance.cjs';
@@ -1,11 +1,56 @@
1
- import { EnyoChargingCard } from "../types/enyo-charging-card.cjs";
1
+ import { EnyoChargingCard, EnyoChargingCardPairingRequest } from "../types/enyo-charging-card.cjs";
2
2
  /**
3
3
  * Interface for managing charging cards in enyo packages.
4
- * Provides read-only operations for charging card information.
4
+ * Provides read-only operations for charging card information as well as
5
+ * RFID pairing support.
5
6
  */
6
7
  export interface EnergyAppChargingCard {
7
8
  /** Get a list of all registered charging cards */
8
9
  list: () => Promise<EnyoChargingCard[]>;
9
10
  /** Get a specific charging card by its ID */
10
11
  getById: (id: string) => Promise<EnyoChargingCard | null>;
12
+ /**
13
+ * Listen for pairing requests, i.e. the host asking this package to put its
14
+ * RFID reader into pairing mode — typically because a user started adding a
15
+ * new charging card in the app.
16
+ *
17
+ * The listener owns the whole pairing attempt: it enables pairing mode on
18
+ * the charger, waits for a card to be held against the reader and resolves
19
+ * with the RFID identifier that was read (the value stored in
20
+ * {@link EnyoChargingCard.rfid}). The host assigns it to the charging card
21
+ * named by {@link EnyoChargingCardPairingRequest.chargingCardId} and clears
22
+ * that card's {@link EnyoChargingCard.pendingRegistration} flag.
23
+ *
24
+ * Reject the returned promise when no card was presented or the charger
25
+ * refused to enter pairing mode; the host then reports the attempt as
26
+ * failed and leaves the card pending. Honour
27
+ * {@link EnyoChargingCardPairingRequest.timeoutMs} when it is set — the host
28
+ * ignores a result that arrives after the deadline.
29
+ *
30
+ * Only register a listener when the package actually drives an RFID reader.
31
+ * Requests carry an optional
32
+ * {@link EnyoChargingCardPairingRequest.applianceId}; a package managing
33
+ * several chargers should check it and reject requests for appliances it
34
+ * does not own.
35
+ *
36
+ * @param listener - Callback invoked for every pairing request, resolving
37
+ * with the RFID identifier read from the presented card
38
+ * @returns A unique listener ID that can be used to remove the listener
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * const chargingCard = energyApp.useChargingCard();
43
+ * const listenerId = chargingCard.listenForPairingStarted(async (request) => {
44
+ * const rfid = await charger.enterPairingMode(request.applianceId, request.timeoutMs);
45
+ * return rfid;
46
+ * });
47
+ * ```
48
+ */
49
+ listenForPairingStarted: (listener: (request: EnyoChargingCardPairingRequest) => Promise<string>) => string;
50
+ /**
51
+ * Removes a previously registered listener.
52
+ *
53
+ * @param listenerId - The ID returned by {@link listenForPairingStarted}
54
+ */
55
+ removeListener: (listenerId: string) => void;
11
56
  }
@@ -9,3 +9,24 @@ export interface EnyoChargingCard {
9
9
  rfid?: string;
10
10
  pendingRegistration?: boolean;
11
11
  }
12
+ /**
13
+ * Details of a pairing request handed to a
14
+ * {@link EnergyAppChargingCard.listenForPairingStarted} listener when the
15
+ * host asks a package to put its RFID reader into pairing mode.
16
+ */
17
+ export interface EnyoChargingCardPairingRequest {
18
+ /** ID of the charging card record the scanned RFID should be assigned to */
19
+ chargingCardId: string;
20
+ /**
21
+ * Appliance (charger) whose RFID reader should enter pairing mode. Omitted
22
+ * when the host does not target a specific charger — in that case the
23
+ * package decides which of its readers to use.
24
+ */
25
+ applianceId?: string;
26
+ /**
27
+ * Time budget, in milliseconds, the package has to deliver a scanned card.
28
+ * The host stops waiting once it elapses, so a listener that resolves later
29
+ * has no effect. Omitted when the host does not impose a deadline.
30
+ */
31
+ timeoutMs?: number;
32
+ }
@@ -15,6 +15,15 @@ var EnyoDataBusCommandReasonTypeEnum;
15
15
  EnyoDataBusCommandReasonTypeEnum["PvSurplusAvailable"] = "pv-surplus-available";
16
16
  /** Command issued because PV surplus is unavailable */
17
17
  EnyoDataBusCommandReasonTypeEnum["PvSurplusUnavailable"] = "pv-surplus-unavailable";
18
+ /**
19
+ * Command issued because PV surplus exists but is allocated to another
20
+ * appliance. Belongs to the
21
+ * {@link EnyoDataBusCommandReasonCategoryEnum.PvSurplus} category. Set
22
+ * {@link EnyoDataBusCommandReason.inFavourOfApplianceType} so the
23
+ * end-user text can name the appliance that got the surplus instead of
24
+ * saying "elsewhere".
25
+ */
26
+ EnyoDataBusCommandReasonTypeEnum["PvSurplusAllocatedElsewhere"] = "pv-surplus-allocated-elsewhere";
18
27
  /** Command issued because battery capacity is available */
19
28
  EnyoDataBusCommandReasonTypeEnum["BatteryCapacityAvailable"] = "battery-capacity-available";
20
29
  /** Command issued because battery capacity is unavailable */
@@ -23,6 +23,15 @@ export declare enum EnyoDataBusCommandReasonTypeEnum {
23
23
  PvSurplusAvailable = "pv-surplus-available",
24
24
  /** Command issued because PV surplus is unavailable */
25
25
  PvSurplusUnavailable = "pv-surplus-unavailable",
26
+ /**
27
+ * Command issued because PV surplus exists but is allocated to another
28
+ * appliance. Belongs to the
29
+ * {@link EnyoDataBusCommandReasonCategoryEnum.PvSurplus} category. Set
30
+ * {@link EnyoDataBusCommandReason.inFavourOfApplianceType} so the
31
+ * end-user text can name the appliance that got the surplus instead of
32
+ * saying "elsewhere".
33
+ */
34
+ PvSurplusAllocatedElsewhere = "pv-surplus-allocated-elsewhere",
26
35
  /** Command issued because battery capacity is available */
27
36
  BatteryCapacityAvailable = "battery-capacity-available",
28
37
  /** Command issued because battery capacity is unavailable */
@@ -100,6 +109,16 @@ export interface EnyoDataBusCommandReason {
100
109
  temperatureC?: number;
101
110
  /** Relevant state of charge as a percentage (battery-driven reasons) */
102
111
  socPercent?: number;
112
+ /**
113
+ * The appliance category the decision was made in favour of.
114
+ *
115
+ * Set on reasons that describe a trade-off between appliances — most
116
+ * notably
117
+ * {@link EnyoDataBusCommandReasonTypeEnum.PvSurplusAllocatedElsewhere} —
118
+ * so the end-user text can name the winning appliance (e.g. "the battery")
119
+ * rather than saying "elsewhere".
120
+ */
121
+ inFavourOfApplianceType?: EnyoApplianceTypeEnum;
103
122
  }
104
123
  /**
105
124
  * Whether a grid operator power limitation caps power drawn from the grid