@enyo-energy/energy-app-sdk 0.0.187 → 0.0.188

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.
@@ -133,6 +133,51 @@ exports.onboardingV2Block = {
133
133
  outcomes,
134
134
  deviceSelection,
135
135
  }),
136
+ /**
137
+ * A link block: a fixed URL the installer opens or copies.
138
+ *
139
+ * Passive content — it produces no routing handle, so a step whose only
140
+ * non-content block is a link still routes through `continue`.
141
+ *
142
+ * @param id - Stable block id, unique within the guide.
143
+ * @param url - Absolute `http(s)` URL. Other schemes are rejected by the validator.
144
+ * @param label - Translated link text (de/en).
145
+ * @param opts - Optional translated `description` and `copyable` (defaults to `true`).
146
+ */
147
+ link: (id, url, label, opts) => ({
148
+ id,
149
+ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Link,
150
+ url,
151
+ label,
152
+ description: opts?.description,
153
+ copyable: opts?.copyable ?? true,
154
+ }),
155
+ /**
156
+ * An input block: the installer types a value and the host checks it,
157
+ * producing the branch.
158
+ *
159
+ * For {@link EnyoOnboardingV2InputValueType.IpAddress} the host runs this
160
+ * app's registered device-test handler against the typed address; see
161
+ * {@link EnyoOnboardingV2InputBlock} for how a verdict picks an outcome.
162
+ * Route the outcomes with {@link onOutcomeV2} — there is no separate helper.
163
+ *
164
+ * @param id - Stable block id, unique within the guide.
165
+ * @param valueType - What is asked for (`Text` | `IpAddress` | `Number`).
166
+ * @param label - Translated field label (de/en).
167
+ * @param submitLabel - Translated submit button text (de/en).
168
+ * @param outcomes - The possible verdicts; each is a routing handle.
169
+ * @param opts - Optional translated `placeholder` and `help`.
170
+ */
171
+ input: (id, valueType, label, submitLabel, outcomes, opts) => ({
172
+ id,
173
+ type: enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Input,
174
+ valueType,
175
+ label,
176
+ submitLabel,
177
+ outcomes,
178
+ placeholder: opts?.placeholder,
179
+ help: opts?.help,
180
+ }),
136
181
  };
137
182
  // ---------------------------------------------------------------------------
138
183
  // Target factories
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import type { EnyoOnboardingTranslatedContent } from '../../types/enyo-onboarding.cjs';
13
13
  import { EnyoOnboardingV2ActionKind, EnyoOnboardingV2ChoiceLayout, EnyoOnboardingV2DeviceSelection } from '../../types/enyo-onboarding-v2.cjs';
14
- import type { EnyoOnboardingV2ActionOutcome, EnyoOnboardingV2Block, EnyoOnboardingV2ChoiceOption, EnyoOnboardingV2DynamicKind, EnyoOnboardingV2Guide, EnyoOnboardingV2HintVariant, EnyoOnboardingV2PauseReason, EnyoOnboardingV2StartVariant, EnyoOnboardingV2Target, EnyoOnboardingV2Transition } 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';
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,41 @@ 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
+ * A link block: a fixed URL the installer opens or copies.
103
+ *
104
+ * Passive content — it produces no routing handle, so a step whose only
105
+ * non-content block is a link still routes through `continue`.
106
+ *
107
+ * @param id - Stable block id, unique within the guide.
108
+ * @param url - Absolute `http(s)` URL. Other schemes are rejected by the validator.
109
+ * @param label - Translated link text (de/en).
110
+ * @param opts - Optional translated `description` and `copyable` (defaults to `true`).
111
+ */
112
+ link: (id: string, url: string, label: EnyoOnboardingTranslatedContent[], opts?: {
113
+ description?: EnyoOnboardingTranslatedContent[];
114
+ copyable?: boolean;
115
+ }) => EnyoOnboardingV2Block;
116
+ /**
117
+ * An input block: the installer types a value and the host checks it,
118
+ * producing the branch.
119
+ *
120
+ * For {@link EnyoOnboardingV2InputValueType.IpAddress} the host runs this
121
+ * app's registered device-test handler against the typed address; see
122
+ * {@link EnyoOnboardingV2InputBlock} for how a verdict picks an outcome.
123
+ * Route the outcomes with {@link onOutcomeV2} — there is no separate helper.
124
+ *
125
+ * @param id - Stable block id, unique within the guide.
126
+ * @param valueType - What is asked for (`Text` | `IpAddress` | `Number`).
127
+ * @param label - Translated field label (de/en).
128
+ * @param submitLabel - Translated submit button text (de/en).
129
+ * @param outcomes - The possible verdicts; each is a routing handle.
130
+ * @param opts - Optional translated `placeholder` and `help`.
131
+ */
132
+ input: (id: string, valueType: EnyoOnboardingV2InputValueType, label: EnyoOnboardingTranslatedContent[], submitLabel: EnyoOnboardingTranslatedContent[], outcomes: EnyoOnboardingV2InputOutcome[], opts?: {
133
+ placeholder?: EnyoOnboardingTranslatedContent[];
134
+ help?: EnyoOnboardingTranslatedContent[];
135
+ }) => EnyoOnboardingV2Block;
101
136
  };
102
137
  /** Typed factories for each transition {@link EnyoOnboardingV2Target}. */
103
138
  export declare const onboardingV2Target: {
@@ -70,6 +70,8 @@ function validateOnboardingGuideV2(guide) {
70
70
  if (!step.blocks?.length)
71
71
  warnings.push(`${at}: no content blocks.`);
72
72
  validateActionBlocks(step, at, errors, warnings);
73
+ validateLinkBlocks(step, at, errors, warnings);
74
+ validateInputBlocks(step, at, errors, warnings);
73
75
  }
74
76
  if (!guide.startStepId || !stepIds.has(guide.startStepId)) {
75
77
  errors.push('`startStepId` must reference an existing step.');
@@ -175,10 +177,119 @@ function validateActionBlocks(step, at, errors, warnings) {
175
177
  }
176
178
  }
177
179
  }
