@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 +46 -1
- package/dist/cjs/energy-app-package-definition.d.cts +142 -0
- package/dist/cjs/implementations/onboarding-v2/package-onboarding-guide-validators.cjs +213 -0
- package/dist/cjs/implementations/onboarding-v2/package-onboarding-guide-validators.d.cts +75 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/types/enyo-charger-appliance.d.cts +57 -0
- package/dist/cjs/types/enyo-data-bus-value.cjs +49 -1
- package/dist/cjs/types/enyo-data-bus-value.d.cts +81 -5
- package/dist/cjs/types/enyo-vehicle.d.cts +37 -0
- package/dist/cjs/version.cjs +1 -1
- package/dist/cjs/version.d.cts +1 -1
- package/dist/energy-app-package-definition.d.ts +142 -0
- package/dist/implementations/onboarding-v2/package-onboarding-guide-validators.d.ts +75 -0
- package/dist/implementations/onboarding-v2/package-onboarding-guide-validators.js +207 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types/enyo-charger-appliance.d.ts +57 -0
- package/dist/types/enyo-data-bus-value.d.ts +81 -5
- package/dist/types/enyo-data-bus-value.js +49 -1
- package/dist/types/enyo-vehicle.d.ts +37 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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;
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -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);
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -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
|