@enyo-energy/energy-app-sdk 0.0.192 → 0.0.194

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 (28) hide show
  1. package/README.md +10 -0
  2. package/dist/cjs/energy-app-package-definition.d.cts +12 -0
  3. package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.cjs +105 -2
  4. package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.d.cts +85 -4
  5. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.cjs +199 -14
  6. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.d.cts +10 -4
  7. package/dist/cjs/integrations/wallbox-integration-energy-app.cjs +34 -0
  8. package/dist/cjs/integrations/wallbox-integration-energy-app.d.cts +23 -1
  9. package/dist/cjs/types/enyo-data-bus-value.cjs +21 -1
  10. package/dist/cjs/types/enyo-data-bus-value.d.cts +155 -1
  11. package/dist/cjs/types/enyo-onboarding-v2.cjs +104 -7
  12. package/dist/cjs/types/enyo-onboarding-v2.d.cts +179 -12
  13. package/dist/cjs/version.cjs +1 -1
  14. package/dist/cjs/version.d.cts +1 -1
  15. package/dist/energy-app-package-definition.d.ts +12 -0
  16. package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.d.ts +85 -4
  17. package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.js +106 -3
  18. package/dist/implementations/onboarding-v2/onboarding-v2-validators.d.ts +10 -4
  19. package/dist/implementations/onboarding-v2/onboarding-v2-validators.js +200 -15
  20. package/dist/integrations/wallbox-integration-energy-app.d.ts +23 -1
  21. package/dist/integrations/wallbox-integration-energy-app.js +34 -0
  22. package/dist/types/enyo-data-bus-value.d.ts +155 -1
  23. package/dist/types/enyo-data-bus-value.js +20 -0
  24. package/dist/types/enyo-onboarding-v2.d.ts +179 -12
  25. package/dist/types/enyo-onboarding-v2.js +103 -6
  26. package/dist/version.d.ts +1 -1
  27. package/dist/version.js +1 -1
  28. package/package.json +1 -1
package/README.md CHANGED
@@ -2054,6 +2054,16 @@ Drives an EV wallbox / charger. Has the richest command surface of all integrati
2054
2054
  - `publishChargingMeterValues(applianceId, data)` — periodic meter values during a session.
2055
2055
  - `publishMaxChargingPowerChanged(applianceId, maxChargingPowerKw)` — e.g. on thermal derating.
2056
2056
  - `publishChargerStatusChanged(applianceId, data)` — OCPP-style status changes.
2057
+ - `publishEnergyManagementChargingState(applianceId, data)` — the app's own continuously
2058
+ updated state for the energy manager. Facts only: `activePhases` (the phase count in
2059
+ force), `phaseSwitching` (`supported` / `available` right now, `availableFromIso` when
2060
+ it is barred until a known time, `inProgress`), `appliedCurrentLimitA` and the
2061
+ `appliedPowerW` it implies, `lastLimitRequest` (`accepted` \| `clamped` \| `rejected`,
2062
+ with what was requested), and `measuredAtIso` — when the reading was taken, as opposed
2063
+ to when the message was sent. No recommended minimum power, no suggested setpoint,
2064
+ nothing derived from the manager's own plan; static nameplate data stays in the
2065
+ appliance metadata. Republish the **complete** payload on every change and periodically
2066
+ as a heartbeat — an omitted field means "unknown", never "unchanged".
2057
2067
 
