@enyo-energy/energy-app-sdk 1.31.0 → 1.33.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)
@@ -828,6 +829,9 @@ vehicle?.priceLimitMode; // how the ceiling is expressed, or none
828
829
  vehicle?.priceLimitCtPerKwh; // ct/kWh ceiling, under 'ct-per-kwh'
829
830
  vehicle?.priceLimitSharePercent; // cheapest n % of the day, under 'cheapest-share'
830
831
  vehicle?.immediateReserveKwh; // full-power reserve before any mode
832
+ vehicle?.minChargingCurrentAmpere; // lowest current per phase the car accepts
833
+ vehicle?.cannotBePaused; // true: never pause a started session
834
+ vehicle?.phaseSwitchDisabled; // true: never switch phases while charging
831
835
  vehicle?.departureTimeHHmm; // daily deadline, wall-clock "07:30"
832
836
  vehicle?.departureTimezone; // IANA zone the time is read in
833
837
 
@@ -2851,6 +2855,43 @@ It checks that names are unique slugs, that paths stay inside the package and ca
2851
2855
  extension, and that any declared MIME type is well formed; two names pointing at one
2852
2856
  path is a warning.
2853
2857
 
2858
+ ## Printable Manual Setup Guide
2859
+
2860
+ A package can declare **one optional** onboarding guide (v2) as static data in its package
2861
+ definition. The enyo CLI reads it during `enyo release`, and enyo renders it as a printable
2862
+ PDF installation manual. It is separate from the interactive guides an app returns from
2863
+ `useOnboardingV2().registerOnboardingGuidesHandler()`: it never takes part in an onboarding
2864
+ run, so it needs no `name` and doesn't conflict with those guides in any way.
2865
+
2866
+ Paper cannot scan the network or ask the app anything, so the guide must describe a
2867
+ **manual setup**:
2868
+
2869
+ - `startVariant: EnyoOnboardingV2StartVariant.ManualSetup` and `requiresNetworkScan: false`
2870
+ - no `network-scan` / `device-test` actions, no `device-select` / `eebus-device-select`
2871
+ blocks, no `dynamic` blocks, no `start-variant` targets
2872
+ - no `applianceId`, `vendorId` or `modelIds`
2873
+ - plain JSON only, and every image block `file` declared in `files`
2874
+
2875
+ ```typescript
2876
+ import {
2877
+ defineEnergyAppPackage,
2878
+ definePublicFile,
2879
+ validatePackageOnboardingGuide
2880
+ } from '@enyo-energy/energy-app-sdk';
2881
+ import {manualSetupGuide} from './src/onboarding/manual-setup.guide.js';
2882
+
2883
+ const packageDef = defineEnergyAppPackage({
2884
+ // ...
2885
+ files: [definePublicFile({name: 'modbus-menu', path: './assets/modbus-menu.png'})],
2886
+ onboardingGuide: manualSetupGuide
2887
+ });
2888
+
2889
+ const {ok, errors, warnings} = validatePackageOnboardingGuide(packageDef);
2890
+ ```
2891
+
2892
+ The complete authoring guide, with the reason behind each rule and a full example, is the
2893
+ JSDoc of `EnergyAppPackageDefinition.onboardingGuide`.
2894
+
2854
2895
  ## Firmware Update Registry
2855
2896
 
2856
2897
  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 +4320,7 @@ Each event carries the **complete** picture of the slot — render it as it arri
4279
4320
 
4280
4321
  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
4322
 
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.
4323
+ **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
4324
 
4284
4325
  **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
4326
 
@@ -4289,6 +4330,10 @@ New reason types came with this surface, so a skipped row can say *why* instead
4289
4330
  - `ElectricityPriceBelowThreshold` on a grid charge — the buy and the peak it is for: "Charging at 33.6 ct for the 17:00 peak at 43.7 ct."
4290
4331
  - `ScheduledOptimization` on a price hold (`Discharge 0`) — `sellStartIso` / `sellPricePerKwh`: "Stored energy kept for 17:00 (43.7 ct)." Not `ElectricityPriceBelowThreshold`, which would say power is cheap for a command that stops the battery.
4291
4332
 
4333
+ **Not enough power to start.** `BelowMinPower` (category `Contention`) carries `powerW` for the power that was available and `minPowerW` for the minimum the appliance needs to start — e.g. "Only 4.1 kW left — needs at least 4.2 kW to start."
4334
+
4335
+ **Left to its own control.** When the energy manager deliberately does not steer an appliance — e.g. a heat pump left alone at night on a static tariff — it states `SelfManaged` (category `Other`). This is the manager's choice not to steer, not the appliance overriding a plan (`ApplianceInitiatedDraw`) or declining power (`AppliancePaused`).
4336
+
4292
4337
  **Installation limits.** Power capped by a limit the owner or installer set — not the grid operator — carries `InstallationLimit` (category `Contention`) with `powerW` set to the ceiling and `context.installationLimit` = `{source, limitW}`, where `source` is `EnyoInstallationLimitSourceEnum.DesiredGridLimit` (`desiredKwLimit`) or `ChargerLimit` (`chargerLimitW`). `limitW` is in **W** even for the desired limit, which is configured in kW. `GridOperatorPowerLimitationActive` stays reserved for limits the grid operator imposed.
