@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.
- package/README.md +126 -1
- package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.cjs +88 -46
- package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.d.cts +85 -39
- package/dist/cjs/implementations/onboarding-v2/onboarding-v2-provider-validators.cjs +12 -50
- package/dist/cjs/implementations/onboarding-v2/onboarding-v2-provider-validators.d.cts +10 -7
- package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.cjs +164 -41
- package/dist/cjs/index.cjs +3 -0
- package/dist/cjs/index.d.cts +3 -0
- package/dist/cjs/packages/energy-app-onboarding-v2.d.cts +48 -7
- package/dist/cjs/types/enyo-data-bus-value.cjs +32 -3
- package/dist/cjs/types/enyo-data-bus-value.d.cts +236 -4
- package/dist/cjs/types/enyo-eebus.cjs +47 -1
- package/dist/cjs/types/enyo-eebus.d.cts +57 -0
- package/dist/cjs/types/enyo-onboarding-v2-device-select.cjs +2 -2
- package/dist/cjs/types/enyo-onboarding-v2-device-select.d.cts +31 -6
- package/dist/cjs/types/enyo-onboarding-v2-eebus-device-select.cjs +29 -0
- package/dist/cjs/types/enyo-onboarding-v2-eebus-device-select.d.cts +174 -0
- package/dist/cjs/types/enyo-onboarding-v2.cjs +36 -64
- package/dist/cjs/types/enyo-onboarding-v2.d.cts +278 -94
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.d.ts +85 -39
- package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.js +88 -46
- package/dist/implementations/onboarding-v2/onboarding-v2-provider-validators.d.ts +10 -7
- package/dist/implementations/onboarding-v2/onboarding-v2-provider-validators.js +12 -50
- package/dist/implementations/onboarding-v2/onboarding-v2-validators.js +164 -41
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/packages/energy-app-onboarding-v2.d.ts +48 -7
- package/dist/types/enyo-data-bus-value.d.ts +236 -4
- package/dist/types/enyo-data-bus-value.js +32 -3
- package/dist/types/enyo-eebus.d.ts +57 -0
- package/dist/types/enyo-eebus.js +46 -0
- package/dist/types/enyo-onboarding-v2-device-select.d.ts +31 -6
- package/dist/types/enyo-onboarding-v2-device-select.js +2 -2
- package/dist/types/enyo-onboarding-v2-eebus-device-select.d.ts +174 -0
- package/dist/types/enyo-onboarding-v2-eebus-device-select.js +28 -0
- package/dist/types/enyo-onboarding-v2.d.ts +278 -94
- package/dist/types/enyo-onboarding-v2.js +36 -64
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
|
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
|
-
*
|
|
247
|
-
*
|
|
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
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* {@link
|
|
253
|
-
*
|
|
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
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
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
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
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
|
|
267
|
-
*
|
|
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
|
-
|
|
296
|
+
deviceSelect: (id, options) => ({
|
|
270
297
|
id,
|
|
271
|
-
type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.
|
|
272
|
-
|
|
273
|
-
label,
|
|
274
|
-
outcomes,
|
|
298
|
+
type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.DeviceSelect,
|
|
299
|
+
...options,
|
|
275
300
|
}),
|
|
276
301
|
/**
|
|
277
|
-
*
|
|
278
|
-
*
|
|
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
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
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
|
|
288
|
-
* keep {@link EnyoOnboardingV2Guide.requiresNetworkScan} at its
|
|
289
|
-
* place a {@link
|
|
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
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
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
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
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
|
|
301
|
-
*
|
|
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
|
-
|
|
347
|
+
eebusDeviceSelect: (id, options) => ({
|
|
304
348
|
id,
|
|
305
|
-
type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.
|
|
306
|
-
|
|
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
|
-
*
|
|
198
|
-
*
|
|
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
|
|
227
|
-
* so without this the run does not know *which* device it is working
|
|
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
|
|
233
|
-
*
|
|
234
|
-
*
|
|
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}
|
|
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
|
|
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
|
|
246
|
-
*
|
|
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
|
-
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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's — attached 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
|
-
*
|
|
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
|
-
//
|
|
128
|
-
|
|
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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's — attached 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
|
-
*
|
|
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
|