2058
2068
  ```typescript
2059
2069
  class MyWallbox extends WallboxIntegrationEnergyApp {
@@ -228,6 +228,18 @@ export interface EnergyAppPackageCompatibilityModel {
228
228
  * upgrade graph in {@link EnergyAppPackageDefinition.firmware} instead.
229
229
  */
230
230
  minimumFirmwareVersion?: string;
231
+ /**
232
+ * Optional device category of this concrete model (e.g. `Inverter`,
233
+ * `BatteryStorage`).
234
+ *
235
+ * Packages can support models of different kinds — a hybrid inverter plus a
236
+ * matching battery, for example — while
237
+ * {@link EnergyAppPackageDefinition.categories} only describes the package
238
+ * as a whole. Declaring the category per model lets the enyo Store and
239
+ * onboarding flows group and filter individual models correctly. Omit when
240
+ * the package-level categories are precise enough.
241
+ */
242
+ category?: EnergyAppPackageCategory;
231
243
  /** Optional internal note explaining model-specific caveats or limitations */
232
244
  internalComment?: string;
233
245
  /**
@@ -133,6 +133,83 @@ 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 EEBUS-pair action block: the installer picks one of the discovered
163
+ * EEBUS peers and the host trusts its SKI.
164
+ *
165
+ * A convenience wrapper over {@link onboardingV2Block.action} that pins the
166
+ * action kind. The picker is drawn from what mDNS discovery found, so the
167
+ * guide must have scanned — keep
168
+ * {@link EnyoOnboardingV2Guide.requiresNetworkScan} at its default or place
169
+ * a {@link EnyoOnboardingV2ActionKind.NetworkScan} block ahead of this one.
170
+ *
171
+ * Most EEBUS devices only announce themselves once pairing is enabled in
172
+ * their own menu or portal, and many ask for a confirmation there while the
173
+ * handshake runs, so put that instruction in a text/hint block on the
174
+ * preceding step — the app cannot do it for the installer.
175
+ *
176
+ * Outcome `value`s must be {@link EnyoOnboardingV2EebusPairOutcome} members;
177
+ * route `not-found` to troubleshooting and `failure` to a step describing
178
+ * the confirmation on the device. A retry must lead into a *second* pairing
179
+ * step: a back-edge onto the same step reads as a loop and ends the run.
180
+ *
181
+ * @param id - Stable block id, unique within the guide.
182
+ * @param label - Translated trigger button text (de/en).
183
+ * @param outcomes - The `paired` / `not-found` / `failure` results; each is a routing handle.
184
+ */
185
+ eebusPair: (id, label, outcomes) => ({
186
+ id,
187
+ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Action,
188
+ action: enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.EebusPair,
189
+ label,
190
+ outcomes,
191
+ }),
192
+ /**
193
+ * An auth block: the installer signs into the energy app's own account
194
+ * system (OAuth / vendor portal).
195
+ *
196
+ * Exactly one routing handle, and it means "the login succeeded". The server
197
+ * decides when it fires, so the installer cannot skip it; there is no failure
198
+ * branch to author — a failed attempt simply keeps them on the step. Route
199
+ * the handle with {@link onOutcomeV2}, passing `outcome.id`.
200
+ *
201
+ * @param id - Stable block id, unique within the guide.
202
+ * @param label - Translated sign-in button text (de/en).
203
+ * @param outcome - The single success handle (`{id, label}`).
204
+ * @param opts - Optional translated `help` naming the account that is needed.
205
+ */
206
+ auth: (id, label, outcome, opts) => ({
207
+ id,
208
+ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Auth,
209
+ label,
210
+ outcome,
211
+ help: opts?.help,
212
+ }),
136
213
  /**
137
214
  * A link block: a fixed URL the installer opens or copies.
138
215
  *
@@ -191,14 +268,40 @@ exports.onboardingV2Target = {
191
268
  step: (stepId) => ({ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Step, stepId }),
192
269
  /** Exit: onboarding succeeded (hand back to the app). */
193
270
  success: () => ({ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Success }),
194
- /** Exit: escalate to enyo support. */
195
- support: () => ({ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Support }),
271
+ /**
272
+ * Exit: escalate to enyo support.
273
+ * @param reason - Optional short internal key describing what failed, e.g.
274
+ * `firmware-too-old`. Never shown to the installer; it travels with the
275
+ * hand-off so support knows why it arrived.
276
+ */
277
+ support: (reason) => ({
278
+ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Support,
279
+ reason,
280
+ }),
196
281
  /**
197
282
  * Exit: pause the run (resumable) with a reason.
283
+ *
284
+ * For {@link EnyoOnboardingV2PauseReason.EnyoTodo} prefer
285
+ * {@link onboardingV2Target.enyoTakeover} — that reason is a terminal
286
+ * hand-off, not a park, and `resumeStepName` does not apply to it.
287
+ *
198
288
  * @param reason - Why the run is parked.
199
289
  * @param resumeStepName - Optional step `name` to resume at.
200
290
  */
201
291
  pause: (reason, resumeStepName) => ({ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Pause, reason, resumeStepName }),
292
+ /**
293
+ * Exit: **enyo übernimmt** — the installer is done and enyo finishes the
294
+ * setup. A terminal exit alongside `success` and `support`: the app shows the
295
+ * takeover screen rather than returning to the cockpit.
296
+ *
297
+ * Emits the unchanged wire shape (`pause` with reason `enyo-todo`), and
298
+ * counts as a completing exit for {@link validateOnboardingGuideV2} — a guide
299
+ * that only ends here needs no `success` branch.
300
+ */
301
+ enyoTakeover: () => ({
302
+ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Pause,
303
+ reason: enyo_onboarding_v2_js_1.EnyoOnboardingV2PauseReason.EnyoTodo,
304
+ }),
202
305
  /**
203
306
  * Jump into another start variant's flow for the same vendor/model.
204
307
  * @param variant - The start variant to hand off to.
@@ -10,8 +10,8 @@
10
10
  * before publishing.
11
11
  */
12
12
  import type { EnyoOnboardingTranslatedContent } from '../../types/enyo-onboarding.cjs';
13
- import { EnyoOnboardingV2ActionKind, EnyoOnboardingV2ChoiceLayout, EnyoOnboardingV2DeviceSelection } from '../../types/enyo-onboarding-v2.cjs';
14
- import type { EnyoOnboardingV2ActionOutcome, EnyoOnboardingV2Block, EnyoOnboardingV2ChoiceOption, EnyoOnboardingV2DynamicKind, EnyoOnboardingV2Guide, EnyoOnboardingV2HintVariant, EnyoOnboardingV2InputOutcome, EnyoOnboardingV2InputValueType, EnyoOnboardingV2PauseReason, EnyoOnboardingV2StartVariant, EnyoOnboardingV2Target, EnyoOnboardingV2Transition } from '../../types/enyo-onboarding-v2.cjs';
13
+ import { EnyoOnboardingV2ActionKind, EnyoOnboardingV2ChoiceLayout, EnyoOnboardingV2DeviceSelection, EnyoOnboardingV2PauseReason } from '../../types/enyo-onboarding-v2.cjs';
14
+ import type { EnyoOnboardingV2ActionOutcome, EnyoOnboardingV2AuthOutcome, EnyoOnboardingV2Block, EnyoOnboardingV2ChoiceOption, EnyoOnboardingV2DynamicKind, EnyoOnboardingV2Guide, EnyoOnboardingV2HintVariant, EnyoOnboardingV2InputOutcome, EnyoOnboardingV2InputValueType, EnyoOnboardingV2StartVariant, EnyoOnboardingV2Target, EnyoOnboardingV2Transition } from '../../types/enyo-onboarding-v2.cjs';
15
15
  /**
16
16
  * Identity helper that type-checks a v2 guide literal at definition time
17
17
  * (mirrors `defineEnergyAppPackage`). Prefer this over a bare object literal so
@@ -98,6 +98,67 @@ 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 EEBUS-pair action block: the installer picks one of the discovered
122
+ * EEBUS peers and the host trusts its SKI.
123
+ *
124
+ * A convenience wrapper over {@link onboardingV2Block.action} that pins the
125
+ * action kind. The picker is drawn from what mDNS discovery found, so the
126
+ * guide must have scanned — keep
127
+ * {@link EnyoOnboardingV2Guide.requiresNetworkScan} at its default or place
128
+ * a {@link EnyoOnboardingV2ActionKind.NetworkScan} block ahead of this one.
129
+ *
130
+ * Most EEBUS devices only announce themselves once pairing is enabled in
131
+ * their own menu or portal, and many ask for a confirmation there while the
132
+ * handshake runs, so put that instruction in a text/hint block on the
133
+ * preceding step — the app cannot do it for the installer.
134
+ *
135
+ * Outcome `value`s must be {@link EnyoOnboardingV2EebusPairOutcome} members;
136
+ * route `not-found` to troubleshooting and `failure` to a step describing
137
+ * the confirmation on the device. A retry must lead into a *second* pairing
138
+ * step: a back-edge onto the same step reads as a loop and ends the run.
139
+ *
140
+ * @param id - Stable block id, unique within the guide.
141
+ * @param label - Translated trigger button text (de/en).
142
+ * @param outcomes - The `paired` / `not-found` / `failure` results; each is a routing handle.
143
+ */
144
+ eebusPair: (id: string, label: EnyoOnboardingTranslatedContent[], outcomes: EnyoOnboardingV2ActionOutcome[]) => EnyoOnboardingV2Block;
145
+ /**
146
+ * An auth block: the installer signs into the energy app's own account
147
+ * system (OAuth / vendor portal).
148
+ *
149
+ * Exactly one routing handle, and it means "the login succeeded". The server
150
+ * decides when it fires, so the installer cannot skip it; there is no failure
151
+ * branch to author — a failed attempt simply keeps them on the step. Route
152
+ * the handle with {@link onOutcomeV2}, passing `outcome.id`.
153
+ *
154
+ * @param id - Stable block id, unique within the guide.
155
+ * @param label - Translated sign-in button text (de/en).
156
+ * @param outcome - The single success handle (`{id, label}`).
157
+ * @param opts - Optional translated `help` naming the account that is needed.
158
+ */
159
+ auth: (id: string, label: EnyoOnboardingTranslatedContent[], outcome: EnyoOnboardingV2AuthOutcome, opts?: {
160
+ help?: EnyoOnboardingTranslatedContent[];
161
+ }) => EnyoOnboardingV2Block;
101
162
  /**
102
163
  * A link block: a fixed URL the installer opens or copies.
103
164
  *
@@ -143,14 +204,34 @@ export declare const onboardingV2Target: {
143
204
  step: (stepId: string) => EnyoOnboardingV2Target;
144
205
  /** Exit: onboarding succeeded (hand back to the app). */
145
206
  success: () => EnyoOnboardingV2Target;
146
- /** Exit: escalate to enyo support. */
147
- support: () => EnyoOnboardingV2Target;
207
+ /**
208
+ * Exit: escalate to enyo support.
209
+ * @param reason - Optional short internal key describing what failed, e.g.
210
+ * `firmware-too-old`. Never shown to the installer; it travels with the
211
+ * hand-off so support knows why it arrived.
212
+ */
213
+ support: (reason?: string) => EnyoOnboardingV2Target;
148
214
  /**
149
215
  * Exit: pause the run (resumable) with a reason.
216
+ *
217
+ * For {@link EnyoOnboardingV2PauseReason.EnyoTodo} prefer
218
+ * {@link onboardingV2Target.enyoTakeover} — that reason is a terminal
219
+ * hand-off, not a park, and `resumeStepName` does not apply to it.
220
+ *
150
221
  * @param reason - Why the run is parked.
151
222
  * @param resumeStepName - Optional step `name` to resume at.
152
223
  */
153
224
  pause: (reason: EnyoOnboardingV2PauseReason, resumeStepName?: string) => EnyoOnboardingV2Target;
225
+ /**
226
+ * Exit: **enyo übernimmt** — the installer is done and enyo finishes the
227
+ * setup. A terminal exit alongside `success` and `support`: the app shows the
228
+ * takeover screen rather than returning to the cockpit.
229
+ *
230
+ * Emits the unchanged wire shape (`pause` with reason `enyo-todo`), and
231
+ * counts as a completing exit for {@link validateOnboardingGuideV2} — a guide
232
+ * that only ends here needs no `success` branch.
233
+ */
234
+ enyoTakeover: () => EnyoOnboardingV2Target;
154
235
  /**
155
236
  * Jump into another start variant's flow for the same vendor/model.
156
237
  * @param variant - The start variant to hand off to.
@@ -5,8 +5,14 @@
5
5
  * energy app can fail fast locally before publishing.
6
6
  *
7
7
  * `errors` block publishing; `warnings` are advisory (e.g. unreachable step, no
8
- * path to success). Use {@link validateOnboardingGuideV2} for the non-throwing
9
- * result, or {@link assertValidOnboardingGuideV2} to throw on the first failure.
8
+ * path to a completing exit). Use {@link validateOnboardingGuideV2} for the
9
+ * non-throwing result, or {@link assertValidOnboardingGuideV2} to throw on the
10
+ * first failure.
11
+ *
12
+ * A run completes either through a `success` target **or** through the
13
+ * "enyo übernimmt" hand-off (`pause` with reason
14
+ * {@link EnyoOnboardingV2PauseReason.EnyoTodo}) — both count, so a guide that
15
+ * legitimately ends in a hand-off is not nagged about a missing success path.
10
16
  */
11
17
  Object.defineProperty(exports, "__esModule", { value: true });
12
18
  exports.OnboardingV2ValidationError = void 0;
@@ -34,8 +40,8 @@ exports.OnboardingV2ValidationError = OnboardingV2ValidationError;
34
40
  const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
35
41
  /**
36
42
  * Structural + plausibility validation of a v2 guide graph. `errors` block
37
- * publishing; `warnings` are advisory (e.g. unreachable step, no path to
38
- * success).
43
+ * publishing; `warnings` are advisory (e.g. unreachable step, no path to a
44
+ * completing exit — `success` or the `enyo-todo` hand-off).
39
45
  *
40
46
  * @param guide - The v2 guide to validate.
41
47
  * @returns The {@link OnboardingV2ValidationResult}.
@@ -72,11 +78,12 @@ function validateOnboardingGuideV2(guide) {
72
78
  validateActionBlocks(step, at, errors, warnings);
73
79
  validateLinkBlocks(step, at, errors, warnings);
74
80
  validateInputBlocks(step, at, errors, warnings);
81
+ validateAuthBlocks(step, at, errors, warnings);
75
82
  }
76
83
  if (!guide.startStepId || !stepIds.has(guide.startStepId)) {
77
84
  errors.push('`startStepId` must reference an existing step.');
78
85
  }
79
- let hasSuccess = false;
86
+ let hasCompletingExit = false;
80
87
  for (const [i, step] of guide.steps.entries()) {
81
88
  const at = `steps[${i}] (${step.name})`;
82
89
  const required = requiredHandleKeys(step);
@@ -90,14 +97,24 @@ function validateOnboardingGuideV2(guide) {
90
97
  wired.add(key);
91
98
  const tg = t.target;
92
99
  if (tg.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Success)
93
- hasSuccess = true;
100
+ hasCompletingExit = true;
94
101
  else if (tg.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Step && !stepIds.has(tg.stepId)) {
95
102
  errors.push(`${at}: transition targets missing step "${tg.stepId}".`);
96
103
  }
97
- else if (tg.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Pause &&
98
- tg.resumeStepName &&
99
- !stepNames.has(tg.resumeStepName)) {
100
- errors.push(`${at}: pause resumeStepName "${tg.resumeStepName}" is unknown.`);
104
+ else if (tg.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.Pause) {
105
+ // "enyo übernimmt" is a terminal hand-off, not a park: it completes
106
+ // the run for the installer, so it satisfies the completion check
107
+ // and has nothing to resume at.
108
+ if (tg.reason === enyo_onboarding_v2_js_1.EnyoOnboardingV2PauseReason.EnyoTodo) {
109
+ hasCompletingExit = true;
110
+ if (tg.resumeStepName) {
111
+ warnings.push(`${at}: pause reason "${enyo_onboarding_v2_js_1.EnyoOnboardingV2PauseReason.EnyoTodo}" is a hand-off to enyo, ` +
112
+ `not a resumable park — resumeStepName "${tg.resumeStepName}" is ignored.`);
113
+ }
114
+ }
115
+ else if (tg.resumeStepName && !stepNames.has(tg.resumeStepName)) {
116
+ errors.push(`${at}: pause resumeStepName "${tg.resumeStepName}" is unknown.`);
117
+ }
101
118
  }
102
119
  else if (tg.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.StartVariant &&
103
120
  tg.variant === guide.startVariant) {
@@ -117,8 +134,11 @@ function validateOnboardingGuideV2(guide) {
117
134
  warnings.push(`step "${step.name}" is unreachable from the start.`);
118
135
  }
119
136
  }
120
- if (!hasSuccess)
121
- warnings.push('No branch reaches a `success` exit.');
137
+ if (!hasCompletingExit) {
138
+ warnings.push('No branch reaches a completing exit — wire a `success` target, or a `pause` with reason ' +
139
+ `"${enyo_onboarding_v2_js_1.EnyoOnboardingV2PauseReason.EnyoTodo}" when enyo takes the setup over.`);
140
+ }
141
+ validateNetworkScanFlag(guide, warnings);
122
142
  return { ok: errors.length === 0, errors, warnings };
123
143
  }
124
144
  /**
@@ -155,6 +175,12 @@ 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
+ }
181
+ if (block.action === enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.EebusPair) {
182
+ validateEebusPairOutcomes(block, at, errors, warnings);
183
+ }
158
184
  continue;
159
185
  }
160
186
  const values = new Set();
@@ -210,8 +236,17 @@ function validateLinkBlocks(step, at, errors, warnings) {
210
236
  }
211
237
  /** Every valid {@link EnyoOnboardingV2InputValueType} value. */
212
238
  const INPUT_VALUE_TYPES = new Set(Object.values(enyo_onboarding_v2_js_1.EnyoOnboardingV2InputValueType));
213
- /** Outcome values the host treats as "the check succeeded". */
214
- const POSITIVE_INPUT_OUTCOMES = new Set(['reachable', 'success', 'found']);
239
+ /**
240
+ * Outcome values the host treats as "the check succeeded" — mirrors the
241
+ * runtime's own positive-outcome set, `paired` included, so an eebus-pair
242
+ * result is not read as a failure.
243
+ */
244
+ const POSITIVE_INPUT_OUTCOMES = new Set([
245
+ 'reachable',
246
+ 'success',
247
+ 'found',
248
+ enyo_onboarding_v2_js_1.EnyoOnboardingV2EebusPairOutcome.Paired,
249
+ ]);
215
250
  /**
216
251
  * The {@link EnyoDeviceTestOutcomeEnum} verdicts that collapse onto a positive
217
252
  * input outcome; every other verdict collapses onto a negative one.
@@ -284,6 +319,153 @@ function validateInputBlocks(step, at, errors, warnings) {
284
319
  }
285
320
  }
286
321
  }
322
+ /** Every {@link EnyoOnboardingV2OcppConnectOutcome} value. */
323
+ const OCPP_CONNECT_OUTCOMES = new Set(Object.values(enyo_onboarding_v2_js_1.EnyoOnboardingV2OcppConnectOutcome));
324
+ /**
325
+ * Validates the outcomes of an {@link EnyoOnboardingV2ActionKind.OcppConnect}
326
+ * block.
327
+ *
328
+ * The block waits for the charger to dial into our CSMS; it can only ever report
329
+ * that the connection arrived or that it did not, so its outcome `value`s are
330
+ * closed over {@link EnyoOnboardingV2OcppConnectOutcome}. Both must be wired: a
331
+ * charger that never calls home — wrong URL typed, no mobile coverage in the
332
+ * garage — is the *common* case, and a guide without a `timeout` branch strands
333
+ * the installer on a spinner.
334
+ *
335
+ * @param block - The ocpp-connect action block being checked.
336
+ * @param at - Human-readable location prefix for messages.
337
+ * @param errors - Collector for blocking problems.
338
+ */
339
+ function validateOcppConnectOutcomes(block, at, errors) {
340
+ const values = new Set();
341
+ for (const outcome of block.outcomes ?? []) {
342
+ if (!OCPP_CONNECT_OUTCOMES.has(outcome.value)) {
343
+ errors.push(`${at}: ocpp-connect block "${block.id}" has outcome value "${outcome.value}", which is not an EnyoOnboardingV2OcppConnectOutcome member.`);
344
+ }
345
+ else if (values.has(outcome.value)) {
346
+ errors.push(`${at}: ocpp-connect block "${block.id}" wires outcome value "${outcome.value}" more than once.`);
347
+ }
348
+ values.add(outcome.value);
349
+ }
350
+ for (const required of OCPP_CONNECT_OUTCOMES) {
351
+ if (!values.has(required)) {
352
+ errors.push(`${at}: ocpp-connect block "${block.id}" has no "${required}" outcome; both results must be routed.`);
353
+ }
354
+ }
355
+ }
356
+ /** Every {@link EnyoOnboardingV2EebusPairOutcome} value. */
357
+ const EEBUS_PAIR_OUTCOMES = new Set(Object.values(enyo_onboarding_v2_js_1.EnyoOnboardingV2EebusPairOutcome));
358
+ /**
359
+ * Validates the outcomes of an {@link EnyoOnboardingV2ActionKind.EebusPair}
360
+ * block.
361
+ *
362
+ * The block reports one of three things — a peer was picked and the SHIP
363
+ * handshake came up, discovery found nothing, or the handshake failed — so its
364
+ * outcome `value`s are closed over {@link EnyoOnboardingV2EebusPairOutcome}.
365
+ * Anything else is an outcome that can never fire.
366
+ *
367
+ * The missing `paired` branch is a warning rather than an error: it strands
368
+ * every successful pairing, but an author staging a guide step by step may
369
+ * legitimately not have wired it yet.
370
+ *
371
+ * @param block - The eebus-pair action block being checked.
372
+ * @param at - Human-readable location prefix for messages.
373
+ * @param errors - Collector for blocking problems.
374
+ * @param warnings - Collector for advisory problems.
375
+ */
376
+ function validateEebusPairOutcomes(block, at, errors, warnings) {
377
+ const values = new Set();
378
+ for (const outcome of block.outcomes ?? []) {
379
+ if (!EEBUS_PAIR_OUTCOMES.has(outcome.value)) {
380
+ errors.push(`${at}: eebus-pair block "${block.id}" has outcome value "${outcome.value}", which is not an EnyoOnboardingV2EebusPairOutcome member.`);
381
+ }
382
+ else if (values.has(outcome.value)) {
383
+ errors.push(`${at}: eebus-pair block "${block.id}" wires outcome value "${outcome.value}" more than once.`);
384
+ }
385
+ values.add(outcome.value);
386
+ }
387
+ if (!values.has(enyo_onboarding_v2_js_1.EnyoOnboardingV2EebusPairOutcome.Paired)) {
388
+ warnings.push(`${at}: eebus-pair block "${block.id}" has no "${enyo_onboarding_v2_js_1.EnyoOnboardingV2EebusPairOutcome.Paired}" outcome — ` +
389
+ 'a successful pairing would have nowhere to go.');
390
+ }
391
+ }
392
+ /**
393
+ * Validates the auth blocks of a step.
394
+ *
395
+ * An auth block has exactly one handle, and the **server** decides when it
396
+ * fires — there is no failure branch to author, because a failed login keeps the
397
+ * installer on the step to retry. So the checks are about the handle existing and
398
+ * being routable at all, and about the step not pretending the login is optional:
399
+ * a second decision block next to it would offer a way past a gate the client is
400
+ * not allowed to skip.
401
+ *
402
+ * @param step - The step whose blocks are checked.
403
+ * @param at - Human-readable location prefix for messages.
404
+ * @param errors - Collector for blocking problems.
405
+ * @param warnings - Collector for advisory problems.
406
+ */
407
+ function validateAuthBlocks(step, at, errors, warnings) {
408
+ const authBlocks = (step.blocks ?? []).filter((b) => b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Auth);
409
+ for (const block of authBlocks) {
410
+ if (!block.label?.length)
411
+ errors.push(`${at}: auth block "${block.id}" has no label.`);
412
+ if (!block.outcome?.id) {
413
+ errors.push(`${at}: auth block "${block.id}" has no outcome id — its success handle cannot be routed.`);
414
+ }
415
+ if (!block.outcome?.label?.length) {
416
+ warnings.push(`${at}: auth block "${block.id}" outcome has no label.`);
417
+ }
418
+ }
419
+ if (authBlocks.length > 1) {
420
+ errors.push(`${at}: more than one auth block; a step can hold at most one login.`);
421
+ }
422
+ if (authBlocks.length === 1) {
423
+ const others = (step.blocks ?? []).filter((b) => b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Choice ||
424
+ b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Action ||
425
+ b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Input);
426
+ if (others.length) {
427
+ warnings.push(`${at}: auth block "${authBlocks[0].id}" shares the step with ${others.length} other decision block(s) — ` +
428
+ 'those give the installer a way past a login the server gates.');
429
+ }
430
+ }
431
+ }
432
+ /**
433
+ * Checks {@link EnyoOnboardingV2Guide.requiresNetworkScan} against what the guide
434
+ * actually does.
435
+ *
436
+ * Opting out means "don't search, start here" — right for a device that is never
437
+ * on the LAN (an OCPP wallbox), wrong if the guide then relies on scan results.
438
+ * Both mismatches are warnings, not errors: the flag describes the host's
439
+ * behaviour before the guide runs, and an author may have a reason.
440
+ *
441
+ * @param guide - The guide being validated.
442
+ * @param warnings - Collector for advisory problems.
443
+ */
444
+ function validateNetworkScanFlag(guide, warnings) {
445
+ if (guide.requiresNetworkScan !== false)
446
+ return;
447
+ const blocks = guide.steps.flatMap((s) => s.blocks ?? []);
448
+ const scansItself = blocks.some((b) => b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Action &&
449
+ b.action === enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.NetworkScan);
450
+ if (scansItself)
451
+ return;
452
+ const dependsOnDetected = blocks.some((b) => b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Action &&
453
+ b.action === enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.DeviceTest &&
454
+ (b.deviceSelection ?? enyo_onboarding_v2_js_1.EnyoOnboardingV2DeviceSelection.Detected) !==
455
+ enyo_onboarding_v2_js_1.EnyoOnboardingV2DeviceSelection.Current);
456
+ if (dependsOnDetected) {
457
+ warnings.push('requiresNetworkScan is false, but a device-test block selects from detected devices — ' +
458
+ 'nothing was scanned, so it has nothing to test. Use deviceSelection "current", or run a ' +
459
+ 'network-scan action inside the guide.');
460
+ }
461
+ const pairsEebus = blocks.some((b) => b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Action &&
462
+ b.action === enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.EebusPair);
463
+ if (pairsEebus) {
464
+ warnings.push('requiresNetworkScan is false, but an eebus-pair block asks the installer to pick a ' +
465
+ 'discovered EEBUS peer — nothing was scanned, so the picker would open on an empty ' +
466
+ 'list. Run a network-scan action inside the guide ahead of it.');
467
+ }
468
+ }
287
469
  /**
288
470
  * The set of routing-handle keys a step must wire exactly once: one per
289
471
  * choice option / action outcome / input outcome, or the single `continue`
@@ -303,6 +485,9 @@ function requiredHandleKeys(step) {
303
485
  for (const o of b.outcomes)
304
486
  keys.add(`outcome:${b.id}:${o.id}`);
305
487
  }
488
+ else if (b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Auth && b.outcome?.id) {
489
+ keys.add(`outcome:${b.id}:${b.outcome.id}`);
490
+ }
306
491
  }
307
492
  if (keys.size === 0)
308
493
  keys.add('continue');
@@ -4,8 +4,14 @@
4
4
  * energy app can fail fast locally before publishing.
5
5
  *
6
6
  * `errors` block publishing; `warnings` are advisory (e.g. unreachable step, no
7
- * path to success). Use {@link validateOnboardingGuideV2} for the non-throwing
8
- * result, or {@link assertValidOnboardingGuideV2} to throw on the first failure.
7
+ * path to a completing exit). Use {@link validateOnboardingGuideV2} for the
8
+ * non-throwing result, or {@link assertValidOnboardingGuideV2} to throw on the
9
+ * first failure.
10
+ *
11
+ * A run completes either through a `success` target **or** through the
12
+ * "enyo übernimmt" hand-off (`pause` with reason
13
+ * {@link EnyoOnboardingV2PauseReason.EnyoTodo}) — both count, so a guide that
14
+ * legitimately ends in a hand-off is not nagged about a missing success path.
9
15
  */
10
16
  import type { EnyoOnboardingV2Guide } from '../../types/enyo-onboarding-v2.cjs';
11
17
  /**
@@ -31,8 +37,8 @@ export interface OnboardingV2ValidationResult {
31
37
  }
32
38
  /**
33
39
  * Structural + plausibility validation of a v2 guide graph. `errors` block
34
- * publishing; `warnings` are advisory (e.g. unreachable step, no path to
35
- * success).
40
+ * publishing; `warnings` are advisory (e.g. unreachable step, no path to a
41
+ * completing exit — `success` or the `enyo-todo` hand-off).
36
42
  *
37
43
  * @param guide - The v2 guide to validate.
38
44
  * @returns The {@link OnboardingV2ValidationResult}.
@@ -117,6 +117,40 @@ class WallboxIntegrationEnergyApp extends integration_energy_app_js_1.Integratio
117
117
  };
118
118
  this.useDataBus().sendMessage([msg]);
119
119
  }
120
+ /**
121
+ * Publishes an `EnergyManagementChargingStateV1` message: the app's own
122
+ * state — the phase count in force, whether a phase switch is possible
123
+ * right now, the limit actually applied and the verdict on the last
124
+ * request — reported to the energy manager.
125
+ *
126
+ * This is a continuously updated state, not an event. Call it whenever
127
+ * anything in the payload changes (a phase switch starts or finishes, a
128
+ * cool-down expires, a new limit is applied) and periodically as a
129
+ * heartbeat, always passing the **complete** picture: an omitted field
130
+ * means "unknown", never "unchanged".
131
+ *
132
+ * Report facts, never advice: the phase count, the applied limit and the
133
+ * power it implies, the request verdict, the measurement timestamp — no
134
+ * recommended minimum power, no suggested setpoint, nothing derived from
135
+ * the energy manager's own plan.
136
+ *
137
+ * @param applianceId - The charger appliance the state describes.
138
+ * @param data - The full charging state; see
139
+ * {@link EnyoDataBusEnergyManagementChargingStateV1}.
140
+ */
141
+ publishEnergyManagementChargingState(applianceId, data) {
142
+ const msg = {
143
+ id: this.generateMessageId(),
144
+ type: 'message',
145
+ message: enyo_data_bus_value_js_1.EnyoDataBusMessageEnum.EnergyManagementChargingStateV1,
146
+ source: this.source,
147
+ applianceId,
148
+ timestampIso: new Date().toISOString(),
149
+ resolution: 'dynamic',
150
+ data
151
+ };
152
+ this.useDataBus().sendMessage([msg]);
153
+ }
120
154
  /**
121
155
  * Publishes a `ChargerStatusChangedV1` message reporting the current OCPP
122
156
  * status (Available, Preparing, Charging, …).