180
+ /** An absolute `http(s)` URL — the only scheme a link block may carry. */
181
+ const HTTP_URL_RE = /^https?:\/\/\S+$/i;
182
+ /**
183
+ * Validates the link blocks of a step.
184
+ *
185
+ * The scheme check is **security-relevant, not cosmetic**: the installer app
186
+ * renders a link block as a tap target, so this keeps `javascript:`, `data:` and
187
+ * `file:` payloads out of it. connect-core enforces the identical rule
188
+ * server-side and the app re-checks before opening — three gates, deliberately.
189
+ * Do not relax it to a generic "looks like a URL" test.
190
+ *
191
+ * @param step - The step whose blocks are checked.
192
+ * @param at - Human-readable location prefix for messages.
193
+ * @param errors - Collector for blocking problems.
194
+ * @param warnings - Collector for advisory problems.
195
+ */
196
+ function validateLinkBlocks(step, at, errors, warnings) {
197
+ for (const block of step.blocks ?? []) {
198
+ if (block.type !== enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Link)
199
+ continue;
200
+ if (!block.url?.trim()) {
201
+ errors.push(`${at}: link block "${block.id}" has no url.`);
202
+ }
203
+ else if (!HTTP_URL_RE.test(block.url.trim())) {
204
+ errors.push(`${at}: link block "${block.id}" url must be an absolute http(s) URL.`);
205
+ }
206
+ if (!block.label?.length) {
207
+ warnings.push(`${at}: link block "${block.id}" has no label — the raw URL is shown instead.`);
208
+ }
209
+ }
210
+ }
211
+ /** Every valid {@link EnyoOnboardingV2InputValueType} value. */
212
+ 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']);
215
+ /**
216
+ * The {@link EnyoDeviceTestOutcomeEnum} verdicts that collapse onto a positive
217
+ * input outcome; every other verdict collapses onto a negative one.
218
+ */
219
+ const POSITIVE_DEVICE_TEST_OUTCOMES = new Set([
220
+ enyo_device_test_js_1.EnyoDeviceTestOutcomeEnum.AppliancesCreated,
221
+ enyo_device_test_js_1.EnyoDeviceTestOutcomeEnum.AppliancesAlreadyExisted,
222
+ enyo_device_test_js_1.EnyoDeviceTestOutcomeEnum.DeviceConfirmedNoAppliance,
223
+ ]);
224
+ /** True when `value` routes a successful check (exact verdict or binary key). */
225
+ function isPositiveInputOutcomeValue(value) {
226
+ return POSITIVE_INPUT_OUTCOMES.has(value) || POSITIVE_DEVICE_TEST_OUTCOMES.has(value);
227
+ }
228
+ /**
229
+ * Validates the input blocks of a step.
230
+ *
231
+ * An {@link EnyoOnboardingV2InputValueType.IpAddress} block is checked more
232
+ * strictly than the other value types because it is the only one the host
233
+ * actually runs a check for: a one-sided outcome set there is a step the
234
+ * installer can enter and never leave, so both "no positive" and "no negative"
235
+ * are errors rather than warnings. Unlike a device test there is no `failed`
236
+ * verdict the host can fall back to.
237
+ *
238
+ * @param step - The step whose blocks are checked.
239
+ * @param at - Human-readable location prefix for messages.
240
+ * @param errors - Collector for blocking problems.
241
+ * @param warnings - Collector for advisory problems.
242
+ */
243
+ function validateInputBlocks(step, at, errors, warnings) {
244
+ for (const block of step.blocks ?? []) {
245
+ if (block.type !== enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Input)
246
+ continue;
247
+ if (!INPUT_VALUE_TYPES.has(block.valueType)) {
248
+ errors.push(`${at}: input block "${block.id}" has an unknown valueType "${block.valueType}".`);
249
+ }
250
+ if (!block.label?.length)
251
+ errors.push(`${at}: input block "${block.id}" has no label.`);
252
+ if (!block.submitLabel?.length) {
253
+ errors.push(`${at}: input block "${block.id}" has no submitLabel.`);
254
+ }
255
+ const outcomes = block.outcomes ?? [];
256
+ if (outcomes.length < 2) {
257
+ errors.push(`${at}: input block "${block.id}" needs at least 2 outcomes.`);
258
+ }
259
+ const ids = new Set();
260
+ const values = new Set();
261
+ for (const outcome of outcomes) {
262
+ if (ids.has(outcome.id)) {
263
+ errors.push(`${at}: input block "${block.id}" has a duplicate outcome id "${outcome.id}".`);
264
+ }
265
+ ids.add(outcome.id);
266
+ if (values.has(outcome.value)) {
267
+ errors.push(`${at}: input block "${block.id}" wires outcome value "${outcome.value}" more than once.`);
268
+ }
269
+ values.add(outcome.value);
270
+ }
271
+ if (block.valueType === enyo_onboarding_v2_js_1.EnyoOnboardingV2InputValueType.IpAddress) {
272
+ if (![...values].some(isPositiveInputOutcomeValue)) {
273
+ errors.push(`${at}: input block "${block.id}" has no outcome for a successful check — ` +
274
+ `wire "reachable" (or an EnyoDeviceTestOutcomeEnum success member).`);
275
+ }
276
+ if (![...values].some((v) => !isPositiveInputOutcomeValue(v))) {
277
+ errors.push(`${at}: input block "${block.id}" has no outcome for a failed check — ` +
278
+ `the installer would be stranded when the device does not answer.`);
279
+ }
280
+ }
281
+ else if (outcomes.length > 1) {
282
+ warnings.push(`${at}: input block "${block.id}" has valueType "${block.valueType}", which runs no check — ` +
283
+ `only the positive outcome can ever fire, so its other ${outcomes.length - 1} outcome(s) are dead branches.`);
284
+ }
285
+ }
286
+ }
178
287
  /**
179
288
  * The set of routing-handle keys a step must wire exactly once: one per
180
- * choice option / action outcome, or the single `continue` handle when the step
181
- * has no interactive block.
289
+ * choice option / action outcome / input outcome, or the single `continue`
290
+ * handle when the step has no interactive block.
291
+ *
292
+ * A `Link` block contributes nothing — it is passive content.
182
293
  */
183
294
  function requiredHandleKeys(step) {
184
295
  const keys = new Set();
@@ -187,7 +298,8 @@ function requiredHandleKeys(step) {
187
298
  for (const o of b.options)
188
299
  keys.add(`choice:${b.id}:${o.id}`);
189
300
  }
190
- else if (b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Action) {
301
+ else if (b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Action ||
302
+ b.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Input) {
191
303
  for (const o of b.outcomes)
192
304
  keys.add(`outcome:${b.id}:${o.id}`);
193
305
  }
@@ -10,6 +10,12 @@ var EnyoAirConditioningApplianceAvailableFeaturesEnum;
10
10
  EnyoAirConditioningApplianceAvailableFeaturesEnum["Cooling"] = "Cooling";
11
11
  /** If the air conditioning unit supports heating */
12
12
  EnyoAirConditioningApplianceAvailableFeaturesEnum["Heating"] = "Heating";
13
+ /** If the air conditioning unit reports electrical power values (e.g. consumption in watts) */
14
+ EnyoAirConditioningApplianceAvailableFeaturesEnum["Power"] = "Power";
15
+ /** If the air conditioning unit's electrical power draw can be steered (e.g. limited to a target in watts) */
16
+ EnyoAirConditioningApplianceAvailableFeaturesEnum["PowerControllable"] = "PowerControllable";
17
+ /** If the air conditioning unit's target room temperature can be set */
18
+ EnyoAirConditioningApplianceAvailableFeaturesEnum["TemperatureControllable"] = "TemperatureControllable";
13
19
  })(EnyoAirConditioningApplianceAvailableFeaturesEnum || (exports.EnyoAirConditioningApplianceAvailableFeaturesEnum = EnyoAirConditioningApplianceAvailableFeaturesEnum = {}));
14
20
  /**
15
21
  * Energy-optimization modes for an air conditioning appliance. These control how
@@ -5,7 +5,13 @@ export declare enum EnyoAirConditioningApplianceAvailableFeaturesEnum {
5
5
  /** If the air conditioning unit supports cooling */
6
6
  Cooling = "Cooling",
7
7
  /** If the air conditioning unit supports heating */
8
- Heating = "Heating"
8
+ Heating = "Heating",
9
+ /** If the air conditioning unit reports electrical power values (e.g. consumption in watts) */
10
+ Power = "Power",
11
+ /** If the air conditioning unit's electrical power draw can be steered (e.g. limited to a target in watts) */
12
+ PowerControllable = "PowerControllable",
13
+ /** If the air conditioning unit's target room temperature can be set */
14
+ TemperatureControllable = "TemperatureControllable"
9
15
  }
10
16
  /**
11
17
  * Energy-optimization modes for an air conditioning appliance. These control how
@@ -11,6 +11,16 @@ var EnyoHeatingRodApplianceAvailableFeaturesEnum;
11
11
  EnyoHeatingRodApplianceAvailableFeaturesEnum["Power"] = "Power";
12
12
  /** If the heating rod supports available power announcements */
13
13
  EnyoHeatingRodApplianceAvailableFeaturesEnum["AvailablePowerAnnouncement"] = "AvailablePowerAnnouncement";
14
+ /**
15
+ * If the heating rod has a domestic hot water temperature sensor, i.e. it
16
+ * reports the measured DHW tank temperature rather than only its own
17
+ * operating state.
18
+ *
19
+ * Without this feature {@link EnyoHeatingRodApplianceMetadata.targetTemperatureC}
20
+ * is a setpoint the appliance cannot verify — consumers should not expect a
21
+ * measured temperature to compare it against.
22
+ */
23
+ EnyoHeatingRodApplianceAvailableFeaturesEnum["DomesticHotWaterSensor"] = "DomesticHotWaterSensor";
14
24
  })(EnyoHeatingRodApplianceAvailableFeaturesEnum || (exports.EnyoHeatingRodApplianceAvailableFeaturesEnum = EnyoHeatingRodApplianceAvailableFeaturesEnum = {}));
15
25
  /**
16
26
  * Operating modes for a heating rod appliance.
@@ -6,7 +6,17 @@ export declare enum EnyoHeatingRodApplianceAvailableFeaturesEnum {
6
6
  /** If the heating rod reports electrical power values (e.g. consumption in watts) */
7
7
  Power = "Power",
8
8
  /** If the heating rod supports available power announcements */
