@enyo-energy/energy-app-sdk 1.31.0 → 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,6 +33,7 @@ The official TypeScript SDK for building Energy Apps on the enyo platform. Creat
33
33
  - [NetworkDeviceManager](#networkdevicemanager)
34
34
  - [Startup pattern](#startup-pattern)
35
35
  - [Package Files](#package-files)
36
+ - [Printable Manual Setup Guide](#printable-manual-setup-guide)
36
37
  - [Firmware Update Registry](#firmware-update-registry)
37
38
  - [Declaring firmware](#declaring-firmware)
38
39
  - [Firmware modes](#firmware-modes)
@@ -2851,6 +2852,43 @@ It checks that names are unique slugs, that paths stay inside the package and ca
2851
2852
  extension, and that any declared MIME type is well formed; two names pointing at one
2852
2853
  path is a warning.
2853
2854
 
2855
+ ## Printable Manual Setup Guide
2856
+
2857
+ A package can declare **one optional** onboarding guide (v2) as static data in its package
2858
+ definition. The enyo CLI reads it during `enyo release`, and enyo renders it as a printable
2859
+ PDF installation manual. It is separate from the interactive guides an app returns from
2860
+ `useOnboardingV2().registerOnboardingGuidesHandler()`: it never takes part in an onboarding
2861
+ run, so it needs no `name` and doesn't conflict with those guides in any way.
2862
+
2863
+ Paper cannot scan the network or ask the app anything, so the guide must describe a
2864
+ **manual setup**:
2865
+
2866
+ - `startVariant: EnyoOnboardingV2StartVariant.ManualSetup` and `requiresNetworkScan: false`
2867
+ - no `network-scan` / `device-test` actions, no `device-select` / `eebus-device-select`
2868
+ blocks, no `dynamic` blocks, no `start-variant` targets
2869
+ - no `applianceId`, `vendorId` or `modelIds`
2870
+ - plain JSON only, and every image block `file` declared in `files`
2871
+
2872
+ ```typescript
2873
+ import {
2874
+ defineEnergyAppPackage,
2875
+ definePublicFile,
2876
+ validatePackageOnboardingGuide
2877
+ } from '@enyo-energy/energy-app-sdk';
2878
+ import {manualSetupGuide} from './src/onboarding/manual-setup.guide.js';
2879
+
2880
+ const packageDef = defineEnergyAppPackage({
2881
+ // ...
2882
+ files: [definePublicFile({name: 'modbus-menu', path: './assets/modbus-menu.png'})],
2883
+ onboardingGuide: manualSetupGuide
2884
+ });
2885
+
2886
+ const {ok, errors, warnings} = validatePackageOnboardingGuide(packageDef);
2887
+ ```
2888
+
2889
+ The complete authoring guide, with the reason behind each rule and a full example, is the
2890
+ JSDoc of `EnergyAppPackageDefinition.onboardingGuide`.
2891
+
2854
2892
  ## Firmware Update Registry
2855
2893
 
2856
2894
  Ship firmware images with your package and hand them to devices at runtime. You declare the files by local path in the package definition, the enyo CLI uploads them during `enyo release`, and the app reaches them through `energyApp.useFirmwareRegistry()`. The release tarball never carries the bytes.
@@ -4279,7 +4317,7 @@ Each event carries the **complete** picture of the slot — render it as it arri
4279
4317
 
4280
4318
  New reason types came with this surface, so a skipped row can say *why* instead of falling back to a generic "scheduled optimization": `SessionComplete`, `AppliancePaused`, `NothingConnected`, `DeadlinePassed`, `WaitingForCheaperSlot`, `AboveOwnPriceLimit`, `BelowMinPower`, `OtherApplianceTurn`, `SupplyExhausted`, `OutsideSchedule` — grouped by the new `SessionState` and `Contention` reason categories. `ApplianceInitiatedDraw` and `PowerOffered` came with the waterfall states above.
4281
4319
 
4282
- **Battery vs. charging car.** While a car charges, the house battery is either held out of it or allowed to help, according to the owner's `batteryEvDischargeMode`. Two reason types (category `BatteryState`) say which: `BatteryReservedFromEv` for the hold (`Discharge 0`) and `BatterySupportsEvCharging` for the release (`mode=Auto`). Both carry `context.batteryToEv` (`EnyoDataBusCommandReasonBatteryToEvContext`) with the owner's `mode`, the hold `trigger` (`EnyoBatteryToEvHoldTriggerEnum`: `OwnerBlocked`, `SocLimitReached`, `AllowanceSpent`, `NoMeasurement`), `socLimitPercent`, `allowanceWh` and `remainingWh`. The energy manager re-checks a hold periodically by briefly releasing the battery; that re-check sets `batteryToEv.probe: true` and should not be listed as its own entry in a command history.
4320
+ **Battery vs. charging car.** While a car charges, the house battery is either held out of it or allowed to help, according to the owner's `batteryEvDischargeMode`. Two reason types (category `BatteryState`) say which: `BatteryReservedFromEv` for the hold (`Discharge 0`) and `BatterySupportsEvCharging` for the release (`mode=Auto`). Both carry `context.batteryToEv` (`EnyoDataBusCommandReasonBatteryToEvContext`) with the owner's `mode`, the hold `trigger` (`EnyoBatteryToEvHoldTriggerEnum`: `OwnerBlocked`, `SocLimitReached`, `AllowanceSpent`, `NoMeasurement`), `socLimitPercent`, `allowanceWh` and `remainingWh`. The energy manager re-checks a hold periodically by briefly releasing the battery; that re-check sets `batteryToEv.probe: true` and should not be listed as its own entry in a command history. The car's side of the same decision is stated on the charger's command with `EvChargingFromHomeBattery` (also category `BatteryState`): `powerW` is the battery's share of the charging setpoint (never the whole setpoint), `socPercent` the battery fleet's SoC, `context.batteryToEv` the owner's mode and the session's `socLimitPercent` / `allowanceWh` / `remainingWh` (never `trigger` or `probe`), and, under `Intelligent` only, `electricityPricePerKwh` is what the stored energy costs.
4283
4321
 
4284
4322
  **Battery first on PV surplus.** When a surplus consumer (EV on PV, heating rod, heat pump boost, surplus automation) is held back or reduced because the battery is charged first up to the owner's `batteryPvPriorityTargetSocPercent`, the energy manager states `BatteryPvPriority` (category `PvSurplus`) on that consumer: `context.batteryPvPriority.targetSocPercent` for the target, `socPercent` for the pack's current SoC and `powerW` for the surplus the battery is taking — e.g. "The battery charges to 40 % first (now 28 %)."
4285
4323
 
@@ -1,6 +1,7 @@
1
1
  import { EnergyAppPermissionType } from "./energy-app-permission.type.cjs";
2
2
  import { EnergyAppModelFeatureEnum } from "./energy-app-model-feature.enum.cjs";
3
3
  import type { EnyoFileTranslation } from "./types/enyo-file.cjs";
4
+ import type { EnyoOnboardingV2Guide } from "./types/enyo-onboarding-v2.cjs";
4
5
  export type EnergyAppPackageLanguage = 'de' | 'en';
5
6
  export declare enum EnergyAppPackageCategory {
6
7
  Inverter = "inverter",
@@ -658,6 +659,147 @@ export interface EnergyAppPackageDefinition {
658
659
  * Only relevant when {@link firmware} is declared.
659
660
  */
660
661
  firmwareMode?: EnergyAppPackageFirmwareMode;
662
+ /**
663
+ * The package's static **manual setup** onboarding guide (v2). It is
664
+ * optional and there is at most one. The enyo CLI reads it at release time
665
+ * and ships it with the package version, where enyo renders it as a
666
+ * printable PDF installation manual.
667
+ *
668
+ * ## What it is for
669
+ *
670
+ * The guides an app returns from
671
+ * {@link EnergyAppOnboardingV2.registerOnboardingGuidesHandler} are
672
+ * interactive and only exist on a device where the app is running. This
673
+ * guide is the opposite: a fixed document that exists as soon as the
674
+ * package is released. An installer can read it on paper before the
675
+ * device is even powered. It is written with the same v2 guide model, so
676
+ * an app that already has an interactive manual setup flow can usually
677
+ * declare the same steps here.
678
+ *
679
+ * It does not take part in onboarding runs. It is never offered by the
680
+ * host, never addressed by name and never compared against the guides from
681
+ * the runtime handler, so there are no naming or precedence rules between
682
+ * the two.
683
+ *
684
+ * ## What it may contain
685
+ *
686
+ * Paper cannot scan the network or ask the app anything, so the guide is
687
+ * limited to a manual setup the installer can follow by reading. The
688
+ * following rules are enforced by `validatePackageOnboardingGuide()`, and
689
+ * the CLI refuses the release if any of them is broken:
690
+ *
691
+ * | Rule | Why |
692
+ * |---|---|
693
+ * | `startVariant` is {@link EnyoOnboardingV2StartVariant.ManualSetup} | The document describes a manual setup, nothing else. |
694
+ * | `requiresNetworkScan: false` is set explicitly | The default is `true`, and a printed manual has no scan to rely on. |
695
+ * | No `network-scan` or `device-test` action blocks | Both act on devices found by a scan. |
696
+ * | No `device-select` or `eebus-device-select` blocks | Both pick from devices the host discovered. |
697
+ * | No `dynamic` blocks | Their value is resolved by the app at runtime and does not exist when the PDF is rendered. |
698
+ * | No `start-variant` targets | They lead into another, scan-based guide that is not part of the document. |
699
+ * | No `applianceId`, `vendorId` or `modelIds` | A document is not bound to an appliance, and enyo derives vendor and models from the package. |
700
+ * | Plain JSON only (no functions, `Date`s, `undefined` in arrays, class instances) | The CLI serializes the guide into the release. |
701
+ * | Every image block `file` is declared in {@link files} | Otherwise the PDF has a hole where the image belongs. |
702
+ *
703
+ * Everything else is allowed. Content blocks (text, headline, bullets,
704
+ * image, hint, link) render as they are. Interactive blocks (choice,
705
+ * select, input, credentials, auth, additional-setup and the
706
+ * `connection-check` / `ocpp-connect` actions) render as instructions,
707
+ * and their outcomes and transitions become the branches of the document
708
+ * ("if the device is not reachable, continue with …"). Give every
709
+ * outcome a label that reads well on paper.
710
+ *
711
+ * `name` is optional and has no meaning here.
712
+ *
713
+ * ## How to write one
714
+ *
715
+ * 1. Put the guide in its own file (for example
716
+ * `src/onboarding/manual-setup.guide.ts`) and build it with
717
+ * `defineOnboardingGuideV2()`, the `onboardingV2Block.*` and
718
+ * `onboardingV2Target.*` factories, and the `onContinueV2()` /
719
+ * `onOutcomeV2()` transition helpers.
720
+ * 2. Keep the file free of runtime dependencies. The CLI loads it while
721
+ * loading this definition, outside the app sandbox, so it must not
722
+ * touch the SDK instance, storage, the network or environment state.
723
+ * 3. Import it here and assign it to `onboardingGuide`.
724
+ * 4. Check it locally with `validatePackageOnboardingGuide(definition)`.
725
+ * 5. Release. The CLI validates the guide again and aborts on errors.
726
+ *
727
+ * ```typescript
728
+ * // src/onboarding/manual-setup.guide.ts
729
+ * import {
730
+ * defineOnboardingGuideV2,
731
+ * EnyoDeviceTestOutcomeEnum,
732
+ * EnyoOnboardingV2InputValueType,
733
+ * EnyoOnboardingV2StartVariant,
734
+ * onboardingV2Block,
735
+ * onboardingV2Target,
736
+ * onContinueV2,
737
+ * onOutcomeV2,
738
+ * } from '@enyo-energy/energy-app-sdk';
739
+ *
740
+ * const t = (de: string, en: string) => [
741
+ * {language: 'de' as const, value: de},
742
+ * {language: 'en' as const, value: en},
743
+ * ];
744
+ *
745
+ * export const manualSetupGuide = defineOnboardingGuideV2({
746
+ * title: t('Wechselrichter manuell einrichten', 'Set up the inverter manually'),
747
+ * startVariant: EnyoOnboardingV2StartVariant.ManualSetup,
748
+ * requiresNetworkScan: false,
749
+ * startStepId: 'prepare',
750
+ * steps: [
751
+ * {
752
+ * id: 'prepare',
753
+ * name: 'modbus-aktivieren',
754
+ * title: t('Modbus TCP aktivieren', 'Enable Modbus TCP'),
755
+ * blocks: [
756
+ * onboardingV2Block.text('intro', t(
757
+ * 'Aktiviere Modbus TCP im Webinterface des Wechselrichters.',
758
+ * 'Enable Modbus TCP in the inverter web interface.',
759
+ * )),
760
+ * onboardingV2Block.imageFile('menu', 'modbus-menu', t('Menü', 'Menu')),
761
+ * ],
762
+ * transitions: [onContinueV2(onboardingV2Target.step('ip'))],
763
+ * },
764
+ * {
765
+ * id: 'ip',
766
+ * name: 'ip-eingeben',
767
+ * title: t('IP-Adresse eingeben', 'Enter the IP address'),
768
+ * blocks: [
769
+ * onboardingV2Block.input(
770
+ * 'device-ip',
771
+ * EnyoOnboardingV2InputValueType.IpAddress,
772
+ * t('IP-Adresse des Wechselrichters', 'Inverter IP address'),
773
+ * t('Verbindung prüfen', 'Check connection'),
774
+ * [
775
+ * {id: 'ok', value: EnyoDeviceTestOutcomeEnum.AppliancesCreated, label: t('Verbunden', 'Connected')},
776
+ * {id: 'unreachable', value: EnyoDeviceTestOutcomeEnum.Unreachable, label: t('Nicht erreichbar', 'Unreachable')},
777
+ * ],
778
+ * {placeholder: t('z. B. 192.168.1.42', 'e.g. 192.168.1.42')},
779
+ * ),
780
+ * ],
781
+ * transitions: [
782
+ * onOutcomeV2('device-ip', 'ok', onboardingV2Target.success()),
783
+ * onOutcomeV2('device-ip', 'unreachable', onboardingV2Target.step('prepare')),
784
+ * ],
785
+ * },
786
+ * ],
787
+ * });
788
+ *
789
+ * // energy-app.package.ts
790
+ * import {defineEnergyAppPackage, definePublicFile} from '@enyo-energy/energy-app-sdk';
791
+ * import {manualSetupGuide} from './src/onboarding/manual-setup.guide.js';
792
+ *
793
+ * export default defineEnergyAppPackage({
794
+ * // ...
795
+ * files: [definePublicFile({name: 'modbus-menu', path: './assets/modbus-menu.png'})],
796
+ * onboardingGuide: manualSetupGuide,
797
+ * });
798
+ * ```
799
+ *
800
+ * Omit the field for packages that have no manual setup to document.
801
+ */
802
+ onboardingGuide?: EnyoOnboardingV2Guide;
661
803
  }
662
804
  /**
663
805
  * Defines an Energy App package with automatic SDK version injection.
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ /**
3
+ * Client-side validator for the static manual setup guide declared in a
4
+ * package definition ({@link EnergyAppPackageDefinition.onboardingGuide}).
5
+ *
6
+ * The guide leaves the app repository as data: the enyo CLI reads it at release
7
+ * time and enyo renders it as a printable PDF. Nothing on paper can scan the
8
+ * network or ask the app for a value, so on top of the regular graph checks of
9
+ * {@link validateOnboardingGuideV2} this rejects everything that only works in
10
+ * an interactive run — scans, device pickers, device tests, dynamic values —
11
+ * and everything the CLI cannot serialize.
12
+ *
13
+ * Use {@link validatePackageOnboardingGuide} for the non-throwing result, or
14
+ * {@link assertValidPackageOnboardingGuide} to throw on failure.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.PackageOnboardingGuideValidationError = exports.PACKAGE_ONBOARDING_GUIDE_START_VARIANT = void 0;
18
+ exports.validatePackageOnboardingGuide = validatePackageOnboardingGuide;
19
+ exports.assertValidPackageOnboardingGuide = assertValidPackageOnboardingGuide;
20
+ const enyo_onboarding_v2_js_1 = require("../../types/enyo-onboarding-v2.cjs");
21
+ const onboarding_v2_validators_js_1 = require("./onboarding-v2-validators.cjs");
22
+ /**
23
+ * The only start variant a package's static guide may use. The guide documents
24
+ * a setup the installer performs by hand.
25
+ */
26
+ exports.PACKAGE_ONBOARDING_GUIDE_START_VARIANT = enyo_onboarding_v2_js_1.EnyoOnboardingV2StartVariant.ManualSetup;
27
+ /**
28
+ * Thrown by {@link assertValidPackageOnboardingGuide} when a package's static
29
+ * onboarding guide fails validation. The message lists every blocking error so
30
+ * callers can surface them directly.
31
+ */
32
+ class PackageOnboardingGuideValidationError extends Error {
33
+ /** The individual blocking errors that caused the failure. */
34
+ errors;
35
+ /**
36
+ * @param errors - The blocking validation errors.
37
+ */
38
+ constructor(errors) {
39
+ super(`Invalid package onboarding guide:\n- ${errors.join('\n- ')}`);
40
+ this.name = 'PackageOnboardingGuideValidationError';
41
+ this.errors = errors;
42
+ }
43
+ }
44
+ exports.PackageOnboardingGuideValidationError = PackageOnboardingGuideValidationError;
45
+ /** Action kinds that act on scanned devices and therefore cannot be printed. */
46
+ const FORBIDDEN_ACTIONS = new Set([
47
+ enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.NetworkScan,
48
+ enyo_onboarding_v2_js_1.EnyoOnboardingV2ActionKind.DeviceTest,
49
+ ]);
50
+ /**
51
+ * Block types that only exist in an interactive run, with the reason shown to
52
+ * the author.
53
+ */
54
+ const FORBIDDEN_BLOCKS = new Map([
55
+ [enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.DeviceSelect, 'it picks from devices the host discovered'],
56
+ [enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.EebusDeviceSelect, 'it picks from devices the host discovered'],
57
+ [enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Dynamic, 'its value is resolved by the app at runtime and does not exist when the PDF is rendered'],
58
+ ]);
59
+ /**
60
+ * Validates the static manual setup guide of a package: the regular v2 graph
61
+ * checks (with image references resolved against the package's `files`), the
62
+ * manual-setup-only rules, and that the guide is plain JSON the CLI can ship.
63
+ *
64
+ * A package without an `onboardingGuide` is trivially valid.
65
+ *
66
+ * @param definition - The package definition, or just its `onboardingGuide`
67
+ * and `files`.
68
+ * @returns The {@link PackageOnboardingGuideValidationResult}.
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * const {ok, errors} = validatePackageOnboardingGuide(packageDef);
73
+ * if (!ok) throw new Error(errors.join('\n'));
74
+ * ```
75
+ */
76
+ function validatePackageOnboardingGuide(definition) {
77
+ const errors = [];
78
+ const warnings = [];
79
+ const guide = definition.onboardingGuide;
80
+ if (guide === undefined)
81
+ return { ok: true, errors, warnings };
82
+ if (guide === null || typeof guide !== 'object' || Array.isArray(guide)) {
83
+ errors.push('`onboardingGuide` must be a single guide object — a package declares at most one.');
84
+ return { ok: false, errors, warnings };
85
+ }
86
+ // Not plain data means not a guide shape the graph checks can walk safely.
87
+ collectNonJsonValues(guide, 'onboardingGuide', errors);
88
+ if (errors.length > 0)
89
+ return { ok: false, errors, warnings };
90
+ const base = (0, onboarding_v2_validators_js_1.validateOnboardingGuideV2)(guide, { files: definition.files });
91
+ errors.push(...base.errors);
92
+ warnings.push(...base.warnings);
93
+ validateManualSetupOnly(guide, errors);
94
+ return { ok: errors.length === 0, errors, warnings };
95
+ }
96
+ /**
97
+ * Like {@link validatePackageOnboardingGuide}, but throws
98
+ * {@link PackageOnboardingGuideValidationError} when there are blocking errors.
99
+ * Warnings never throw.
100
+ *
101
+ * @param definition - The package definition, or just its `onboardingGuide`
102
+ * and `files`.
103
+ * @returns The declared guide, or `undefined` when the package declares none.
104
+ * @throws {PackageOnboardingGuideValidationError} When validation produces any error.
105
+ */
106
+ function assertValidPackageOnboardingGuide(definition) {
107
+ const { ok, errors } = validatePackageOnboardingGuide(definition);
108
+ if (!ok)
109
+ throw new PackageOnboardingGuideValidationError(errors);
110
+ return definition.onboardingGuide;
111
+ }
112
+ /**
113
+ * Applies the rules that make a guide printable: manual setup variant, no
114
+ * scan, nothing that depends on discovered devices or runtime values, no link
115
+ * into another guide, and no appliance or catalog binding.
116
+ *
117
+ * @param guide - The guide being validated.
118
+ * @param errors - Collector for blocking problems.
119
+ */
120
+ function validateManualSetupOnly(guide, errors) {
121
+ if (guide.startVariant !== exports.PACKAGE_ONBOARDING_GUIDE_START_VARIANT) {
122
+ errors.push(`\`startVariant\` must be "${exports.PACKAGE_ONBOARDING_GUIDE_START_VARIANT}" — the package guide ` +
123
+ `documents a manual setup, got "${guide.startVariant}".`);
124
+ }
125
+ if (guide.requiresNetworkScan !== false) {
126
+ errors.push('`requiresNetworkScan` must be set to `false` — it defaults to `true`, and a printed ' +
127
+ 'manual has no scan to rely on.');
128
+ }
129
+ if (guide.applianceId !== undefined) {
130
+ errors.push('`applianceId` must not be set — a printed manual is not bound to an appliance.');
131
+ }
132
+ if (guide.vendorId !== undefined || guide.modelIds !== undefined) {
133
+ errors.push('`vendorId` / `modelIds` must not be set — enyo derives them from the package and its ' +
134
+ '`compatibility` list.');
135
+ }
136
+ for (const [i, step] of (guide.steps ?? []).entries()) {
137
+ const at = `steps[${i}] (${step.name || '?'})`;
138
+ for (const block of step.blocks ?? []) {
139
+ const reason = FORBIDDEN_BLOCKS.get(block.type);
140
+ if (reason) {
141
+ errors.push(`${at}: ${block.type} block "${block.id}" is not allowed in the package guide — ${reason}.`);
142
+ }
143
+ else if (block.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2BlockType.Action && FORBIDDEN_ACTIONS.has(block.action)) {
144
+ errors.push(`${at}: ${block.action} action "${block.id}" is not allowed in the package guide — ` +
145
+ 'it acts on devices found by a scan.');
146
+ }
147
+ }
148
+ for (const transition of step.transitions ?? []) {
149
+ if (transition.target?.type === enyo_onboarding_v2_js_1.EnyoOnboardingV2TargetType.StartVariant) {
150
+ errors.push(`${at}: transition "${transition.id}" leads to start variant "${transition.target.variant}" — ` +
151
+ 'the package guide cannot hand over to another guide.');
152
+ }
153
+ }
154
+ }
155
+ }
156
+ /**
157
+ * Walks a value and reports everything that does not survive a JSON round trip
158
+ * unchanged: functions, symbols, bigints, non-finite numbers, `undefined` array
159
+ * entries, and objects that are not plain objects (class instances, `Date`,
160
+ * `Map`, …). `undefined` object properties are fine — they are simply dropped.
161
+ *
162
+ * @param value - The value to inspect.
163
+ * @param path - Human-readable location of `value`, used in messages.
164
+ * @param errors - Collector for blocking problems.
165
+ * @param seen - Objects already on the current path, to report cycles.
166
+ */
167
+ function collectNonJsonValues(value, path, errors, seen = new Set()) {
168
+ switch (typeof value) {
169
+ case 'string':
170
+ case 'boolean':
171
+ return;
172
+ case 'number':
173
+ if (!Number.isFinite(value))
174
+ errors.push(`${path}: ${value} is not representable in JSON.`);
175
+ return;
176
+ case 'undefined':
177
+ return;
178
+ case 'function':
179
+ case 'symbol':
180
+ case 'bigint':
181
+ errors.push(`${path}: a ${typeof value} cannot be serialized into the release — use plain data only.`);
182
+ return;
183
+ }
184
+ if (value === null)
185
+ return;
186
+ const obj = value;
187
+ if (seen.has(obj)) {
188
+ errors.push(`${path}: circular reference — the guide must be a tree of plain data.`);
189
+ return;
190
+ }
191
+ if (Array.isArray(obj)) {
192
+ seen.add(obj);
193
+ obj.forEach((entry, i) => {
194
+ if (entry === undefined)
195
+ errors.push(`${path}[${i}]: undefined array entries become null in JSON.`);
196
+ else
197
+ collectNonJsonValues(entry, `${path}[${i}]`, errors, seen);
198
+ });
199
+ seen.delete(obj);
200
+ return;
201
+ }
202
+ const proto = Object.getPrototypeOf(obj);
203
+ if (proto !== Object.prototype && proto !== null) {
204
+ const kind = obj.constructor?.name ?? 'non-plain object';
205
+ errors.push(`${path}: ${kind} is not plain data — use object literals, strings, numbers and booleans.`);
206
+ return;
207
+ }
208
+ seen.add(obj);
209
+ for (const [key, entry] of Object.entries(obj)) {
210
+ collectNonJsonValues(entry, `${path}.${key}`, errors, seen);
211
+ }
212
+ seen.delete(obj);
213
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Client-side validator for the static manual setup guide declared in a
3
+ * package definition ({@link EnergyAppPackageDefinition.onboardingGuide}).
4
+ *
5
+ * The guide leaves the app repository as data: the enyo CLI reads it at release
6
+ * time and enyo renders it as a printable PDF. Nothing on paper can scan the
7
+ * network or ask the app for a value, so on top of the regular graph checks of
8
+ * {@link validateOnboardingGuideV2} this rejects everything that only works in
9
+ * an interactive run — scans, device pickers, device tests, dynamic values —
10
+ * and everything the CLI cannot serialize.
11
+ *
12
+ * Use {@link validatePackageOnboardingGuide} for the non-throwing result, or
13
+ * {@link assertValidPackageOnboardingGuide} to throw on failure.
14
+ */
15
+ import type { EnergyAppPackageDefinition } from '../../energy-app-package-definition.cjs';
16
+ import { EnyoOnboardingV2StartVariant } from '../../types/enyo-onboarding-v2.cjs';
17
+ import type { EnyoOnboardingV2Guide } from '../../types/enyo-onboarding-v2.cjs';
18
+ /**
19
+ * The only start variant a package's static guide may use. The guide documents
20
+ * a setup the installer performs by hand.
21
+ */
22
+ export declare const PACKAGE_ONBOARDING_GUIDE_START_VARIANT = EnyoOnboardingV2StartVariant.ManualSetup;
23
+ /**
24
+ * Thrown by {@link assertValidPackageOnboardingGuide} when a package's static
25
+ * onboarding guide fails validation. The message lists every blocking error so
26
+ * callers can surface them directly.
27
+ */
28
+ export declare class PackageOnboardingGuideValidationError extends Error {
29
+ /** The individual blocking errors that caused the failure. */
30
+ readonly errors: string[];
31
+ /**
32
+ * @param errors - The blocking validation errors.
33
+ */
34
+ constructor(errors: string[]);
35
+ }
36
+ /** The outcome of validating a package's static onboarding guide. */
37
+ export interface PackageOnboardingGuideValidationResult {
38
+ /** True when there are no blocking `errors` (warnings are still allowed). */
39
+ ok: boolean;
40
+ /** Blocking problems — these must be fixed before releasing. */
41
+ errors: string[];
42
+ /** Advisory problems — allowed, but usually worth reviewing. */
43
+ warnings: string[];
44
+ }
45
+ /** The parts of a package definition the static guide is validated against. */
46
+ export type PackageOnboardingGuideSource = Pick<EnergyAppPackageDefinition, 'onboardingGuide' | 'files'>;
47
+ /**
48
+ * Validates the static manual setup guide of a package: the regular v2 graph
49
+ * checks (with image references resolved against the package's `files`), the
50
+ * manual-setup-only rules, and that the guide is plain JSON the CLI can ship.
51
+ *
52
+ * A package without an `onboardingGuide` is trivially valid.
53
+ *
54
+ * @param definition - The package definition, or just its `onboardingGuide`
55
+ * and `files`.
56
+ * @returns The {@link PackageOnboardingGuideValidationResult}.
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const {ok, errors} = validatePackageOnboardingGuide(packageDef);
61
+ * if (!ok) throw new Error(errors.join('\n'));
62
+ * ```
63
+ */
64
+ export declare function validatePackageOnboardingGuide(definition: PackageOnboardingGuideSource): PackageOnboardingGuideValidationResult;
65
+ /**
66
+ * Like {@link validatePackageOnboardingGuide}, but throws
67
+ * {@link PackageOnboardingGuideValidationError} when there are blocking errors.
68
+ * Warnings never throw.
69
+ *
70
+ * @param definition - The package definition, or just its `onboardingGuide`
71
+ * and `files`.
72
+ * @returns The declared guide, or `undefined` when the package declares none.
73
+ * @throws {PackageOnboardingGuideValidationError} When validation produces any error.
74
+ */
75
+ export declare function assertValidPackageOnboardingGuide(definition: PackageOnboardingGuideSource): EnyoOnboardingV2Guide | undefined;
@@ -115,6 +115,7 @@ __exportStar(require("./packages/energy-app-onboarding.cjs"), exports);
115
115
  __exportStar(require("./types/enyo-onboarding-v2.cjs"), exports);
116
116
  __exportStar(require("./implementations/onboarding-v2/define-onboarding-guide-v2.cjs"), exports);
117
117
  __exportStar(require("./implementations/onboarding-v2/onboarding-v2-validators.cjs"), exports);
118
+ __exportStar(require("./implementations/onboarding-v2/package-onboarding-guide-validators.cjs"), exports);
118
119
  __exportStar(require("./types/enyo-onboarding-v2-provider.cjs"), exports);
119
120
  __exportStar(require("./packages/energy-app-onboarding-v2.cjs"), exports);
120
121
  __exportStar(require("./implementations/onboarding-v2/onboarding-v2-provider-validators.cjs"), exports);
@@ -99,6 +99,7 @@ export * from './packages/energy-app-onboarding.cjs';
99
99
  export * from './types/enyo-onboarding-v2.cjs';
100
100
  export * from './implementations/onboarding-v2/define-onboarding-guide-v2.cjs';
101
101
  export * from './implementations/onboarding-v2/onboarding-v2-validators.cjs';
102
+ export * from './implementations/onboarding-v2/package-onboarding-guide-validators.cjs';
102
103
  export * from './types/enyo-onboarding-v2-provider.cjs';
103
104
  export * from './packages/energy-app-onboarding-v2.cjs';
104
105
  export * from './implementations/onboarding-v2/onboarding-v2-provider-validators.cjs';
@@ -263,6 +263,37 @@ var EnyoDataBusCommandReasonTypeEnum;
263
263
  * {@link EnyoDataBusCommandReasonCategoryEnum.BatteryState}.
264
264
  */
265
265
  EnyoDataBusCommandReasonTypeEnum["BatterySupportsEvCharging"] = "battery-supports-ev-charging";
266
+ /**
267
+ * The car charges on energy from the house battery, within the owner's
268
+ * battery-to-EV setting (`batteryEvDischargeMode`).
269
+ *
270
+ * Stated on the CHARGER's command by the decision maker that routed the
271
+ * battery's energy into the car — never inferred from the battery's
272
+ * measured discharge. The battery states its own side of the same decision
273
+ * with {@link BatterySupportsEvCharging} (released) or
274
+ * {@link BatteryReservedFromEv} (held); this reason is the car's side, and
275
+ * the two are sent together.
276
+ *
277
+ * Fields:
278
+ * - {@link EnyoDataBusCommandReason.powerW}: the battery's share of the
279
+ * charging setpoint, in W. The rest of the setpoint is solar. Never the
280
+ * whole setpoint.
281
+ * - {@link EnyoDataBusCommandReason.socPercent}: the battery fleet's current
282
+ * SoC.
283
+ * - {@link EnyoDataBusCommandReasonContext.batteryToEv}: the owner's mode and
284
+ * where the session stands against it — `socLimitPercent` under
285
+ * `SocLimit`, `allowanceWh` under `FixedWh`, `remainingWh` under both.
286
+ * `trigger` and `probe` are never set here: they describe a battery hold.
287
+ * - {@link EnyoDataBusCommandReason.electricityPricePerKwh}: under
288
+ * `Intelligent` only, what the stored energy costs, which the car's own
289
+ * price limit was compared against. Absent under `SocLimit` / `FixedWh`,
290
+ * which feed the car at any stored price.
291
+ *
292
+ * Told apart from {@link BatteryCapacityAvailable}, which says only that a
293
+ * battery can supply power and names no owner setting. Category
294
+ * {@link EnyoDataBusCommandReasonCategoryEnum.BatteryState}.
295
+ */
296
+ EnyoDataBusCommandReasonTypeEnum["EvChargingFromHomeBattery"] = "ev-charging-from-home-battery";
266
297
  /**
267
298
  * The appliance was switched off a moment ago and is held off for its
268
299
  * declared minimum off-time, so it is not cycled on and off. Stated by the
@@ -279,6 +279,37 @@ export declare enum EnyoDataBusCommandReasonTypeEnum {
279
279
  * {@link EnyoDataBusCommandReasonCategoryEnum.BatteryState}.
280
280
  */
281
281
  BatterySupportsEvCharging = "battery-supports-ev-charging",
282
+ /**
283
+ * The car charges on energy from the house battery, within the owner's
284
+ * battery-to-EV setting (`batteryEvDischargeMode`).
285
+ *
286
+ * Stated on the CHARGER's command by the decision maker that routed the
287
+ * battery's energy into the car — never inferred from the battery's
288
+ * measured discharge. The battery states its own side of the same decision
289
+ * with {@link BatterySupportsEvCharging} (released) or
290
+ * {@link BatteryReservedFromEv} (held); this reason is the car's side, and
291
+ * the two are sent together.
292
+ *
293
+ * Fields:
294
+ * - {@link EnyoDataBusCommandReason.powerW}: the battery's share of the
295
+ * charging setpoint, in W. The rest of the setpoint is solar. Never the
296
+ * whole setpoint.
297
+ * - {@link EnyoDataBusCommandReason.socPercent}: the battery fleet's current
298
+ * SoC.
299
+ * - {@link EnyoDataBusCommandReasonContext.batteryToEv}: the owner's mode and
300
+ * where the session stands against it — `socLimitPercent` under
301
+ * `SocLimit`, `allowanceWh` under `FixedWh`, `remainingWh` under both.
302
+ * `trigger` and `probe` are never set here: they describe a battery hold.
303
+ * - {@link EnyoDataBusCommandReason.electricityPricePerKwh}: under
304
+ * `Intelligent` only, what the stored energy costs, which the car's own
305
+ * price limit was compared against. Absent under `SocLimit` / `FixedWh`,
306
+ * which feed the car at any stored price.
307
+ *
308
+ * Told apart from {@link BatteryCapacityAvailable}, which says only that a
309
+ * battery can supply power and names no owner setting. Category
310
+ * {@link EnyoDataBusCommandReasonCategoryEnum.BatteryState}.
311
+ */
312
+ EvChargingFromHomeBattery = "ev-charging-from-home-battery",
282
313
  /**
283
314
  * The appliance was switched off a moment ago and is held off for its
284
315
  * declared minimum off-time, so it is not cycled on and off. Stated by the
@@ -675,7 +706,10 @@ export interface EnyoDataBusCommandReasonContext {
675
706
  * The owner's battery-to-EV setting (`batteryEvDischargeMode`) and where
676
707
  * the charging session stands against it. Set on
677
708
  * {@link EnyoDataBusCommandReasonTypeEnum.BatteryReservedFromEv} and
678
- * {@link EnyoDataBusCommandReasonTypeEnum.BatterySupportsEvCharging}.
709
+ * {@link EnyoDataBusCommandReasonTypeEnum.BatterySupportsEvCharging} (the
710
+ * battery's command), and on
711
+ * {@link EnyoDataBusCommandReasonTypeEnum.EvChargingFromHomeBattery} (the
712
+ * charger's command).
679
713
  */
680
714
  batteryToEv?: EnyoDataBusCommandReasonBatteryToEvContext;
681
715
  /**
@@ -962,8 +996,9 @@ export declare enum EnyoBatteryToEvHoldTriggerEnum {
962
996
  /**
963
997
  * The owner's battery-to-EV setting (`batteryEvDischargeMode`) and where
964
998
  * the current charging session stands against it — the structured backing of
965
- * {@link EnyoDataBusCommandReasonTypeEnum.BatteryReservedFromEv} and
966
- * {@link EnyoDataBusCommandReasonTypeEnum.BatterySupportsEvCharging}.
999
+ * {@link EnyoDataBusCommandReasonTypeEnum.BatteryReservedFromEv},
1000
+ * {@link EnyoDataBusCommandReasonTypeEnum.BatterySupportsEvCharging} and
1001
+ * {@link EnyoDataBusCommandReasonTypeEnum.EvChargingFromHomeBattery}.
967
1002
  *
968
1003
  * All energy values are in watt-hours (Wh).
969
1004
  *
@@ -994,6 +1029,20 @@ export declare enum EnyoBatteryToEvHoldTriggerEnum {
994
1029
  * }},
995
1030
  * };
996
1031
  *
1032
+ * // The charger's side of the same decision: 2.4 kW of the car's setpoint is
1033
+ * // the battery's, which may go on down to 50 %:
1034
+ * const charging: EnyoDataBusCommandReason = {
1035
+ * type: EnyoDataBusCommandReasonTypeEnum.EvChargingFromHomeBattery,
1036
+ * category: EnyoDataBusCommandReasonCategoryEnum.BatteryState,
1037
+ * powerW: 2400,
1038
+ * socPercent: 79,
1039
+ * context: {batteryToEv: {
1040
+ * mode: EnergyManagerBatteryEvDischargeModeEnum.SocLimit,
1041
+ * socLimitPercent: 50,
1042
+ * remainingWh: 3895,
1043
+ * }},
1044
+ * };
1045
+ *
997
1046
  * // The periodic re-check of an existing hold — not a new decision:
998
1047
  * const probe: EnyoDataBusCommandReason = {
999
1048
  * type: EnyoDataBusCommandReasonTypeEnum.BatterySupportsEvCharging,
@@ -1014,6 +1063,7 @@ export interface EnyoDataBusCommandReasonBatteryToEvContext {
1014
1063
  /**
1015
1064
  * Why the battery is held. Set on
1016
1065
  * {@link EnyoDataBusCommandReasonTypeEnum.BatteryReservedFromEv} only.
1066
+ * Never set on {@link EnyoDataBusCommandReasonTypeEnum.EvChargingFromHomeBattery}.
1017
1067
  */
1018
1068
  trigger?: EnyoBatteryToEvHoldTriggerEnum;
1019
1069
  /**
@@ -1036,6 +1086,7 @@ export interface EnyoDataBusCommandReasonBatteryToEvContext {
1036
1086
  * `true` for a short re-check of an existing hold: the battery is offered
1037
1087
  * `Auto` to see whether it still drains into the car. Not a change of
1038
1088
  * decision — consumers SHOULD NOT list it as a separate history entry.
1089
+ * Never set on {@link EnyoDataBusCommandReasonTypeEnum.EvChargingFromHomeBattery}.
1039
1090
  */
1040
1091
  probe?: boolean;
1041
1092
  }