@enyo-energy/energy-app-sdk 1.12.0 → 1.13.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 (42) hide show
  1. package/README.md +126 -1
  2. package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.cjs +88 -46
  3. package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.d.cts +85 -39
  4. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-provider-validators.cjs +12 -50
  5. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-provider-validators.d.cts +10 -7
  6. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.cjs +164 -41
  7. package/dist/cjs/index.cjs +3 -0
  8. package/dist/cjs/index.d.cts +3 -0
  9. package/dist/cjs/packages/energy-app-onboarding-v2.d.cts +48 -7
  10. package/dist/cjs/types/enyo-data-bus-value.cjs +32 -3
  11. package/dist/cjs/types/enyo-data-bus-value.d.cts +236 -4
  12. package/dist/cjs/types/enyo-eebus.cjs +47 -1
  13. package/dist/cjs/types/enyo-eebus.d.cts +57 -0
  14. package/dist/cjs/types/enyo-onboarding-v2-device-select.cjs +2 -2
  15. package/dist/cjs/types/enyo-onboarding-v2-device-select.d.cts +31 -6
  16. package/dist/cjs/types/enyo-onboarding-v2-eebus-device-select.cjs +29 -0
  17. package/dist/cjs/types/enyo-onboarding-v2-eebus-device-select.d.cts +174 -0
  18. package/dist/cjs/types/enyo-onboarding-v2.cjs +36 -64
  19. package/dist/cjs/types/enyo-onboarding-v2.d.cts +278 -94
  20. package/dist/cjs/version.cjs +1 -1
  21. package/dist/cjs/version.d.cts +1 -1
  22. package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.d.ts +85 -39
  23. package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.js +88 -46
  24. package/dist/implementations/onboarding-v2/onboarding-v2-provider-validators.d.ts +10 -7
  25. package/dist/implementations/onboarding-v2/onboarding-v2-provider-validators.js +12 -50
  26. package/dist/implementations/onboarding-v2/onboarding-v2-validators.js +164 -41
  27. package/dist/index.d.ts +3 -0
  28. package/dist/index.js +3 -0
  29. package/dist/packages/energy-app-onboarding-v2.d.ts +48 -7
  30. package/dist/types/enyo-data-bus-value.d.ts +236 -4
  31. package/dist/types/enyo-data-bus-value.js +32 -3
  32. package/dist/types/enyo-eebus.d.ts +57 -0
  33. package/dist/types/enyo-eebus.js +46 -0
  34. package/dist/types/enyo-onboarding-v2-device-select.d.ts +31 -6
  35. package/dist/types/enyo-onboarding-v2-device-select.js +2 -2
  36. package/dist/types/enyo-onboarding-v2-eebus-device-select.d.ts +174 -0
  37. package/dist/types/enyo-onboarding-v2-eebus-device-select.js +28 -0
  38. package/dist/types/enyo-onboarding-v2.d.ts +278 -94
  39. package/dist/types/enyo-onboarding-v2.js +36 -64
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/package.json +1 -1
package/README.md CHANGED
@@ -85,6 +85,7 @@ The official TypeScript SDK for building Energy Apps on the enyo platform. Creat
85
85
  - [Device Integration](#device-integration)
86
86
  - [Data Bus Messaging](#data-bus-messaging)
87
87
  - [Announcing Appliance Flexibility](#announcing-appliance-flexibility)
88
+ - [Explaining Why a Command Was Issued](#explaining-why-a-command-was-issued)
88
89
  - [Settings Management](#settings-management)
89
90
  - [Troubleshooting](#troubleshooting)
90
91
  - [External Libraries](#external-libraries)
@@ -1680,9 +1681,27 @@ await energyApp.useOnboardingV2().registerAdditionalSetupHandler(async (request)
1680
1681
 
1681
1682
  Every such block must declare a `failed` outcome — it absorbs a rejection, a timeout, a missing handler, and an outcome value matching no branch (guide and handler are linked only by strings). Secrets are never logged, never prefilled, and not kept in run state. See [ONBOARDING.md](./ONBOARDING.md#app-defined-setups-additional-setup).
1682
1683
 
1684
+ Guides pick the device on a screen of their own. `device-select` lists the network devices the run found; `eebus-device-select` lists the discovered EEBUS peers, filtered by the SHIP device type they announce (`EnyoEebusDeviceTypeEnum`). Both carry their own `headline`/`description`, and both **skip the screen entirely** when exactly one candidate matches the filter — the handler still runs and the run is still bound, with `autoSelected: true` on the request. Register one handler per kind; a network device and an EEBUS peer are different things and neither handler is a fallback for the other:
1685
+
1686
+ ```typescript
1687
+ const onboarding = energyApp.useOnboardingV2();
1688
+
1689
+ await onboarding.registerDeviceSelectHandler(async (request) => ({
1690
+ requestId: request.requestId,
1691
+ applianceIds: await adoptDevices(request.devices, request.applianceId),
1692
+ }));
1693
+
1694
+ await onboarding.registerEebusDeviceSelectHandler(async (request) => ({
1695
+ requestId: request.requestId, // request.peer.ski identifies the paired device
1696
+ applianceIds: await adoptEebusPeer(request.peer, request.applianceId),
1697
+ }));
1698
+ ```
1699
+
1700
+ On an `offline-reconnect` run the request carries the appliance that went offline — re-point it and answer with the same `applianceId` rather than creating a second one. See [ONBOARDING.md](./ONBOARDING.md#picking-the-device-device-select).
1701
+
1683
1702
  Your answer wins over the host's own resolution, so a wrong value is worse than none — the installer pastes it into a charger and it fails minutes later as an `ocpp-connect` timeout. `validateOnboardingV2DynamicResult()` catches the copy-target mistakes (whitespace, a non-absolute URL, a plaintext scheme).
1684
1703
 
1685
- Each guide must carry the `vendorId`, `modelIds` and `startVariant` it applies to that is how the host selects one for a run, and there is no publish step left to bind them. See [ONBOARDING.md](./ONBOARDING.md#serving-guides-the-host-pulls-the-app-never-publishes) for the full v2 model.
1704
+ Each guide declares the `startVariant` it applies to (`device-not-found`, `device-found-config`, `manual-setup`, `maintenance`, `offline-reconnect`). It must **not** declare `vendorId` or `modelIds`: enyo attaches those bindings when the guide is registered, an app-supplied value is overwritten, and `validateOnboardingGuideV2()` warns about it. See [ONBOARDING.md](./ONBOARDING.md#serving-guides-the-host-pulls-the-app-never-publishes) for the full v2 model.
1686
1705
 
1687
1706
  Not permission-gated.
1688
1707
 
@@ -3603,6 +3622,112 @@ Notes:
3603
3622
  grant can be read against the announcement it answers. It is advisory — the
3604
3623
  command's `powerW` remains the only limit.
3605
3624
 
3625
+ #### Explaining Why a Command Was Issued
3626
+
3627
+ Every data bus command can carry an `EnyoDataBusCommandReason`. Its `type`
3628
+ (`EnyoDataBusCommandReasonTypeEnum`) and `category` are **closed, coarse
3629
+ vocabularies** — they exist for filtering, iconography and analytics, not for
3630
+ narrating a decision. There is no "thermal cost" or "efficiency" member, and you
3631
+ must not synthesize one: pick the honest type and attach the numbers.
3632
+
3633
+ The prose for the end user lives in `translation` (per language). The numbers
3634
+ behind that prose live in the optional `context`
3635
+ (`EnyoDataBusCommandReasonContext`), so a consumer can verify, re-render or audit
3636
+ the explanation instead of taking it on trust:
3637
+
3638
+ | Section | Answers | Key fields |
3639
+ | --- | --- | --- |
3640
+ | `proactive` | Did we act on a forecast rather than a measurement? | `boolean` |
3641
+ | `forecast` | What did we see coming? | `outdoorTemperatureC`, `thresholdTemperatureC`, `windowStartIso`/`windowEndIso`, `triggerTimestampIso` |
3642
+ | `placement` | When does the block run, and by when must it be done? | `startIso`, `endIso`, `durationMinutes`, `deadlineIso` |
3643
+ | `efficiency` | Why this slot and not the cheaper-looking one? | `coefficientOfPerformance`, `effectiveCostPerKwhThermal`, `compared*` |
3644
+ | `thermalStorage` | What ends the block? | `target`, `energyPerKelvinKwh`, `overheatKelvin`, `plannedEnergyKwh`, `absorbableEnergyKwh` |
3645
+
3646
+ Worked example — a buffer tank charged ahead of a cold front. The command is
3647
+ `ScheduledOptimization` (category `Schedule`), which is the honest type for a
3648
+ block an optimizer placed in advance:
3649
+
3650
+ ```typescript
3651
+ import {
3652
+ EnyoCurrencyEnum,
3653
+ EnyoDataBusCommandReason,
3654
+ EnyoDataBusCommandReasonCategoryEnum,
3655
+ EnyoDataBusCommandReasonTypeEnum,
3656
+ EnyoFlexibilityTargetEnum,
3657
+ } from '@enyo-energy/energy-app-sdk';
3658
+
3659
+ const reason: EnyoDataBusCommandReason = {
3660
+ type: EnyoDataBusCommandReasonTypeEnum.ScheduledOptimization,
3661
+ category: EnyoDataBusCommandReasonCategoryEnum.Schedule,
3662
+ translation: [
3663
+ {
3664
+ language: 'en',
3665
+ value:
3666
+ 'Banking 2 hours of cheap heat into the buffer tank before tonight\'s cold front, ' +
3667
+ 'so the compressor can coast through the expensive hours.',
3668
+ },
3669
+ ],
3670
+ // Per kWh of *electricity* — the raw price of the chosen slot.
3671
+ electricityPricePerKwh: 0.30,
3672
+ currency: EnyoCurrencyEnum.EUR,
3673
+ context: {
3674
+ // We watch the weather, not the tank.
3675
+ proactive: true,
3676
+ forecast: {
3677
+ outdoorTemperatureC: 6.5,
3678
+ thresholdTemperatureC: 10,
3679
+ windowStartIso: '2026-01-14T12:00:00Z',
3680
+ windowEndIso: '2026-01-15T12:00:00Z',
3681
+ triggerTimestampIso: '2026-01-14T21:00:00Z',
3682
+ },
3683
+ // A 120-minute block, finished before that cold front arrives.
3684
+ placement: {
3685
+ startIso: '2026-01-14T12:00:00Z',
3686
+ endIso: '2026-01-14T14:00:00Z',
3687
+ durationMinutes: 120,
3688
+ deadlineIso: '2026-01-14T21:00:00Z',
3689
+ },
3690
+ // Placed by price ÷ COP, not price alone: a warm midday hour at 30 ct
3691
+ // delivers cheaper heat than a cold night hour at 20 ct.
3692
+ efficiency: {
3693
+ coefficientOfPerformance: 4.2,
3694
+ effectiveCostPerKwhThermal: 0.0714,
3695
+ comparedElectricityPricePerKwh: 0.20,
3696
+ comparedCoefficientOfPerformance: 2.4,
3697
+ comparedEffectiveCostPerKwhThermal: 0.0833,
3698
+ comparedStartIso: '2026-01-14T02:00:00Z',
3699
+ },
3700
+ // And it stops once the store is physically full: the pump tells us one
3701
+ // Kelvin of overheat absorbs 1.2 kWh, so 5 K of headroom is 6.1 kWh.
3702
+ thermalStorage: {
3703
+ target: EnyoFlexibilityTargetEnum.BufferTank,
3704
+ energyPerKelvinKwh: 1.2,
3705
+ overheatKelvin: 5,
3706
+ plannedEnergyKwh: 6.0,
3707
+ absorbableEnergyKwh: 6.1,
3708
+ },
3709
+ },
3710
+ };
3711
+ ```
3712
+
3713
+ Notes:
3714
+
3715
+ - **`context` is entirely optional and additive.** A reason that sets none of it
3716
+ behaves exactly as before; consumers must tolerate every section being absent.
3717
+ - **Costs are not all per kWh of electricity.** `electricityPricePerKwh` is per
3718
+ kWh drawn; `effectiveCostPerKwhThermal` is per kWh *delivered*, in the same
3719
+ `currency`. Mixing the two is what makes an efficiency-placed command look like
3720
+ a mistake.
3721
+ - **One rejected alternative, not a solver dump.** The `compared*` fields describe
3722
+ the single best slot that lost, because that is what explains the choice to a
3723
+ person.
3724
+ - **Nothing in a reason controls anything.** It explains a command; the command's
3725
+ own fields stay authoritative.
3726
+ - **`thermalStorage.target` reuses `EnyoFlexibilityTargetEnum`**, the same
3727
+ vocabulary as flexibility announcements and the `targets` breakdown on
3728
+ available-power commands — so a grant and its justification name the same
3729
+ physical sink without a mapping table.
3730
+
3606
3731
  ### Settings Management
3607
3732
 
3608
3733
  Dynamic configuration with user interface:
@@ -243,69 +243,111 @@ exports.onboardingV2Block = {
243
243
  outcomes,
244
244
  }),
245
245
  /**
246
- * An EEBUS-pair action block: the installer picks one of the discovered
247
- * EEBUS peers and the host trusts its SKI.
246
+ * A device-select picker: its own screen listing the network devices the run
247
+ * has found, so the installer can say which one is being onboarded.
248
248
  *
249
- * A convenience wrapper over {@link onboardingV2Block.action} that pins the
250
- * action kind. The picker is drawn from what mDNS discovery found, so the
251
- * guide must have scanned keep
252
- * {@link EnyoOnboardingV2Guide.requiresNetworkScan} at its default or place
253
- * a {@link EnyoOnboardingV2ActionKind.NetworkScan} block ahead of this one.
249
+ * Use it whenever a scan can turn up more than one candidate. A
250
+ * {@link onboardingV2Block.networkScan} branches on found/not-found but binds
251
+ * nothing, so without this the run does not know *which* device it is working
252
+ * on — and {@link EnyoOnboardingV2DeviceSelection.Current} and
253
+ * {@link EnyoOnboardingV2DynamicKind.DeviceIp} have nothing to resolve
254
+ * against.
254
255
  *
255
- * Most EEBUS devices only announce themselves once pairing is enabled in
256
- * their own menu or portal, and many ask for a confirmation there while the
257
- * handshake runs, so put that instruction in a text/hint block on the
258
- * preceding step the app cannot do it for the installer.
256
+ * The block owns the step: give it one of its own, put the screen's wording
257
+ * in `headline`/`description` rather than in neighbouring blocks, and let it
258
+ * disappear when it has nothing to ask with `autoSelectSingleMatch` left at
259
+ * its default, a single matching device is bound without rendering anything.
259
260
  *
260
- * Outcome `value`s must be {@link EnyoOnboardingV2EebusPairOutcome} members;
261
- * route `not-found` to troubleshooting and `failure` to a step describing
262
- * the confirmation on the device. A retry must lead into a *second* pairing
263
- * step: a back-edge onto the same step reads as a loop and ends the run.
261
+ * The list is what discovery found, so the guide must have scanned: keep
262
+ * {@link EnyoOnboardingV2Guide.requiresNetworkScan} at its default, or place a
263
+ * {@link onboardingV2Block.networkScan} ahead of this one.
264
+ *
265
+ * Outcome `value`s must be {@link EnyoOnboardingV2DeviceSelectOutcome}
266
+ * members; route `not-found` to troubleshooting rather than to a step that
267
+ * assumes a device exists.
268
+ *
269
+ * Register an {@link EnyoOnboardingV2DeviceSelectHandler}
270
+ * ({@link EnergyAppOnboardingV2.registerDeviceSelectHandler}) to turn the pick
271
+ * into appliances — the host awaits it and binds the run to the ids it
272
+ * returns, whether or not the screen was shown. Without one the pick binds an
273
+ * address and nothing more.
264
274
  *
265
275
  * @param id - Stable block id, unique within the guide.
266
- * @param label - Translated trigger button text (de/en).
267
- * @param outcomes - The `paired` / `not-found` / `failure` results; each is a routing handle.
276
+ * @param options - Screen wording, optional `detectedAt` filter, skip
277
+ * behaviour, and the `selected` / `not-found` routing handles.
278
+ * @returns The device-select block.
279
+ *
280
+ * @example
281
+ * ```ts
282
+ * onboardingV2Block.deviceSelect('pick', {
283
+ * headline: t('Gerät auswählen', 'Select the device'),
284
+ * description: t(
285
+ * 'Vergleichen Sie die Seriennummer auf dem Typenschild.',
286
+ * 'Compare the serial number on the type plate.',
287
+ * ),
288
+ * detectedAt: [EnyoNetworkDeviceDetectedAtEnum.Modbus],
289
+ * outcomes: [
290
+ * {id: 'ok', value: EnyoOnboardingV2DeviceSelectOutcome.Selected, label: t('Ausgewählt', 'Selected')},
291
+ * {id: 'none', value: EnyoOnboardingV2DeviceSelectOutcome.NotFound, label: t('Nicht dabei', 'Not listed')},
292
+ * ],
293
+ * });
294
+ * ```
268
295
  */
269
- eebusPair: (id, label, outcomes) => ({
296
+ deviceSelect: (id, options) => ({
270
297
  id,
271
- type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Action,
272
- action: enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.EebusPair,
273
- label,
274
- outcomes,
298
+ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.DeviceSelect,
299
+ ...options,
275
300
  }),
276
301
  /**
277
- * A device-select block: the installer picks the device being onboarded from
278
- * everything the run has found.
302
+ * An EEBUS device-select picker: its own screen listing the discovered EEBUS
303
+ * peers, so the installer can pick the one to trust — the host pairs it and
304
+ * records its SKI.
279
305
  *
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.
306
+ * Filter it. `deviceTypes` is what turns this from "here are the six EEBUS
307
+ * devices in the house" into "here is your heat pump", and with one match it
308
+ * skips the screen entirely instead of asking a question with one possible
309
+ * answer.
286
310
  *
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.
311
+ * The picker is drawn from what mDNS discovery found, so the guide must have
312
+ * scanned — keep {@link EnyoOnboardingV2Guide.requiresNetworkScan} at its
313
+ * default or place a {@link onboardingV2Block.networkScan} ahead of this one.
290
314
  *
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.
315
+ * Most EEBUS devices only announce themselves once pairing is enabled in
316
+ * their own menu or portal, and many ask for a confirmation there while the
317
+ * handshake runs, so put that instruction in a text/hint block on the
318
+ * preceding step — the app cannot do it for the installer.
294
319
  *
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.
320
+ * Outcome `value`s must be {@link EnyoOnboardingV2EebusPairOutcome} members;
321
+ * route `not-found` to troubleshooting and `failure` to a step describing the
322
+ * confirmation on the device. A retry must lead into a *second* picker step:
323
+ * a back-edge onto the same step reads as a loop and ends the run.
324
+ *
325
+ * Register an {@link EnyoOnboardingV2EebusDeviceSelectHandler}
326
+ * ({@link EnergyAppOnboardingV2.registerEebusDeviceSelectHandler}) to turn the
327
+ * paired peer into appliances.
298
328
  *
299
329
  * @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.
330
+ * @param options - Screen wording, optional `deviceTypes` filter, skip
331
+ * behaviour, and the `paired` / `not-found` / `failure` routing handles.
332
+ * @returns The EEBUS device-select block.
333
+ *
334
+ * @example
335
+ * ```ts
336
+ * onboardingV2Block.eebusDeviceSelect('pair', {
337
+ * headline: t('Wärmepumpe auswählen', 'Select the heat pump'),
338
+ * deviceTypes: [EnyoEebusDeviceTypeEnum.HeatPumpAppliance],
339
+ * outcomes: [
340
+ * {id: 'ok', value: EnyoOnboardingV2EebusPairOutcome.Paired, label: t('Gekoppelt', 'Paired')},
341
+ * {id: 'none', value: EnyoOnboardingV2EebusPairOutcome.NotFound, label: t('Nichts gefunden', 'Nothing found')},
342
+ * {id: 'error', value: EnyoOnboardingV2EebusPairOutcome.Failure, label: t('Kopplung fehlgeschlagen', 'Pairing failed')},
343
+ * ],
344
+ * });
345
+ * ```
302
346
  */
303
- deviceSelect: (id, label, outcomes) => ({
347
+ eebusDeviceSelect: (id, options) => ({
304
348
  id,
305
- type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Action,
306
- action: enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.DeviceSelect,
307
- label,
308
- outcomes,
349
+ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.EebusDeviceSelect,
350
+ ...options,
309
351
  }),
310
352
  /**
311
353
  * An auth block: the installer signs into the energy app's own account
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import type { EnyoOnboardingTranslatedContent } from '../../types/enyo-onboarding.cjs';
13
13
  import { EnyoOnboardingV2ActionKind, EnyoOnboardingV2Credential, EnyoOnboardingV2SelectOption, EnyoOnboardingV2ChoiceLayout, EnyoOnboardingV2DeviceSelection, EnyoOnboardingV2PauseReason } from '../../types/enyo-onboarding-v2.cjs';
14
- import type { EnyoOnboardingV2ActionOutcome, EnyoOnboardingV2AuthOutcome, EnyoOnboardingV2SetupField, EnyoOnboardingV2SetupOutcome, EnyoOnboardingV2SetupSkipHandle, EnyoOnboardingV2Block, EnyoOnboardingV2ChoiceOption, EnyoOnboardingV2DynamicKind, EnyoOnboardingV2Guide, EnyoOnboardingV2HintVariant, EnyoOnboardingV2InputOutcome, EnyoOnboardingV2InputValueType, EnyoOnboardingV2StartVariant, EnyoOnboardingV2Target, EnyoOnboardingV2Transition } from '../../types/enyo-onboarding-v2.cjs';
14
+ import type { EnyoOnboardingV2ActionOutcome, EnyoOnboardingV2AuthOutcome, EnyoOnboardingV2SetupField, EnyoOnboardingV2SetupOutcome, EnyoOnboardingV2SetupSkipHandle, EnyoOnboardingV2Block, EnyoOnboardingV2ChoiceOption, EnyoOnboardingV2DeviceSelectBlock, EnyoOnboardingV2EebusDeviceSelectBlock, 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
@@ -194,58 +194,104 @@ export declare const onboardingV2Block: {
194
194
  */
195
195
  ocppConnect: (id: string, label: EnyoOnboardingTranslatedContent[], outcomes: EnyoOnboardingV2ActionOutcome[]) => EnyoOnboardingV2Block;
196
196
  /**
197
- * An EEBUS-pair action block: the installer picks one of the discovered
198
- * EEBUS peers and the host trusts its SKI.
199
- *
200
- * A convenience wrapper over {@link onboardingV2Block.action} that pins the
201
- * action kind. The picker is drawn from what mDNS discovery found, so the
202
- * guide must have scanned — keep
203
- * {@link EnyoOnboardingV2Guide.requiresNetworkScan} at its default or place
204
- * a {@link EnyoOnboardingV2ActionKind.NetworkScan} block ahead of this one.
205
- *
206
- * Most EEBUS devices only announce themselves once pairing is enabled in
207
- * their own menu or portal, and many ask for a confirmation there while the
208
- * handshake runs, so put that instruction in a text/hint block on the
209
- * preceding step — the app cannot do it for the installer.
210
- *
211
- * Outcome `value`s must be {@link EnyoOnboardingV2EebusPairOutcome} members;
212
- * route `not-found` to troubleshooting and `failure` to a step describing
213
- * the confirmation on the device. A retry must lead into a *second* pairing
214
- * step: a back-edge onto the same step reads as a loop and ends the run.
215
- *
216
- * @param id - Stable block id, unique within the guide.
217
- * @param label - Translated trigger button text (de/en).
218
- * @param outcomes - The `paired` / `not-found` / `failure` results; each is a routing handle.
219
- */
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.
197
+ * A device-select picker: its own screen listing the network devices the run
198
+ * has found, so the installer can say which one is being onboarded.
224
199
  *
225
200
  * 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
201
+ * {@link onboardingV2Block.networkScan} branches on found/not-found but binds
202
+ * nothing, so without this the run does not know *which* device it is working
203
+ * on — and {@link EnyoOnboardingV2DeviceSelection.Current} and
229
204
  * {@link EnyoOnboardingV2DynamicKind.DeviceIp} have nothing to resolve
230
205
  * against.
231
206
  *
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.
207
+ * The block owns the step: give it one of its own, put the screen's wording
208
+ * in `headline`/`description` rather than in neighbouring blocks, and let it
209
+ * disappear when it has nothing to ask — with `autoSelectSingleMatch` left at
210
+ * its default, a single matching device is bound without rendering anything.
211
+ *
212
+ * The list is what discovery found, so the guide must have scanned: keep
213
+ * {@link EnyoOnboardingV2Guide.requiresNetworkScan} at its default, or place a
214
+ * {@link onboardingV2Block.networkScan} ahead of this one.
235
215
  *
236
216
  * Outcome `value`s must be {@link EnyoOnboardingV2DeviceSelectOutcome}
237
217
  * members; route `not-found` to troubleshooting rather than to a step that
238
218
  * assumes a device exists.
239
219
  *
240
- * Register an {@link EnyoOnboardingV2DeviceSelectHandler} to turn the pick
220
+ * Register an {@link EnyoOnboardingV2DeviceSelectHandler}
221
+ * ({@link EnergyAppOnboardingV2.registerDeviceSelectHandler}) to turn the pick
241
222
  * 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.
223
+ * returns, whether or not the screen was shown. Without one the pick binds an
224
+ * address and nothing more.
243
225
  *
244
226
  * @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.
227
+ * @param options - Screen wording, optional `detectedAt` filter, skip
228
+ * behaviour, and the `selected` / `not-found` routing handles.
229
+ * @returns The device-select block.
230
+ *
231
+ * @example
232
+ * ```ts
233
+ * onboardingV2Block.deviceSelect('pick', {
234
+ * headline: t('Gerät auswählen', 'Select the device'),
235
+ * description: t(
236
+ * 'Vergleichen Sie die Seriennummer auf dem Typenschild.',
237
+ * 'Compare the serial number on the type plate.',
238
+ * ),
239
+ * detectedAt: [EnyoNetworkDeviceDetectedAtEnum.Modbus],
240
+ * outcomes: [
241
+ * {id: 'ok', value: EnyoOnboardingV2DeviceSelectOutcome.Selected, label: t('Ausgewählt', 'Selected')},
242
+ * {id: 'none', value: EnyoOnboardingV2DeviceSelectOutcome.NotFound, label: t('Nicht dabei', 'Not listed')},
243
+ * ],
244
+ * });
245
+ * ```
246
+ */
247
+ deviceSelect: (id: string, options: Omit<EnyoOnboardingV2DeviceSelectBlock, "id" | "type">) => EnyoOnboardingV2Block;
248
+ /**
249
+ * An EEBUS device-select picker: its own screen listing the discovered EEBUS
250
+ * peers, so the installer can pick the one to trust — the host pairs it and
251
+ * records its SKI.
252
+ *
253
+ * Filter it. `deviceTypes` is what turns this from "here are the six EEBUS
254
+ * devices in the house" into "here is your heat pump", and with one match it
255
+ * skips the screen entirely instead of asking a question with one possible
256
+ * answer.
257
+ *
258
+ * The picker is drawn from what mDNS discovery found, so the guide must have
259
+ * scanned — keep {@link EnyoOnboardingV2Guide.requiresNetworkScan} at its
260
+ * default or place a {@link onboardingV2Block.networkScan} ahead of this one.
261
+ *
262
+ * Most EEBUS devices only announce themselves once pairing is enabled in
263
+ * their own menu or portal, and many ask for a confirmation there while the
264
+ * handshake runs, so put that instruction in a text/hint block on the
265
+ * preceding step — the app cannot do it for the installer.
266
+ *
267
+ * Outcome `value`s must be {@link EnyoOnboardingV2EebusPairOutcome} members;
268
+ * route `not-found` to troubleshooting and `failure` to a step describing the
269
+ * confirmation on the device. A retry must lead into a *second* picker step:
270
+ * a back-edge onto the same step reads as a loop and ends the run.
271
+ *
272
+ * Register an {@link EnyoOnboardingV2EebusDeviceSelectHandler}
273
+ * ({@link EnergyAppOnboardingV2.registerEebusDeviceSelectHandler}) to turn the
274
+ * paired peer into appliances.
275
+ *
276
+ * @param id - Stable block id, unique within the guide.
277
+ * @param options - Screen wording, optional `deviceTypes` filter, skip
278
+ * behaviour, and the `paired` / `not-found` / `failure` routing handles.
279
+ * @returns The EEBUS device-select block.
280
+ *
281
+ * @example
282
+ * ```ts
283
+ * onboardingV2Block.eebusDeviceSelect('pair', {
284
+ * headline: t('Wärmepumpe auswählen', 'Select the heat pump'),
285
+ * deviceTypes: [EnyoEebusDeviceTypeEnum.HeatPumpAppliance],
286
+ * outcomes: [
287
+ * {id: 'ok', value: EnyoOnboardingV2EebusPairOutcome.Paired, label: t('Gekoppelt', 'Paired')},
288
+ * {id: 'none', value: EnyoOnboardingV2EebusPairOutcome.NotFound, label: t('Nichts gefunden', 'Nothing found')},
289
+ * {id: 'error', value: EnyoOnboardingV2EebusPairOutcome.Failure, label: t('Kopplung fehlgeschlagen', 'Pairing failed')},
290
+ * ],
291
+ * });
292
+ * ```
247
293
  */
248
- deviceSelect: (id: string, label: EnyoOnboardingTranslatedContent[], outcomes: EnyoOnboardingV2ActionOutcome[]) => EnyoOnboardingV2Block;
294
+ eebusDeviceSelect: (id: string, options: Omit<EnyoOnboardingV2EebusDeviceSelectBlock, "id" | "type">) => EnyoOnboardingV2Block;
249
295
  /**
250
296
  * An auth block: the installer signs into the energy app's own account
251
297
  * system (OAuth / vendor portal).
@@ -4,12 +4,15 @@
4
4
  * answer an app hands back when the host asks for its v2 onboarding guides.
5
5
  *
6
6
  * {@link validateOnboardingGuideV2} checks one guide's graph. This checks the
7
- * *set*: that every guide in it is publishable, that each one says which
8
- * vendor/model/start-variant it applies to, and that no two of them claim the
9
- * same one. Those last two only become checkable here, because a guide is now
10
- * selected out of an app's own answer rather than bound to a catalog entry at
11
- * publish time — a guide with no binding can never be chosen for a device, and
12
- * two guides with the same binding leave the host with no way to pick.
7
+ * *set*: that every guide in it is publishable and that no two of them use the
8
+ * same `name`, which is the handle an app addresses one specific guide by.
9
+ *
10
+ * It deliberately says nothing about vendor and model bindings. Those are
11
+ * enyo'sattached when a guide is registered, derived from the package serving
12
+ * it and the vendor catalog so an app's answer cannot claim one, cannot
13
+ * collide on one, and should not carry one
14
+ * ({@link EnyoOnboardingV2Guide.vendorId}). A guide that sets them anyway is
15
+ * warned about by {@link validateOnboardingGuideV2}.
13
16
  *
14
17
  * `errors` mean the answer is not fit to return; `warnings` are advisory. Use
15
18
  * {@link validateOnboardingV2GuidesResult} for the non-throwing result, or
@@ -38,12 +41,6 @@ class OnboardingV2GuidesValidationError extends Error {
38
41
  }
39
42
  }
40
43
  exports.OnboardingV2GuidesValidationError = OnboardingV2GuidesValidationError;
41
- /**
42
- * Placeholder used in a binding key for a guide that names no model — it applies
43
- * to every model of its vendor, and therefore collides with any other such guide
44
- * for the same vendor and start variant.
45
- */
46
- const ANY_MODEL = '*';
47
44
  /**
48
45
  * A short human-readable label for a guide, for use in messages.
49
46
  *
@@ -62,25 +59,9 @@ function guideLabel(guide, index) {
62
59
  const title = guide.title?.[0]?.value;
63
60
  return `guides[${index}] (${title ? `"${title}"` : (guide.startVariant ?? '?')})`;
64
61
  }
65
- /**
66
- * Every (vendor, model, start variant) binding a guide claims.
67
- *
68
- * A guide with several `modelIds` claims one binding per model, so two guides
69
- * that overlap on a single model collide even when the rest of their model lists
70
- * differ.
71
- *
72
- * @param guide - The guide to derive bindings for.
73
- * @returns The binding keys, or an empty array when the guide names no vendor.
74
- */
75
- function bindingKeys(guide) {
76
- if (!guide.vendorId)
77
- return [];
78
- const models = guide.modelIds?.length ? guide.modelIds : [ANY_MODEL];
79
- return models.map((modelId) => `${guide.vendorId}|${modelId}|${guide.startVariant}`);
80
- }
81
62
  /**
82
63
  * Validates a complete guides answer: the envelope, every guide in it, and the
83
- * bindings across them.
64
+ * `name` handles across them.
84
65
  *
85
66
  * Each guide is run through {@link validateOnboardingGuideV2}, and its errors
86
67
  * and warnings are surfaced here prefixed with the guide's position — pass the
@@ -124,23 +105,14 @@ function validateOnboardingV2GuidesResult(result, context) {
124
105
  warnings.push('Empty `guides` retires every guide the host cached for this app. ' +
125
106
  'Resolve the handler with `null` instead if the intent was "no answer right now".');
126
107
  }
127
- // Which guide(s) claimed each binding, so a collision can name both sides.
128
- const claimedBy = new Map();
129
- // Same, for the explicit `name` handles — a duplicate makes an app unable to
130
- // say which of the two it means.
108
+ // The explicit `name` handles a duplicate makes an app unable to say which
109
+ // of the two guides it means.
131
110
  const namedBy = new Map();
132
111
  for (const [i, guide] of result.guides.entries()) {
133
112
  const at = guideLabel(guide, i);
134
113
  const guideResult = (0, onboarding_v2_validators_js_1.validateOnboardingGuideV2)(guide, context);
135
114
  errors.push(...guideResult.errors.map((e) => `${at}: ${e}`));
136
115
  warnings.push(...guideResult.warnings.map((w) => `${at}: ${w}`));
137
- if (!guide.vendorId) {
138
- warnings.push(`${at}: no vendorId — the host matches a run by vendor, model and start variant, ` +
139
- 'so an unbound guide can never be selected.');
140
- }
141
- else if (!guide.modelIds?.length) {
142
- warnings.push(`${at}: no modelIds — this guide applies to every model of "${guide.vendorId}".`);
143
- }
144
116
  const name = guide.name?.trim();
145
117
  if (name) {
146
118
  const previouslyNamed = namedBy.get(name);
@@ -152,16 +124,6 @@ function validateOnboardingV2GuidesResult(result, context) {
152
124
  namedBy.set(name, at);
153
125
  }
154
126
  }
155
- for (const key of bindingKeys(guide)) {
156
- const previous = claimedBy.get(key);
157
- if (previous) {
158
- errors.push(`${at}: binding "${key}" is already claimed by ${previous} — ` +
159
- 'the host cannot choose between two guides for the same vendor, model and start variant.');
160
- }
161
- else {
162
- claimedBy.set(key, at);
163
- }
164
- }
165
127
  }
166
128
  return { ok: errors.length === 0, errors, warnings };
167
129
  }
@@ -3,12 +3,15 @@
3
3
  * answer an app hands back when the host asks for its v2 onboarding guides.
4
4
  *
5
5
  * {@link validateOnboardingGuideV2} checks one guide's graph. This checks the
6
- * *set*: that every guide in it is publishable, that each one says which
7
- * vendor/model/start-variant it applies to, and that no two of them claim the
8
- * same one. Those last two only become checkable here, because a guide is now
9
- * selected out of an app's own answer rather than bound to a catalog entry at
10
- * publish time — a guide with no binding can never be chosen for a device, and
11
- * two guides with the same binding leave the host with no way to pick.
6
+ * *set*: that every guide in it is publishable and that no two of them use the
7
+ * same `name`, which is the handle an app addresses one specific guide by.
8
+ *
9
+ * It deliberately says nothing about vendor and model bindings. Those are
10
+ * enyo'sattached when a guide is registered, derived from the package serving
11
+ * it and the vendor catalog so an app's answer cannot claim one, cannot
12
+ * collide on one, and should not carry one
13
+ * ({@link EnyoOnboardingV2Guide.vendorId}). A guide that sets them anyway is
14
+ * warned about by {@link validateOnboardingGuideV2}.
12
15
  *
13
16
  * `errors` mean the answer is not fit to return; `warnings` are advisory. Use
14
17
  * {@link validateOnboardingV2GuidesResult} for the non-throwing result, or
@@ -40,7 +43,7 @@ export interface OnboardingV2GuidesValidationResult {
40
43
  }
41
44
  /**
42
45
  * Validates a complete guides answer: the envelope, every guide in it, and the
43
- * bindings across them.
46
+ * `name` handles across them.
44
47
  *
45
48
  * Each guide is run through {@link validateOnboardingGuideV2}, and its errors
46
49
  * and warnings are surfaced here prefixed with the guide's position — pass the