9
- AvailablePowerAnnouncement = "AvailablePowerAnnouncement"
9
+ AvailablePowerAnnouncement = "AvailablePowerAnnouncement",
10
+ /**
11
+ * If the heating rod has a domestic hot water temperature sensor, i.e. it
12
+ * reports the measured DHW tank temperature rather than only its own
13
+ * operating state.
14
+ *
15
+ * Without this feature {@link EnyoHeatingRodApplianceMetadata.targetTemperatureC}
16
+ * is a setpoint the appliance cannot verify — consumers should not expect a
17
+ * measured temperature to compare it against.
18
+ */
19
+ DomesticHotWaterSensor = "DomesticHotWaterSensor"
10
20
  }
11
21
  /**
12
22
  * Operating modes for a heating rod appliance.
@@ -22,7 +22,7 @@
22
22
  * before publishing.
23
23
  */
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.EnyoOnboardingV2TargetType = exports.EnyoOnboardingV2TransitionSourceKind = exports.EnyoOnboardingV2BlockType = exports.EnyoOnboardingV2ChoiceLayout = exports.EnyoOnboardingV2IconKey = exports.EnyoOnboardingV2DeviceSelection = exports.EnyoOnboardingV2ActionKind = exports.EnyoOnboardingV2DynamicKind = exports.EnyoOnboardingV2HintVariant = exports.EnyoOnboardingV2PauseReason = exports.EnyoOnboardingV2StartVariant = void 0;
25
+ exports.EnyoOnboardingV2TargetType = exports.EnyoOnboardingV2TransitionSourceKind = exports.EnyoOnboardingV2BlockType = exports.EnyoOnboardingV2ChoiceLayout = exports.EnyoOnboardingV2IconKey = exports.EnyoOnboardingV2DeviceSelection = exports.EnyoOnboardingV2InputValueType = exports.EnyoOnboardingV2ActionKind = exports.EnyoOnboardingV2DynamicKind = exports.EnyoOnboardingV2HintVariant = exports.EnyoOnboardingV2PauseReason = exports.EnyoOnboardingV2StartVariant = void 0;
26
26
  // ---------------------------------------------------------------------------
27
27
  // Enumerable string enums
28
28
  // ---------------------------------------------------------------------------
@@ -88,6 +88,19 @@ var EnyoOnboardingV2ActionKind;
88
88
  */
89
89
  EnyoOnboardingV2ActionKind["DeviceTest"] = "device-test";
90
90
  })(EnyoOnboardingV2ActionKind || (exports.EnyoOnboardingV2ActionKind = EnyoOnboardingV2ActionKind = {}));
91
+ /**
92
+ * What an {@link EnyoOnboardingV2InputBlock} asks the installer for. Drives the
93
+ * keyboard the app shows, the format check it applies and the seeded texts.
94
+ */
95
+ var EnyoOnboardingV2InputValueType;
96
+ (function (EnyoOnboardingV2InputValueType) {
97
+ /** Free text; any non-empty value is accepted. */
98
+ EnyoOnboardingV2InputValueType["Text"] = "text";
99
+ /** An IPv4 address. This is the only type the host actually checks. */
100
+ EnyoOnboardingV2InputValueType["IpAddress"] = "ip-address";
101
+ /** A number; `,` and `.` are both accepted as the decimal separator. */
102
+ EnyoOnboardingV2InputValueType["Number"] = "number";
103
+ })(EnyoOnboardingV2InputValueType || (exports.EnyoOnboardingV2InputValueType = EnyoOnboardingV2InputValueType = {}));
91
104
  /**
92
105
  * Which devices a {@link EnyoOnboardingV2ActionKind.DeviceTest} block hands to
93
106
  * the app.
@@ -129,6 +142,8 @@ var EnyoOnboardingV2BlockType;
129
142
  EnyoOnboardingV2BlockType["Dynamic"] = "dynamic";
130
143
  EnyoOnboardingV2BlockType["Choice"] = "choice";
131
144
  EnyoOnboardingV2BlockType["Action"] = "action";
145
+ EnyoOnboardingV2BlockType["Link"] = "link";
146
+ EnyoOnboardingV2BlockType["Input"] = "input";
132
147
  })(EnyoOnboardingV2BlockType || (exports.EnyoOnboardingV2BlockType = EnyoOnboardingV2BlockType = {}));
133
148
  // ---------------------------------------------------------------------------
134
149
  // Routing: transitions & targets
@@ -140,7 +155,7 @@ var EnyoOnboardingV2TransitionSourceKind;
140
155
  EnyoOnboardingV2TransitionSourceKind["Continue"] = "continue";
141
156
  /** A specific option of a `Choice` block was picked. */
142
157
  EnyoOnboardingV2TransitionSourceKind["Choice"] = "choice";
143
- /** A specific outcome of an `Action` block fired. */
158
+ /** A specific outcome of an `Action` or `Input` block fired. */
144
159
  EnyoOnboardingV2TransitionSourceKind["Outcome"] = "outcome";
145
160
  })(EnyoOnboardingV2TransitionSourceKind || (exports.EnyoOnboardingV2TransitionSourceKind = EnyoOnboardingV2TransitionSourceKind = {}));
146
161
  /** Discriminator for the {@link EnyoOnboardingV2Target} union. */
@@ -78,6 +78,18 @@ export declare enum EnyoOnboardingV2ActionKind {
78
78
  */
79
79
  DeviceTest = "device-test"
80
80
  }
81
+ /**
82
+ * What an {@link EnyoOnboardingV2InputBlock} asks the installer for. Drives the
83
+ * keyboard the app shows, the format check it applies and the seeded texts.
84
+ */
85
+ export declare enum EnyoOnboardingV2InputValueType {
86
+ /** Free text; any non-empty value is accepted. */
87
+ Text = "text",
88
+ /** An IPv4 address. This is the only type the host actually checks. */
89
+ IpAddress = "ip-address",
90
+ /** A number; `,` and `.` are both accepted as the decimal separator. */
91
+ Number = "number"
92
+ }
81
93
  /**
82
94
  * Which devices a {@link EnyoOnboardingV2ActionKind.DeviceTest} block hands to
83
95
  * the app.
@@ -111,7 +123,9 @@ export declare enum EnyoOnboardingV2BlockType {
111
123
  Hint = "hint",
112
124
  Dynamic = "dynamic",
113
125
  Choice = "choice",
114
- Action = "action"
126
+ Action = "action",
127
+ Link = "link",
128
+ Input = "input"
115
129
  }
116
130
  /** Fields shared by every content/interactive block. */