4293
4338
 
4294
4339
  **Grid-optimised charging.** A cost-optimised session placed in, or waiting for, a low-load hour because of `gridOptimizedEvCharging` carries `GridLoadOptimized` (category `Schedule`) with `context.placement` (chosen window and deadline) and `context.gridLoad` (`epexPricePerKwh`, `currency`). The EPEX price is a grid-load proxy, not what the customer pays, so the text must not promise a saving — e.g. "Charging from 02:00, when the grid is less busy — ready by 07:00."
@@ -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';
@@ -143,6 +143,43 @@ export interface EnyoChargerApplianceMetadata {
143
143
  * cannot switch phases.
144
144
  */
145
145
  phaseSwitchThresholdKw?: number;
146
+ /**
147
+ * Lowest charging current, in Amperes per phase, the charger can hold a
148
+ * session at — typically `6` (the IEC 61851 minimum), higher on chargers
149
+ * that cannot regulate down that far.
150
+ *
151
+ * The EMS must not issue a non-zero current limit below it; a session is
152
+ * held at the higher of this value and the vehicle's
153
+ * {@link EnyoVehicle.minChargingCurrentAmpere}. Omit when unknown.
154
+ */
155
+ minChargingCurrentAmpere?: number;
156
+ /**
157
+ * Whether a charging session on this charger must not be paused once it
158
+ * has started — e.g. chargers that end the session or need a re-plug
159
+ * when the current is set to `0`.
160
+ *
161
+ * When `true`, the EMS must not pause the session (setpoint `0`) but hold
162
+ * it at least at its minimum current until it is complete. Applies if
163
+ * either this or the vehicle's {@link EnyoVehicle.cannotBePaused} is
164
+ * `true`. Omitted means `false`.
165
+ *
166
+ * @default false
167
+ */
168
+ cannotBePaused?: boolean;
169
+ /**
170
+ * Whether the EMS must not switch phases (1 ↔ 3) on this charger, even
171
+ * though the hardware supports it (see {@link availablePhases}) — e.g.
172
+ * because the installer disabled it or switching proved unreliable.
173
+ *
174
+ * When `true`, sessions stay in the phase configuration they started in,
175
+ * and {@link phaseSwitchThresholdKw}, {@link phaseSwitchDelaySeconds} and
176
+ * {@link phaseSwitchIntervalSeconds} are not used. Applies if either this
177
+ * or the vehicle's {@link EnyoVehicle.phaseSwitchDisabled} is `true`.
178
+ * Omitted means `false`.
179
+ *
180
+ * @default false
181
+ */
182
+ phaseSwitchDisabled?: boolean;
146
183
  /**
147
184
  * Granularity, in Amperes, at which the charging current limit can be set
148
185
  * on this charger. Any current limit the EMS issues should be a multiple of
@@ -166,6 +203,26 @@ export interface EnyoChargerApplianceMetadata {
166
203
  * `ThreeToOnePhaseSwitch` feature)
167
204
  */
168
205
  availablePhases?: EnyoChargerAppliancePhase[];
206
+ /**
207
+ * Time, in seconds, the charger needs to carry out a phase switch after it
208
+ * has been triggered (e.g. 1 → 3 phases or 3 → 1 phase).
209
+ *
210
+ * During this delay the charger is still operating in the previous phase
211
+ * configuration; the EMS should not expect the new phase count to be in
212
+ * effect until it has elapsed. Only meaningful on chargers that can switch
213
+ * phases (see {@link availablePhases}). Omit when unknown.
214
+ */
215
+ phaseSwitchDelaySeconds?: number;
216
+ /**
217
+ * Minimum time, in seconds, the charger has to stay in a phase
218
+ * configuration after a phase switch before another phase switch is
219
+ * possible.
220
+ *
221
+ * The EMS must not trigger a new phase switch until this interval has
222
+ * passed since the last switch. Only meaningful on chargers that can switch
223
+ * phases (see {@link availablePhases}). Omit when unknown.
224
+ */
225
+ phaseSwitchIntervalSeconds?: number;
169
226
  /**
170
227
  * Whether the energy manager is allowed to actively control (steer) this
171
228
  * charger. When `false`, the charger is treated as read-only/monitor-only