@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.
- package/README.md +20 -5
- package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.cjs +74 -2
- package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.d.cts +60 -4
- package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.cjs +142 -12
- package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.d.cts +10 -4
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.d.cts +2 -0
- package/dist/cjs/packages/energy-app-charging-card.d.cts +47 -2
- package/dist/cjs/types/enyo-charging-card.d.cts +21 -0
- package/dist/cjs/types/enyo-data-bus-value.cjs +9 -0
- package/dist/cjs/types/enyo-data-bus-value.d.cts +19 -0
- package/dist/cjs/types/enyo-onboarding-v2.cjs +54 -7
- package/dist/cjs/types/enyo-onboarding-v2.d.cts +128 -12
- 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 +60 -4
- package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.js +75 -3
- package/dist/implementations/onboarding-v2/onboarding-v2-validators.d.ts +10 -4
- package/dist/implementations/onboarding-v2/onboarding-v2-validators.js +143 -13
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/packages/energy-app-charging-card.d.ts +47 -2
- package/dist/types/enyo-charging-card.d.ts +21 -0
- package/dist/types/enyo-data-bus-value.d.ts +19 -0
- package/dist/types/enyo-data-bus-value.js +9 -0
- package/dist/types/enyo-onboarding-v2.d.ts +128 -12
- package/dist/types/enyo-onboarding-v2.js +53 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
-
|
|
760
|
+
Read registered charging cards and pair new RFID cards:
|
|
761
761
|
|
|
762
762
|
```typescript
|
|
763
763
|
const chargingCards = energyApp.useChargingCard();
|
|
764
764
|
|
|
765
|
-
//
|
|
766
|
-
const
|
|
765
|
+
// List all registered charging cards
|
|
766
|
+
const cards = await chargingCards.list();
|
|
767
767
|
|
|
768
|
-
// Get card
|
|
769
|
-
const
|
|
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
|
-
/**
|
|
195
|
-
|
|
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,
|
|
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
|
-
/**
|
|
147
|
-
|
|
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
|
|
9
|
-
* result, or {@link assertValidOnboardingGuideV2} to throw on the
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 (!
|
|
121
|
-
warnings.push('No branch reaches a `success`
|
|
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
|
|
8
|
-
* result, or {@link assertValidOnboardingGuideV2} to throw on the
|
|
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}.
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -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);
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -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
|