117
131
  export interface EnyoOnboardingV2BlockBase {
@@ -209,17 +223,139 @@ export interface EnyoOnboardingV2ActionBlock extends EnyoOnboardingV2BlockBase {
209
223
  */
210
224
  deviceSelection?: EnyoOnboardingV2DeviceSelection;
211
225
  }
226
+ /**
227
+ * A fixed URL the installer opens or copies — a vendor portal, a manual, a
228
+ * firmware download.
229
+ *
230
+ * Passive content: it produces no routing handle, so a step whose only
231
+ * non-content block is a link still routes through its single `continue`
232
+ * handle. Unlike {@link EnyoOnboardingV2DynamicBlock}, the URL is the same for
233
+ * every installer — nothing is resolved per device.
234
+ */
235
+ export interface EnyoOnboardingV2LinkBlock extends EnyoOnboardingV2BlockBase {
236
+ type: EnyoOnboardingV2BlockType.Link;
237
+ /**
238
+ * Absolute `http(s)` URL, not translated. Any other scheme is rejected by
239
+ * the validator and by the host: the installer app renders this as a tap
240
+ * target.
241
+ */
242
+ url: string;
243
+ /** Translated link text (de/en). Falls back to the raw URL when empty. */
244
+ label: EnyoOnboardingTranslatedContent[];
245
+ /** Optional translated context line below the link (de/en). */
246
+ description?: EnyoOnboardingTranslatedContent[];
247
+ /**
248
+ * Show a copy-to-clipboard button. Defaults to `true` — on a phone it is the
249
+ * more useful affordance of the two.
250
+ */
251
+ copyable?: boolean;
252
+ }
253
+ /**
254
+ * A possible verdict of an {@link EnyoOnboardingV2InputBlock}'s check; each
255
+ * outcome is a routing handle.
256
+ */
257
+ export interface EnyoOnboardingV2InputOutcome {
258
+ /** Stable id, unique within the block; referenced by a transition. */
259
+ id: string;
260
+ /**
261
+ * Semantic key, not translated. Either an {@link EnyoDeviceTestOutcomeEnum}
262
+ * member (routed exactly) or one of the binary keys the host collapses
263
+ * onto — `reachable` / `unreachable`. See {@link EnyoOnboardingV2InputBlock}
264
+ * for the full mapping.
265
+ */
266
+ value: string;
267
+ /** Translated display label for the verdict (de/en). */
268
+ label: EnyoOnboardingTranslatedContent[];
269
+ }
270
+ /**
271
+ * The installer types a value and the **host** checks it, producing the branch —
272
+ * unlike {@link EnyoOnboardingV2ChoiceBlock}, where the installer picks the
273
+ * branch directly.
274
+ *
275
+ * For {@link EnyoOnboardingV2InputValueType.IpAddress} the host pairs the typed
276
+ * address with this energy app (creating the network device when the scan never
277
+ * discovered it) and runs the app's registered {@link EnyoDeviceTestHandler}
278
+ * against it, so the verdict is the app's own — it confirms *your* device, not
279
+ * merely a live host. `Text` and `Number` are recorded and take the positive
280
+ * branch; there is nothing the host could check about them.
281
+ *
282
+ * Each outcome MUST have exactly one outgoing transition.
283
+ *
284
+ * **How a verdict picks an outcome**
285
+ *
286
+ * 1. *Exact match wins.* An outcome whose `value` is an
287
+ * {@link EnyoDeviceTestOutcomeEnum} member receives that verdict verbatim —
288
+ * so a guide can route `authentication-required` to a credentials step and
289
+ * `user-action-required` to a "press the pairing button" step.
290
+ * 2. *Otherwise it collapses to a binary pair.* `appliances-created`,
291
+ * `appliances-already-existed` and `device-confirmed-no-appliance` go to the
292
+ * first outcome valued `reachable` / `success` / `found`; everything else
293
+ * (`unreachable`, `not-supported`, `authentication-required`,
294
+ * `access-not-granted`, `user-action-required`, `failed`) goes to the first
295
+ * outcome valued `unreachable` / `failure` / `failed` / `not-found`.
296
+ * 3. `Text` and `Number` run no check: the value is recorded and the flow takes
297
+ * the positive outcome (first `reachable`/`success`/`found`, else the first
298
+ * outcome). Any further outcome on such a block can never fire.
299
+ * 4. An offline hub, a package that is not installed, or no energy app linked to
300
+ * the vendor/model all resolve to the **negative** branch — never an error.
301
+ * The guide always gets an outcome to route on.
302
+ *
303
+ * The typed value is persisted per block in the run state and handed back on
304
+ * resume/back, so a typo is corrected rather than retyped. Values may be secrets
305
+ * (a device password typed into a `Text` input): the host logs outcome keys
306
+ * only, never the value itself.
307
+ *
308
+ * @example
309
+ * ```ts
310
+ * onboardingV2Block.input(
311
+ * 'b3',
312
+ * EnyoOnboardingV2InputValueType.IpAddress,
313
+ * t('IP-Adresse des Geräts', 'Device IP address'),
314
+ * t('Gerät prüfen', 'Check device'),
315
+ * [
316
+ * {id: 'ok', value: 'reachable', label: t('Gerät erreichbar', 'Device reachable')},
317
+ * {
318
+ * id: 'auth',
319
+ * value: EnyoDeviceTestOutcomeEnum.AuthenticationRequired,
320
+ * label: t('Passwort nötig', 'Password required'),
321
+ * },
322
+ * {id: 'no', value: 'unreachable', label: t('Nicht erreichbar', 'Not reachable')},
323
+ * ],
324
+ * {placeholder: t('z. B. 192.168.1.42', 'e.g. 192.168.1.42')},
325
+ * );
326
+ * ```
327
+ */
328
+ export interface EnyoOnboardingV2InputBlock extends EnyoOnboardingV2BlockBase {
329
+ type: EnyoOnboardingV2BlockType.Input;
330
+ /** What is asked for; drives keyboard, format check and seeded texts. */
331
+ valueType: EnyoOnboardingV2InputValueType;
332
+ /** Translated field label, e.g. "IP-Adresse des Geräts" (de/en). */
333
+ label: EnyoOnboardingTranslatedContent[];
334
+ /** Optional translated placeholder, e.g. "z. B. 192.168.1.42" (de/en). */
335
+ placeholder?: EnyoOnboardingTranslatedContent[];
336
+ /** Optional translated help text — where the installer finds the value (de/en). */
337
+ help?: EnyoOnboardingTranslatedContent[];
338
+ /** Translated submit button text, e.g. "Gerät prüfen" (de/en). */
339
+ submitLabel: EnyoOnboardingTranslatedContent[];
340
+ /** The possible verdicts; each is a routing handle. At least 2. */
341
+ outcomes: EnyoOnboardingV2InputOutcome[];
342
+ }
212
343
  /** Any block that can appear in a step's `blocks`. */
213
- export type EnyoOnboardingV2Block = EnyoOnboardingV2TextBlock | EnyoOnboardingV2HeadlineBlock | EnyoOnboardingV2BulletsBlock | EnyoOnboardingV2ImageBlock | EnyoOnboardingV2HintBlock | EnyoOnboardingV2DynamicBlock | EnyoOnboardingV2ChoiceBlock | EnyoOnboardingV2ActionBlock;
214
- /** Blocks that produce routing handles (a step's decision points). */
215
- export type EnyoOnboardingV2InteractiveBlock = EnyoOnboardingV2ChoiceBlock | EnyoOnboardingV2ActionBlock;
344
+ export type EnyoOnboardingV2Block = EnyoOnboardingV2TextBlock | EnyoOnboardingV2HeadlineBlock | EnyoOnboardingV2BulletsBlock | EnyoOnboardingV2ImageBlock | EnyoOnboardingV2HintBlock | EnyoOnboardingV2DynamicBlock | EnyoOnboardingV2ChoiceBlock | EnyoOnboardingV2ActionBlock | EnyoOnboardingV2LinkBlock | EnyoOnboardingV2InputBlock;
345
+ /**
346
+ * Blocks that produce routing handles (a step's decision points).
347
+ *
348
+ * A {@link EnyoOnboardingV2LinkBlock} is deliberately absent: it is passive
349
+ * content and routes nothing.
350
+ */
351
+ export type EnyoOnboardingV2InteractiveBlock = EnyoOnboardingV2ChoiceBlock | EnyoOnboardingV2ActionBlock | EnyoOnboardingV2InputBlock;
216
352
  /** Discriminator for the {@link EnyoOnboardingV2TransitionSource} union. */
217
353
  export declare enum EnyoOnboardingV2TransitionSourceKind {
218
354
  /** The step's plain "continue" button (steps with no interactive block). */
219
355
  Continue = "continue",
220
356
  /** A specific option of a `Choice` block was picked. */
221
357
  Choice = "choice",
222
- /** A specific outcome of an `Action` block fired. */
358
+ /** A specific outcome of an `Action` or `Input` block fired. */
223
359
  Outcome = "outcome"
224
360
  }
225
361
  /** Where a transition leaves from within a step. */
@@ -234,7 +370,7 @@ export type EnyoOnboardingV2TransitionSource =
234
370
  blockId: string;
235
371
  optionId: string;
236
372
  }
237
- /** A specific outcome of an `Action` block fired. */
373
+ /** A specific outcome of an `Action` or `Input` block fired. */
238
374
  | {
239
375
  kind: EnyoOnboardingV2TransitionSourceKind.Outcome;
240
376
  blockId: string;
@@ -9,7 +9,7 @@ exports.getSdkVersion = getSdkVersion;
9
9
  /**
10
10
  * Current version of the enyo Energy App SDK.
11
11
  */
12
- exports.SDK_VERSION = '0.0.187';
12
+ exports.SDK_VERSION = '0.0.188';
13
13
  /**
14
14
  * Gets the current SDK version.
15
15
  * @returns The semantic version string of the SDK
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export declare const SDK_VERSION = "0.0.187";
8
+ export declare const SDK_VERSION = "0.0.188";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import type { EnyoOnboardingTranslatedContent } from '../../types/enyo-onboarding.js';
13
13
  import { EnyoOnboardingV2ActionKind, EnyoOnboardingV2ChoiceLayout, EnyoOnboardingV2DeviceSelection } from '../../types/enyo-onboarding-v2.js';
14
- import type { EnyoOnboardingV2ActionOutcome, EnyoOnboardingV2Block, EnyoOnboardingV2ChoiceOption, EnyoOnboardingV2DynamicKind, EnyoOnboardingV2Guide, EnyoOnboardingV2HintVariant, EnyoOnboardingV2PauseReason, EnyoOnboardingV2StartVariant, EnyoOnboardingV2Target, EnyoOnboardingV2Transition } from '../../types/enyo-onboarding-v2.js';
14
+ import type { EnyoOnboardingV2ActionOutcome, EnyoOnboardingV2Block, EnyoOnboardingV2ChoiceOption, EnyoOnboardingV2DynamicKind, EnyoOnboardingV2Guide, EnyoOnboardingV2HintVariant, EnyoOnboardingV2InputOutcome, EnyoOnboardingV2InputValueType, EnyoOnboardingV2PauseReason, EnyoOnboardingV2StartVariant, EnyoOnboardingV2Target, EnyoOnboardingV2Transition } from '../../types/enyo-onboarding-v2.js';
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,41 @@ 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
+ * A link block: a fixed URL the installer opens or copies.
103
+ *
104
+ * Passive content — it produces no routing handle, so a step whose only
105
+ * non-content block is a link still routes through `continue`.
106
+ *
107
+ * @param id - Stable block id, unique within the guide.
108
+ * @param url - Absolute `http(s)` URL. Other schemes are rejected by the validator.
109
+ * @param label - Translated link text (de/en).
110
+ * @param opts - Optional translated `description` and `copyable` (defaults to `true`).
111
+ */
112
+ link: (id: string, url: string, label: EnyoOnboardingTranslatedContent[], opts?: {
113
+ description?: EnyoOnboardingTranslatedContent[];
114
+ copyable?: boolean;
115
+ }) => EnyoOnboardingV2Block;
116
+ /**
117
+ * An input block: the installer types a value and the host checks it,
118
+ * producing the branch.
119
+ *
120
+ * For {@link EnyoOnboardingV2InputValueType.IpAddress} the host runs this
121
+ * app's registered device-test handler against the typed address; see
122
+ * {@link EnyoOnboardingV2InputBlock} for how a verdict picks an outcome.
123
+ * Route the outcomes with {@link onOutcomeV2} — there is no separate helper.
124
+ *
125
+ * @param id - Stable block id, unique within the guide.
126
+ * @param valueType - What is asked for (`Text` | `IpAddress` | `Number`).
127
+ * @param label - Translated field label (de/en).
128
+ * @param submitLabel - Translated submit button text (de/en).
129
+ * @param outcomes - The possible verdicts; each is a routing handle.
130
+ * @param opts - Optional translated `placeholder` and `help`.
131
+ */
132
+ input: (id: string, valueType: EnyoOnboardingV2InputValueType, label: EnyoOnboardingTranslatedContent[], submitLabel: EnyoOnboardingTranslatedContent[], outcomes: EnyoOnboardingV2InputOutcome[], opts?: {
133
+ placeholder?: EnyoOnboardingTranslatedContent[];
134
+ help?: EnyoOnboardingTranslatedContent[];
135
+ }) => EnyoOnboardingV2Block;
101
136
  };
102
137
  /** Typed factories for each transition {@link EnyoOnboardingV2Target}. */
103
138
  export declare const onboardingV2Target: {
@@ -126,6 +126,51 @@ export const onboardingV2Block = {
126
126
  outcomes,
127
127
  deviceSelection,
128
128
  }),
129
+ /**
130
+ * A link block: a fixed URL the installer opens or copies.
131
+ *
132
+ * Passive content — it produces no routing handle, so a step whose only
133
+ * non-content block is a link still routes through `continue`.
134
+ *
135
+ * @param id - Stable block id, unique within the guide.
136
+ * @param url - Absolute `http(s)` URL. Other schemes are rejected by the validator.
137
+ * @param label - Translated link text (de/en).
138
+ * @param opts - Optional translated `description` and `copyable` (defaults to `true`).
139
+ */
140
+ link: (id, url, label, opts) => ({
141
+ id,
142
+ type: EnyoOnboardingV2BlockType.Link,
143
+ url,
144
+ label,
145
+ description: opts?.description,
146
+ copyable: opts?.copyable ?? true,
147
+ }),
148
+ /**
149
+ * An input block: the installer types a value and the host checks it,
150
+ * producing the branch.
151
+ *
152
+ * For {@link EnyoOnboardingV2InputValueType.IpAddress} the host runs this
153
+ * app's registered device-test handler against the typed address; see
154
+ * {@link EnyoOnboardingV2InputBlock} for how a verdict picks an outcome.
155
+ * Route the outcomes with {@link onOutcomeV2} — there is no separate helper.
156
+ *
157
+ * @param id - Stable block id, unique within the guide.
158
+ * @param valueType - What is asked for (`Text` | `IpAddress` | `Number`).
159
+ * @param label - Translated field label (de/en).
160
+ * @param submitLabel - Translated submit button text (de/en).
161
+ * @param outcomes - The possible verdicts; each is a routing handle.
162
+ * @param opts - Optional translated `placeholder` and `help`.
163
+ */
164
+ input: (id, valueType, label, submitLabel, outcomes, opts) => ({
165
+ id,
166
+ type: EnyoOnboardingV2BlockType.Input,
167
+ valueType,
168
+ label,
169
+ submitLabel,
170
+ outcomes,
171
+ placeholder: opts?.placeholder,
172
+ help: opts?.help,
173
+ }),
129
174
  };
130
175
  // ---------------------------------------------------------------------------
131
176
  // Target factories
@@ -7,7 +7,7 @@
7
7
  * path to success). Use {@link validateOnboardingGuideV2} for the non-throwing
8
8
  * result, or {@link assertValidOnboardingGuideV2} to throw on the first failure.
9
9
  */
10
- import { EnyoOnboardingV2ActionKind, EnyoOnboardingV2BlockType, EnyoOnboardingV2TargetType, EnyoOnboardingV2TransitionSourceKind, } from '../../types/enyo-onboarding-v2.js';
10
+ import { EnyoOnboardingV2ActionKind, EnyoOnboardingV2BlockType, EnyoOnboardingV2InputValueType, EnyoOnboardingV2TargetType, EnyoOnboardingV2TransitionSourceKind, } from '../../types/enyo-onboarding-v2.js';
11
11
  import { EnyoDeviceTestOutcomeEnum } from '../../types/enyo-device-test.js';
12
12
  /**
13
13
  * Thrown by {@link assertValidOnboardingGuideV2} when a guide fails validation.
@@ -64,6 +64,8 @@ export function validateOnboardingGuideV2(guide) {
64
64
  if (!step.blocks?.length)
65
65
  warnings.push(`${at}: no content blocks.`);
66
66
  validateActionBlocks(step, at, errors, warnings);
67
+ validateLinkBlocks(step, at, errors, warnings);
68
+ validateInputBlocks(step, at, errors, warnings);
67
69
  }
68
70
  if (!guide.startStepId || !stepIds.has(guide.startStepId)) {
69
71
  errors.push('`startStepId` must reference an existing step.');
@@ -169,10 +171,119 @@ function validateActionBlocks(step, at, errors, warnings) {
169
171
  }
170
172
  }
171
173
  }
174
+ /** An absolute `http(s)` URL — the only scheme a link block may carry. */
175
+ const HTTP_URL_RE = /^https?:\/\/\S+$/i;
176
+ /**
177
+ * Validates the link blocks of a step.
178
+ *
179
+ * The scheme check is **security-relevant, not cosmetic**: the installer app
180
+ * renders a link block as a tap target, so this keeps `javascript:`, `data:` and
181
+ * `file:` payloads out of it. connect-core enforces the identical rule
182
+ * server-side and the app re-checks before opening — three gates, deliberately.
183
+ * Do not relax it to a generic "looks like a URL" test.
184
+ *
185
+ * @param step - The step whose blocks are checked.
186
+ * @param at - Human-readable location prefix for messages.
187
+ * @param errors - Collector for blocking problems.
188
+ * @param warnings - Collector for advisory problems.
189
+ */
190
+ function validateLinkBlocks(step, at, errors, warnings) {
191
+ for (const block of step.blocks ?? []) {
192
+ if (block.type !== EnyoOnboardingV2BlockType.Link)
193
+ continue;
194
+ if (!block.url?.trim()) {
195
+ errors.push(`${at}: link block "${block.id}" has no url.`);
196
+ }
197
+ else if (!HTTP_URL_RE.test(block.url.trim())) {
198
+ errors.push(`${at}: link block "${block.id}" url must be an absolute http(s) URL.`);
199
+ }
200
+ if (!block.label?.length) {
201
+ warnings.push(`${at}: link block "${block.id}" has no label — the raw URL is shown instead.`);
202
+ }
203
+ }
204
+ }
205
+ /** Every valid {@link EnyoOnboardingV2InputValueType} value. */
206
+ const INPUT_VALUE_TYPES = new Set(Object.values(EnyoOnboardingV2InputValueType));
207
+ /** Outcome values the host treats as "the check succeeded". */
208
+ const POSITIVE_INPUT_OUTCOMES = new Set(['reachable', 'success', 'found']);
209
+ /**
210
+ * The {@link EnyoDeviceTestOutcomeEnum} verdicts that collapse onto a positive
211
+ * input outcome; every other verdict collapses onto a negative one.
212
+ */
213
+ const POSITIVE_DEVICE_TEST_OUTCOMES = new Set([
214
+ EnyoDeviceTestOutcomeEnum.AppliancesCreated,
215
+ EnyoDeviceTestOutcomeEnum.AppliancesAlreadyExisted,
216
+ EnyoDeviceTestOutcomeEnum.DeviceConfirmedNoAppliance,
217
+ ]);
218
+ /** True when `value` routes a successful check (exact verdict or binary key). */
219
+ function isPositiveInputOutcomeValue(value) {
220
+ return POSITIVE_INPUT_OUTCOMES.has(value) || POSITIVE_DEVICE_TEST_OUTCOMES.has(value);
221
+ }
222
+ /**
223
+ * Validates the input blocks of a step.
224
+ *
225
+ * An {@link EnyoOnboardingV2InputValueType.IpAddress} block is checked more
226
+ * strictly than the other value types because it is the only one the host
227
+ * actually runs a check for: a one-sided outcome set there is a step the
228
+ * installer can enter and never leave, so both "no positive" and "no negative"
229
+ * are errors rather than warnings. Unlike a device test there is no `failed`
230
+ * verdict the host can fall back to.
231
+ *
232
+ * @param step - The step whose blocks are checked.
233
+ * @param at - Human-readable location prefix for messages.
234
+ * @param errors - Collector for blocking problems.
235
+ * @param warnings - Collector for advisory problems.
236
+ */
237
+ function validateInputBlocks(step, at, errors, warnings) {
238
+ for (const block of step.blocks ?? []) {
239
+ if (block.type !== EnyoOnboardingV2BlockType.Input)
240
+ continue;
241
+ if (!INPUT_VALUE_TYPES.has(block.valueType)) {
242
+ errors.push(`${at}: input block "${block.id}" has an unknown valueType "${block.valueType}".`);
243
+ }
244
+ if (!block.label?.length)
245
+ errors.push(`${at}: input block "${block.id}" has no label.`);
246
+ if (!block.submitLabel?.length) {
247
+ errors.push(`${at}: input block "${block.id}" has no submitLabel.`);
248
+ }
249
+ const outcomes = block.outcomes ?? [];
250
+ if (outcomes.length < 2) {
251
+ errors.push(`${at}: input block "${block.id}" needs at least 2 outcomes.`);
252
+ }
253
+ const ids = new Set();
254
+ const values = new Set();
255
+ for (const outcome of outcomes) {
256
+ if (ids.has(outcome.id)) {
257
+ errors.push(`${at}: input block "${block.id}" has a duplicate outcome id "${outcome.id}".`);
258
+ }
259
+ ids.add(outcome.id);
260
+ if (values.has(outcome.value)) {
261
+ errors.push(`${at}: input block "${block.id}" wires outcome value "${outcome.value}" more than once.`);
262
+ }
263
+ values.add(outcome.value);
264
+ }
265
+ if (block.valueType === EnyoOnboardingV2InputValueType.IpAddress) {
266
+ if (![...values].some(isPositiveInputOutcomeValue)) {
267
+ errors.push(`${at}: input block "${block.id}" has no outcome for a successful check — ` +
268
+ `wire "reachable" (or an EnyoDeviceTestOutcomeEnum success member).`);
269
+ }
270
+ if (![...values].some((v) => !isPositiveInputOutcomeValue(v))) {
271
+ errors.push(`${at}: input block "${block.id}" has no outcome for a failed check — ` +
272
+ `the installer would be stranded when the device does not answer.`);
273
+ }
274
+ }
275
+ else if (outcomes.length > 1) {
276
+ warnings.push(`${at}: input block "${block.id}" has valueType "${block.valueType}", which runs no check — ` +
277
+ `only the positive outcome can ever fire, so its other ${outcomes.length - 1} outcome(s) are dead branches.`);
278
+ }
279
+ }
280
+ }
172
281
  /**
173
282
  * The set of routing-handle keys a step must wire exactly once: one per
174
- * choice option / action outcome, or the single `continue` handle when the step
175
- * has no interactive block.
283
+ * choice option / action outcome / input outcome, or the single `continue`
284
+ * handle when the step has no interactive block.
285
+ *
286
+ * A `Link` block contributes nothing — it is passive content.
176
287
  */
177
288
  function requiredHandleKeys(step) {
178
289
  const keys = new Set();
@@ -181,7 +292,8 @@ function requiredHandleKeys(step) {
181
292
  for (const o of b.options)
182
293
  keys.add(`choice:${b.id}:${o.id}`);
183
294
  }
184
- else if (b.type === EnyoOnboardingV2BlockType.Action) {
295
+ else if (b.type === EnyoOnboardingV2BlockType.Action ||
296
+ b.type === EnyoOnboardingV2BlockType.Input) {
185
297
  for (const o of b.outcomes)
186
298
  keys.add(`outcome:${b.id}:${o.id}`);
187
299
  }
@@ -5,7 +5,13 @@ export declare enum EnyoAirConditioningApplianceAvailableFeaturesEnum {
5
5
  /** If the air conditioning unit supports cooling */
6
6
  Cooling = "Cooling",
7
7
  /** If the air conditioning unit supports heating */
8
- Heating = "Heating"
8
+ Heating = "Heating",
9
+ /** If the air conditioning unit reports electrical power values (e.g. consumption in watts) */
10
+ Power = "Power",
11
+ /** If the air conditioning unit's electrical power draw can be steered (e.g. limited to a target in watts) */
12
+ PowerControllable = "PowerControllable",
13
+ /** If the air conditioning unit's target room temperature can be set */
14
+ TemperatureControllable = "TemperatureControllable"
9
15
  }
10
16
  /**
11
17
  * Energy-optimization modes for an air conditioning appliance. These control how
@@ -7,6 +7,12 @@ export var EnyoAirConditioningApplianceAvailableFeaturesEnum;
7
7
  EnyoAirConditioningApplianceAvailableFeaturesEnum["Cooling"] = "Cooling";
8
8
  /** If the air conditioning unit supports heating */
9
9
  EnyoAirConditioningApplianceAvailableFeaturesEnum["Heating"] = "Heating";
10
+ /** If the air conditioning unit reports electrical power values (e.g. consumption in watts) */
11
+ EnyoAirConditioningApplianceAvailableFeaturesEnum["Power"] = "Power";
12
+ /** If the air conditioning unit's electrical power draw can be steered (e.g. limited to a target in watts) */
13
+ EnyoAirConditioningApplianceAvailableFeaturesEnum["PowerControllable"] = "PowerControllable";
14
+ /** If the air conditioning unit's target room temperature can be set */
15
+ EnyoAirConditioningApplianceAvailableFeaturesEnum["TemperatureControllable"] = "TemperatureControllable";
10
16
  })(EnyoAirConditioningApplianceAvailableFeaturesEnum || (EnyoAirConditioningApplianceAvailableFeaturesEnum = {}));
11
17
  /**
12
18
  * Energy-optimization modes for an air conditioning appliance. These control how
@@ -6,7 +6,17 @@ export declare enum EnyoHeatingRodApplianceAvailableFeaturesEnum {
6
6
  /** If the heating rod reports electrical power values (e.g. consumption in watts) */
7
7
  Power = "Power",
8
8
  /** If the heating rod supports available power announcements */
9
- AvailablePowerAnnouncement = "AvailablePowerAnnouncement"
9
+ AvailablePowerAnnouncement = "AvailablePowerAnnouncement",
10
+ /**
11
+ * If the heating rod has a domestic hot water temperature sensor, i.e. it
12
+ * reports the measured DHW tank temperature rather than only its own
13
+ * operating state.
14
+ *
15
+ * Without this feature {@link EnyoHeatingRodApplianceMetadata.targetTemperatureC}
16
+ * is a setpoint the appliance cannot verify — consumers should not expect a
17
+ * measured temperature to compare it against.
18
+ */
19
+ DomesticHotWaterSensor = "DomesticHotWaterSensor"
10
20
  }
11
21
  /**
12
22
  * Operating modes for a heating rod appliance.
@@ -8,6 +8,16 @@ export var EnyoHeatingRodApplianceAvailableFeaturesEnum;
8
8
  EnyoHeatingRodApplianceAvailableFeaturesEnum["Power"] = "Power";
9
9
  /** If the heating rod supports available power announcements */
10
10
  EnyoHeatingRodApplianceAvailableFeaturesEnum["AvailablePowerAnnouncement"] = "AvailablePowerAnnouncement";
11
+ /**
12
+ * If the heating rod has a domestic hot water temperature sensor, i.e. it
13
+ * reports the measured DHW tank temperature rather than only its own
14
+ * operating state.
15
+ *
16
+ * Without this feature {@link EnyoHeatingRodApplianceMetadata.targetTemperatureC}
17
+ * is a setpoint the appliance cannot verify — consumers should not expect a
18
+ * measured temperature to compare it against.
19
+ */
20
+ EnyoHeatingRodApplianceAvailableFeaturesEnum["DomesticHotWaterSensor"] = "DomesticHotWaterSensor";
11
21
  })(EnyoHeatingRodApplianceAvailableFeaturesEnum || (EnyoHeatingRodApplianceAvailableFeaturesEnum = {}));
12
22
  /**
13
23
  * Operating modes for a heating rod appliance.
@@ -78,6 +78,18 @@ export declare enum EnyoOnboardingV2ActionKind {
78
78
  */
79
79
  DeviceTest = "device-test"
80
80
  }
81
+ /**
82
+ * What an {@link EnyoOnboardingV2InputBlock} asks the installer for. Drives the
83
+ * keyboard the app shows, the format check it applies and the seeded texts.
84
+ */
85
+ export declare enum EnyoOnboardingV2InputValueType {
86
+ /** Free text; any non-empty value is accepted. */
87
+ Text = "text",
88
+ /** An IPv4 address. This is the only type the host actually checks. */
89
+ IpAddress = "ip-address",
90
+ /** A number; `,` and `.` are both accepted as the decimal separator. */
91
+ Number = "number"
92
+ }
81
93
  /**
82
94
  * Which devices a {@link EnyoOnboardingV2ActionKind.DeviceTest} block hands to
83
95
  * the app.
@@ -111,7 +123,9 @@ export declare enum EnyoOnboardingV2BlockType {
111
123
  Hint = "hint",
112
124
  Dynamic = "dynamic",
113
125
  Choice = "choice",
114
- Action = "action"
126
+ Action = "action",
127
+ Link = "link",
128
+ Input = "input"
115
129
  }
116
130
  /** Fields shared by every content/interactive block. */
117
131
  export interface EnyoOnboardingV2BlockBase {
@@ -209,17 +223,139 @@ export interface EnyoOnboardingV2ActionBlock extends EnyoOnboardingV2BlockBase {
209
223
  */
210
224
  deviceSelection?: EnyoOnboardingV2DeviceSelection;
211
225
  }
226
+ /**
227
+ * A fixed URL the installer opens or copies — a vendor portal, a manual, a
228
+ * firmware download.
229
+ *
230
+ * Passive content: it produces no routing handle, so a step whose only
231
+ * non-content block is a link still routes through its single `continue`
232
+ * handle. Unlike {@link EnyoOnboardingV2DynamicBlock}, the URL is the same for
233
+ * every installer — nothing is resolved per device.
234
+ */
235
+ export interface EnyoOnboardingV2LinkBlock extends EnyoOnboardingV2BlockBase {
236
+ type: EnyoOnboardingV2BlockType.Link;
237
+ /**
238
+ * Absolute `http(s)` URL, not translated. Any other scheme is rejected by
239
+ * the validator and by the host: the installer app renders this as a tap
240
+ * target.
241
+ */
242
+ url: string;
243
+ /** Translated link text (de/en). Falls back to the raw URL when empty. */
244
+ label: EnyoOnboardingTranslatedContent[];
245
+ /** Optional translated context line below the link (de/en). */
246
+ description?: EnyoOnboardingTranslatedContent[];
247
+ /**
248
+ * Show a copy-to-clipboard button. Defaults to `true` — on a phone it is the
249
+ * more useful affordance of the two.
250
+ */
251
+ copyable?: boolean;
252
+ }
253
+ /**
254
+ * A possible verdict of an {@link EnyoOnboardingV2InputBlock}'s check; each
255
+ * outcome is a routing handle.
256
+ */
257
+ export interface EnyoOnboardingV2InputOutcome {
258
+ /** Stable id, unique within the block; referenced by a transition. */
259
+ id: string;
260
+ /**
261
+ * Semantic key, not translated. Either an {@link EnyoDeviceTestOutcomeEnum}
262
+ * member (routed exactly) or one of the binary keys the host collapses
263
+ * onto — `reachable` / `unreachable`. See {@link EnyoOnboardingV2InputBlock}
264
+ * for the full mapping.
265
+ */
266
+ value: string;
267
+ /** Translated display label for the verdict (de/en). */
268
+ label: EnyoOnboardingTranslatedContent[];
269
+ }
270
+ /**
271
+ * The installer types a value and the **host** checks it, producing the branch —
272
+ * unlike {@link EnyoOnboardingV2ChoiceBlock}, where the installer picks the
273
+ * branch directly.
274
+ *
275
+ * For {@link EnyoOnboardingV2InputValueType.IpAddress} the host pairs the typed
276
+ * address with this energy app (creating the network device when the scan never
277
+ * discovered it) and runs the app's registered {@link EnyoDeviceTestHandler}
278
+ * against it, so the verdict is the app's own — it confirms *your* device, not
279
+ * merely a live host. `Text` and `Number` are recorded and take the positive
280
+ * branch; there is nothing the host could check about them.
281
+ *
282
+ * Each outcome MUST have exactly one outgoing transition.
283
+ *
284
+ * **How a verdict picks an outcome**
285
+ *
286
+ * 1. *Exact match wins.* An outcome whose `value` is an
287
+ * {@link EnyoDeviceTestOutcomeEnum} member receives that verdict verbatim —
288
+ * so a guide can route `authentication-required` to a credentials step and
289
+ * `user-action-required` to a "press the pairing button" step.
290
+ * 2. *Otherwise it collapses to a binary pair.* `appliances-created`,
291
+ * `appliances-already-existed` and `device-confirmed-no-appliance` go to the
292
+ * first outcome valued `reachable` / `success` / `found`; everything else
293
+ * (`unreachable`, `not-supported`, `authentication-required`,
294
+ * `access-not-granted`, `user-action-required`, `failed`) goes to the first
295
+ * outcome valued `unreachable` / `failure` / `failed` / `not-found`.
296
+ * 3. `Text` and `Number` run no check: the value is recorded and the flow takes
297
+ * the positive outcome (first `reachable`/`success`/`found`, else the first
298
+ * outcome). Any further outcome on such a block can never fire.
299
+ * 4. An offline hub, a package that is not installed, or no energy app linked to
300
+ * the vendor/model all resolve to the **negative** branch — never an error.
301
+ * The guide always gets an outcome to route on.
302
+ *
303
+ * The typed value is persisted per block in the run state and handed back on
304
+ * resume/back, so a typo is corrected rather than retyped. Values may be secrets
305
+ * (a device password typed into a `Text` input): the host logs outcome keys
306
+ * only, never the value itself.
307
+ *
308
+ * @example
309
+ * ```ts
310
+ * onboardingV2Block.input(
311
+ * 'b3',
312
+ * EnyoOnboardingV2InputValueType.IpAddress,
313
+ * t('IP-Adresse des Geräts', 'Device IP address'),
314
+ * t('Gerät prüfen', 'Check device'),
315
+ * [
316
+ * {id: 'ok', value: 'reachable', label: t('Gerät erreichbar', 'Device reachable')},
317
+ * {
318
+ * id: 'auth',
319
+ * value: EnyoDeviceTestOutcomeEnum.AuthenticationRequired,
320
+ * label: t('Passwort nötig', 'Password required'),
321
+ * },
322
+ * {id: 'no', value: 'unreachable', label: t('Nicht erreichbar', 'Not reachable')},
323
+ * ],
324
+ * {placeholder: t('z. B. 192.168.1.42', 'e.g. 192.168.1.42')},
325
+ * );
326
+ * ```
327
+ */
328
+ export interface EnyoOnboardingV2InputBlock extends EnyoOnboardingV2BlockBase {
329
+ type: EnyoOnboardingV2BlockType.Input;
330
+ /** What is asked for; drives keyboard, format check and seeded texts. */
331
+ valueType: EnyoOnboardingV2InputValueType;
332
+ /** Translated field label, e.g. "IP-Adresse des Geräts" (de/en). */
333
+ label: EnyoOnboardingTranslatedContent[];
334
+ /** Optional translated placeholder, e.g. "z. B. 192.168.1.42" (de/en). */
335
+ placeholder?: EnyoOnboardingTranslatedContent[];
336
+ /** Optional translated help text — where the installer finds the value (de/en). */
337
+ help?: EnyoOnboardingTranslatedContent[];
338
+ /** Translated submit button text, e.g. "Gerät prüfen" (de/en). */
339
+ submitLabel: EnyoOnboardingTranslatedContent[];
340
+ /** The possible verdicts; each is a routing handle. At least 2. */
341
+ outcomes: EnyoOnboardingV2InputOutcome[];
342
+ }
212
343
  /** Any block that can appear in a step's `blocks`. */
213
- export type EnyoOnboardingV2Block = EnyoOnboardingV2TextBlock | EnyoOnboardingV2HeadlineBlock | EnyoOnboardingV2BulletsBlock | EnyoOnboardingV2ImageBlock | EnyoOnboardingV2HintBlock | EnyoOnboardingV2DynamicBlock | EnyoOnboardingV2ChoiceBlock | EnyoOnboardingV2ActionBlock;
214
- /** Blocks that produce routing handles (a step's decision points). */
215
- export type EnyoOnboardingV2InteractiveBlock = EnyoOnboardingV2ChoiceBlock | EnyoOnboardingV2ActionBlock;
344
+ export type EnyoOnboardingV2Block = EnyoOnboardingV2TextBlock | EnyoOnboardingV2HeadlineBlock | EnyoOnboardingV2BulletsBlock | EnyoOnboardingV2ImageBlock | EnyoOnboardingV2HintBlock | EnyoOnboardingV2DynamicBlock | EnyoOnboardingV2ChoiceBlock | EnyoOnboardingV2ActionBlock | EnyoOnboardingV2LinkBlock | EnyoOnboardingV2InputBlock;
345
+ /**
346
+ * Blocks that produce routing handles (a step's decision points).
347
+ *
348
+ * A {@link EnyoOnboardingV2LinkBlock} is deliberately absent: it is passive
349
+ * content and routes nothing.
350
+ */
351
+ export type EnyoOnboardingV2InteractiveBlock = EnyoOnboardingV2ChoiceBlock | EnyoOnboardingV2ActionBlock | EnyoOnboardingV2InputBlock;
216
352
  /** Discriminator for the {@link EnyoOnboardingV2TransitionSource} union. */
217
353
  export declare enum EnyoOnboardingV2TransitionSourceKind {
218
354
  /** The step's plain "continue" button (steps with no interactive block). */
219
355
  Continue = "continue",
220
356
  /** A specific option of a `Choice` block was picked. */
221
357
  Choice = "choice",
222
- /** A specific outcome of an `Action` block fired. */
358
+ /** A specific outcome of an `Action` or `Input` block fired. */
223
359
  Outcome = "outcome"
224
360
  }
225
361
  /** Where a transition leaves from within a step. */
@@ -234,7 +370,7 @@ export type EnyoOnboardingV2TransitionSource =
234
370
  blockId: string;
235
371
  optionId: string;
236
372
  }
237
- /** A specific outcome of an `Action` block fired. */
373
+ /** A specific outcome of an `Action` or `Input` block fired. */
238
374
  | {
239
375
  kind: EnyoOnboardingV2TransitionSourceKind.Outcome;
240
376
  blockId: string;
@@ -85,6 +85,19 @@ export var EnyoOnboardingV2ActionKind;
85
85
  */
86
86
  EnyoOnboardingV2ActionKind["DeviceTest"] = "device-test";
87
87
  })(EnyoOnboardingV2ActionKind || (EnyoOnboardingV2ActionKind = {}));
88
+ /**
89
+ * What an {@link EnyoOnboardingV2InputBlock} asks the installer for. Drives the
90
+ * keyboard the app shows, the format check it applies and the seeded texts.
91
+ */
92
+ export var EnyoOnboardingV2InputValueType;
93
+ (function (EnyoOnboardingV2InputValueType) {
94
+ /** Free text; any non-empty value is accepted. */
95
+ EnyoOnboardingV2InputValueType["Text"] = "text";
96
+ /** An IPv4 address. This is the only type the host actually checks. */
97
+ EnyoOnboardingV2InputValueType["IpAddress"] = "ip-address";
98
+ /** A number; `,` and `.` are both accepted as the decimal separator. */
99
+ EnyoOnboardingV2InputValueType["Number"] = "number";
100
+ })(EnyoOnboardingV2InputValueType || (EnyoOnboardingV2InputValueType = {}));
88
101
  /**
89
102
  * Which devices a {@link EnyoOnboardingV2ActionKind.DeviceTest} block hands to
90
103
  * the app.
@@ -126,6 +139,8 @@ export var EnyoOnboardingV2BlockType;
126
139
  EnyoOnboardingV2BlockType["Dynamic"] = "dynamic";
127
140
  EnyoOnboardingV2BlockType["Choice"] = "choice";
128
141
  EnyoOnboardingV2BlockType["Action"] = "action";
142
+ EnyoOnboardingV2BlockType["Link"] = "link";
143
+ EnyoOnboardingV2BlockType["Input"] = "input";
129
144
  })(EnyoOnboardingV2BlockType || (EnyoOnboardingV2BlockType = {}));
130
145
  // ---------------------------------------------------------------------------
131
146
  // Routing: transitions & targets
@@ -137,7 +152,7 @@ export var EnyoOnboardingV2TransitionSourceKind;
137
152
  EnyoOnboardingV2TransitionSourceKind["Continue"] = "continue";
138
153
  /** A specific option of a `Choice` block was picked. */
139
154
  EnyoOnboardingV2TransitionSourceKind["Choice"] = "choice";
140
- /** A specific outcome of an `Action` block fired. */
155
+ /** A specific outcome of an `Action` or `Input` block fired. */
141
156
  EnyoOnboardingV2TransitionSourceKind["Outcome"] = "outcome";
142
157
  })(EnyoOnboardingV2TransitionSourceKind || (EnyoOnboardingV2TransitionSourceKind = {}));
143
158
  /** Discriminator for the {@link EnyoOnboardingV2Target} union. */
package/dist/version.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export declare const SDK_VERSION = "0.0.187";
8
+ export declare const SDK_VERSION = "0.0.188";
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/dist/version.js CHANGED
@@ -5,7 +5,7 @@
5
5
  /**
6
6
  * Current version of the enyo Energy App SDK.
7
7
  */
8
- export const SDK_VERSION = '0.0.187';
8
+ export const SDK_VERSION = '0.0.188';
9
9
  /**
10
10
  * Gets the current SDK version.
11
11
  * @returns The semantic version string of the SDK
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enyo-energy/energy-app-sdk",
3
- "version": "0.0.187",
3
+ "version": "0.0.188",
4
4
  "description": "enyo Energy App SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",