@enyo-energy/energy-app-sdk 0.0.183 → 0.0.185

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +281 -0
  2. package/dist/cjs/energy-app-package-definition.cjs +12 -1
  3. package/dist/cjs/energy-app-package-definition.d.cts +155 -1
  4. package/dist/cjs/energy-app-permission.type.cjs +2 -0
  5. package/dist/cjs/energy-app-permission.type.d.cts +4 -2
  6. package/dist/cjs/energy-app.cjs +45 -0
  7. package/dist/cjs/energy-app.d.cts +42 -0
  8. package/dist/cjs/enyo-energy-app-sdk.d.cts +9 -0
  9. package/dist/cjs/implementations/device-test/device-test-validators.cjs +223 -0
  10. package/dist/cjs/implementations/device-test/device-test-validators.d.cts +84 -0
  11. package/dist/cjs/implementations/firmware/define-firmware-file.cjs +142 -0
  12. package/dist/cjs/implementations/firmware/define-firmware-file.d.cts +110 -0
  13. package/dist/cjs/implementations/firmware/firmware-validators.cjs +314 -0
  14. package/dist/cjs/implementations/firmware/firmware-validators.d.cts +98 -0
  15. package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.cjs +22 -0
  16. package/dist/cjs/implementations/onboarding-v2/define-onboarding-guide-v2.d.cts +17 -2
  17. package/dist/cjs/implementations/onboarding-v2/onboarding-v2-validators.cjs +43 -0
  18. package/dist/cjs/index.cjs +9 -0
  19. package/dist/cjs/index.d.cts +9 -0
  20. package/dist/cjs/packages/energy-app-device-test.cjs +2 -0
  21. package/dist/cjs/packages/energy-app-device-test.d.cts +123 -0
  22. package/dist/cjs/packages/energy-app-firmware-registry.cjs +2 -0
  23. package/dist/cjs/packages/energy-app-firmware-registry.d.cts +167 -0
  24. package/dist/cjs/packages/energy-app-savings.cjs +2 -0
  25. package/dist/cjs/packages/energy-app-savings.d.cts +126 -0
  26. package/dist/cjs/types/enyo-device-test.cjs +102 -0
  27. package/dist/cjs/types/enyo-device-test.d.cts +214 -0
  28. package/dist/cjs/types/enyo-firmware-registry.cjs +2 -0
  29. package/dist/cjs/types/enyo-firmware-registry.d.cts +113 -0
  30. package/dist/cjs/types/enyo-onboarding-v2.cjs +26 -1
  31. package/dist/cjs/types/enyo-onboarding-v2.d.cts +31 -1
  32. package/dist/cjs/types/enyo-savings.cjs +71 -0
  33. package/dist/cjs/types/enyo-savings.d.cts +395 -0
  34. package/dist/cjs/version.cjs +1 -1
  35. package/dist/cjs/version.d.cts +1 -1
  36. package/dist/energy-app-package-definition.d.ts +155 -1
  37. package/dist/energy-app-package-definition.js +11 -0
  38. package/dist/energy-app-permission.type.d.ts +4 -2
  39. package/dist/energy-app-permission.type.js +2 -0
  40. package/dist/energy-app.d.ts +42 -0
  41. package/dist/energy-app.js +45 -0
  42. package/dist/enyo-energy-app-sdk.d.ts +9 -0
  43. package/dist/implementations/device-test/device-test-validators.d.ts +84 -0
  44. package/dist/implementations/device-test/device-test-validators.js +216 -0
  45. package/dist/implementations/firmware/define-firmware-file.d.ts +110 -0
  46. package/dist/implementations/firmware/define-firmware-file.js +136 -0
  47. package/dist/implementations/firmware/firmware-validators.d.ts +98 -0
  48. package/dist/implementations/firmware/firmware-validators.js +308 -0
  49. package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.d.ts +17 -2
  50. package/dist/implementations/onboarding-v2/define-onboarding-guide-v2.js +23 -1
  51. package/dist/implementations/onboarding-v2/onboarding-v2-validators.js +44 -1
  52. package/dist/index.d.ts +9 -0
  53. package/dist/index.js +9 -0
  54. package/dist/packages/energy-app-device-test.d.ts +123 -0
  55. package/dist/packages/energy-app-device-test.js +1 -0
  56. package/dist/packages/energy-app-firmware-registry.d.ts +167 -0
  57. package/dist/packages/energy-app-firmware-registry.js +1 -0
  58. package/dist/packages/energy-app-savings.d.ts +126 -0
  59. package/dist/packages/energy-app-savings.js +1 -0
  60. package/dist/types/enyo-device-test.d.ts +214 -0
  61. package/dist/types/enyo-device-test.js +99 -0
  62. package/dist/types/enyo-firmware-registry.d.ts +113 -0
  63. package/dist/types/enyo-firmware-registry.js +1 -0
  64. package/dist/types/enyo-onboarding-v2.d.ts +31 -1
  65. package/dist/types/enyo-onboarding-v2.js +25 -0
  66. package/dist/types/enyo-savings.d.ts +395 -0
  67. package/dist/types/enyo-savings.js +68 -0
  68. package/dist/version.d.ts +1 -1
  69. package/dist/version.js +1 -1
  70. package/package.json +1 -1
package/README.md CHANGED
@@ -32,6 +32,12 @@ The official TypeScript SDK for building Energy Apps on the enyo platform. Creat
32
32
  - [NetworkAccessGuard](#networkaccessguard)
33
33
  - [NetworkDeviceManager](#networkdevicemanager)
34
34
  - [Startup pattern](#startup-pattern)
35
+ - [Firmware Update Registry](#firmware-update-registry)
36
+ - [Declaring firmware](#declaring-firmware)
37
+ - [Firmware modes](#firmware-modes)
38
+ - [Resolving the next version](#resolving-the-next-version)
39
+ - [Downloading a file](#downloading-a-file)
40
+ - [Validating the graph](#validating-the-graph)
35
41
  - [Retry Framework](#retry-framework)
36
42
  - [Device Integrations](#device-integrations)
37
43
  - [IntegrationEnergyApp (Base Class)](#integrationenergyapp-base-class)
@@ -346,6 +352,7 @@ Energy Apps use a granular permissions system to control access to system resour
346
352
  - **`PvForecastRegister`** / **`PvForecastUse`**: Publish / consume PV forecasts
347
353
  - **`DynamicPriceForecastRegister`** / **`DynamicPriceForecastUse`**: Publish / consume dynamic-price forecasts
348
354
  - **`PvSystemRegister`** / **`PvSystemUse`**: Register / read PV system configuration
355
+ - **`Savings`**: Publish and read back day-scoped savings reports
349
356
 
350
357
  #### Site & Identity Permissions
351
358
 
@@ -468,6 +475,66 @@ const inverters = await networkDevices.search({
468
475
  const deviceInfo = await networkDevices.getDeviceInfo(device.id);
469
476
  ```
470
477
 
478
+ #### `useDeviceTest(): EnergyAppDeviceTest`
479
+
480
+ Answer the host's "is this device yours, and did it yield appliances?" question.
481
+
482
+ Every other device API points outward — you scan, or you get notified, and you decide what to do next. This one is inverted: the host has detected a device it cannot identify on its own and calls **into** your app. Register one handler; it is used by the onboarding v2 `device-test` action, by background auto-detection, and by user-triggered re-tests (see `request.origin`).
483
+
484
+ ```typescript
485
+ await energyApp.useDeviceTest().registerDeviceTestHandler(async (request) => {
486
+ const devices: EnyoDeviceTestDeviceResult[] = [];
487
+ const appliances: EnyoDeviceTestApplianceResult[] = [];
488
+
489
+ for (const device of request.devices) {
490
+ const identity = await readVendorRegisters(device); // your protocol
491
+ if (!identity) {
492
+ devices.push({networkDeviceId: device.id, outcome: EnyoDeviceTestOutcomeEnum.NotSupported});
493
+ continue;
494
+ }
495
+
496
+ const existing = await findApplianceForSerial(identity.serialNumber);
497
+ const applianceId = await energyApp.useAppliances().save(buildAppliance(device, identity), existing?.id);
498
+
499
+ appliances.push({
500
+ applianceId,
501
+ applianceType: EnyoApplianceTypeEnum.Inverter,
502
+ disposition: existing
503
+ ? EnyoDeviceTestApplianceDispositionEnum.Updated
504
+ : EnyoDeviceTestApplianceDispositionEnum.Created,
505
+ networkDeviceId: device.id
506
+ });
507
+ devices.push({
508
+ networkDeviceId: device.id,
509
+ outcome: existing
510
+ ? EnyoDeviceTestOutcomeEnum.AppliancesAlreadyExisted
511
+ : EnyoDeviceTestOutcomeEnum.AppliancesCreated,
512
+ vendor: identity.vendor,
513
+ model: identity.model,
514
+ serialNumber: identity.serialNumber
515
+ });
516
+ }
517
+
518
+ return {
519
+ requestId: request.requestId,
520
+ outcome: aggregateDeviceTestOutcome(devices),
521
+ devices,
522
+ appliances
523
+ };
524
+ });
525
+ ```
526
+
527
+ Rules worth respecting:
528
+
529
+ - **The host owns the clock.** `request.timeoutMs` is your budget; the host stops waiting when it expires and treats the run as `failed`. Your handler is *not* told, so bound socket timeouts and retries to fit and never leave connections open past the point where an answer could matter.
530
+ - **One handler per package.** Registering again replaces the previous one. Register during startup — a request arriving before registration is answered `failed`, which in a guided run means the installer sees the failure branch.
531
+ - **Answer for every device.** A twelve-device scan needs twelve verdicts; "three are mine, one needs a password, eight are the neighbour's printer" is the normal case.
532
+ - **`created` vs `already-existed` are different answers.** Both are successes, but the guide branches differently — one is "set up", the other "already set up".
533
+ - **Derive the aggregate with `aggregateDeviceTestOutcome()`.** Success dominates a sibling device's failure, and actionable outcomes outrank unactionable ones; hand-rolling that precedence is where the bugs are.
534
+ - **No permission is needed to register**, but the work inside needs `NetworkDeviceAccess` to reach a device and `Appliance` to create one. Without them, answer `access-not-granted` rather than throwing.
535
+
536
+ Validate a result during development with `validateDeviceTestResult(result, request)` — it catches contradictions like `appliances-created` with no created appliance, which would otherwise route an installer to a success screen for appliances that do not exist.
537
+
471
538
  #### `useModbus(): EnergyAppModbus`
472
539
 
473
540
  Access Modbus communication:
@@ -1120,6 +1187,66 @@ diag.energyManagerDiagnostics(
1120
1187
  );
1121
1188
  ```
1122
1189
 
1190
+ #### `useSavings(): EnergyAppSavings`
1191
+
1192
+ Publish what the energy management saved the customer on a finished day, and read back which days were already reported.
1193
+
1194
+ The app settles a day by replaying its **measured** environment through a simulation of the same house running **uncontrolled**, and pricing both worlds against the tariff that actually applied. Publishing is an upsert keyed by `dayIso` + `method`, so a day may be recomputed after a backfill or a bugfix. The platform stores days and owns every aggregation above them (month, year, lifetime) — and excludes `Low` confidence days from those rollups.
1195
+
1196
+ ```typescript
1197
+ const savings = energyApp.useSavings();
1198
+
1199
+ // On boot: which days still need settling?
1200
+ const { missingDayIsos } = await savings.getDailySavings({
1201
+ startDayIso: '2026-07-01',
1202
+ endDayIso: '2026-07-31'
1203
+ });
1204
+
1205
+ for (const dayIso of missingDayIsos) {
1206
+ await savings.publishDailySavings({
1207
+ schemaVersion: 1,
1208
+ dayIso,
1209
+ timeZone: 'Europe/Berlin',
1210
+ dayStartUtcMs: startOfLocalDayUtcMs(dayIso),
1211
+ dayEndUtcMs: endOfLocalDayUtcMs(dayIso), // 23 or 25 h on DST days
1212
+ method: EnyoSavingsMethodEnum.Settled,
1213
+ computedAtIso: new Date().toISOString(),
1214
+ calculatorVersion: '3.2.0',
1215
+ confidence: EnyoSavingsConfidenceEnum.High,
1216
+ confidenceIssues: [],
1217
+ costs: {
1218
+ currency: EnyoCurrencyEnum.EUR,
1219
+ optimizedCost: 1.42,
1220
+ baselineCost: 3.07,
1221
+ savings: 1.65,
1222
+ savingsFromSelfConsumption: 1.12,
1223
+ savingsFromArbitrage: 0.53
1224
+ },
1225
+ energy: { /* both worlds, Wh */ },
1226
+ metrics: { /* both worlds */ },
1227
+ attribution: [
1228
+ { applianceType: EnergyAppApplianceTypeEnum.Charger, savings: 0.91, shiftedEnergyWh: 12400 }
1229
+ ],
1230
+ coverage: [
1231
+ { series: 'pv', source: EnyoSavingsDataSourceEnum.Measured, expectedBuckets: 96, presentBuckets: 96 }
1232
+ ],
1233
+ assumptions: [
1234
+ { key: 'battery.dischargeEfficiency', value: 0.95 }
1235
+ ]
1236
+ });
1237
+ }
1238
+ ```
1239
+
1240
+ Notes worth respecting when producing a report:
1241
+
1242
+ - **Publish both worlds, never only the delta.** A lone savings number is unauditable and cannot be re-aggregated.
1243
+ - **Assumptions travel with the report.** The counterfactual rests on guesses; without them, changing a default silently rewrites history.
1244
+ - **`dayIso` is a local calendar date.** The IANA zone and the exact UTC bounds go alongside — anything assuming 96 buckets is wrong twice a year.
1245
+ - **Per-slot detail (`slots`) is opt-in.** Settlement is stateless, so the app can regenerate it on demand; only publish it for days under investigation.
1246
+ - **Units follow the platform:** energy in Wh, power in W, prices per kWh, currency as `EnyoCurrencyEnum`.
1247
+
1248
+ Requires the `Savings` permission.
1249
+
1123
1250
  ### Operational Utilities
1124
1251
 
1125
1252
  #### `useOnboarding(): EnergyAppOnboarding`
@@ -1567,6 +1694,160 @@ async function connectDevice(networkDeviceId: string) {
1567
1694
 
1568
1695
  This pattern matches the wiring used by real Sungrow / Fronius energy-app packages: one `NetworkDeviceManager` per package, `ensureAccess` before every connect, `withAccessGuard` around every poll, and a single `getDevices({ accessStatus: 'granted' })` pass at startup to cover the warm-restart case.
1569
1696
 
1697
+ ## Firmware Update Registry
1698
+
1699
+ 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.
1700
+
1701
+ Requires the `FirmwareRegistry` permission.
1702
+
1703
+ **Firmware versions are opaque strings.** A version is whatever the vendor calls it — `2.4.1`, `2024-11-rc3`, `A7F2` — and is never parsed, ordered, or compared beyond exact equality. Nothing can be derived from the string itself, so the update order is declared rather than computed. `firmwareMode` picks which form that takes.
1704
+
1705
+ ### Firmware modes
1706
+
1707
+ | `firmwareMode` | Order comes from | Use when |
1708
+ |---|---|---|
1709
+ | `'latest'` *(default)* | **Declaration order** — the last entry declared for the device's model is always the one offered | Devices accept any image directly |
1710
+ | `'dependent'` | The explicit **`installForFirmwareVersion`** edges on each entry | Devices must be stepped through intermediate versions |
1711
+
1712
+ ### Declaring firmware
1713
+
1714
+ `firmwareMode: 'latest'` — a plain list, last one wins:
1715
+
1716
+ ```typescript
1717
+ import { defineEnergyAppPackage, defineFirmwareFile, EnergyAppPackageFirmwareModeEnum, EnergyAppPermissionTypeEnum } from '@enyo-energy/energy-app-sdk';
1718
+
1719
+ export default defineEnergyAppPackage({
1720
+ version: '1',
1721
+ packageName: 'acme-wallbox',
1722
+ // ...
1723
+ permissions: [EnergyAppPermissionTypeEnum.FirmwareRegistry],
1724
+ firmwareMode: EnergyAppPackageFirmwareModeEnum.Latest,
1725
+ firmware: [
1726
+ defineFirmwareFile({
1727
+ fileId: 'ac22-2024-11',
1728
+ path: './firmware/ac22-2024-11.bin',
1729
+ firmwareVersion: '2024-11-rc3',
1730
+ modelNames: ['AC-22-Pro']
1731
+ }),
1732
+ defineFirmwareFile({
1733
+ fileId: 'ac22-current',
1734
+ path: './firmware/ac22-current.bin',
1735
+ firmwareVersion: 'A7F2',
1736
+ modelNames: ['AC-22-Pro']
1737
+ })
1738
+ // ↑ last declared for AC-22-Pro — every AC-22-Pro is offered this one
1739
+ ]
1740
+ });
1741
+ ```
1742
+
1743
+ `firmwareMode: 'dependent'` — each entry declares the versions it installs *for*:
1744
+
1745
+ ```typescript
1746
+ firmwareMode: EnergyAppPackageFirmwareModeEnum.Dependent,
1747
+ firmware: [
1748
+ defineFirmwareFile({
1749
+ fileId: 'ac22-baseline',
1750
+ path: './firmware/ac22-2024-11-rc3.bin',
1751
+ firmwareVersion: '2024-11-rc3',
1752
+ modelNames: ['AC-22-Pro'],
1753
+ // offered to devices whose reported version matches no declared entry
1754
+ fallbackForUnknownVersion: true
1755
+ }),
1756
+ defineFirmwareFile({
1757
+ fileId: 'ac22-hotfix-a',
1758
+ path: './firmware/ac22-hotfix-a.bin',
1759
+ firmwareVersion: 'A7F2',
1760
+ installForFirmwareVersion: ['2024-11-rc3'],
1761
+ modelNames: ['AC-22-Pro']
1762
+ }),
1763
+ defineFirmwareFile({
1764
+ fileId: 'ac22-stable',
1765
+ path: './firmware/ac22-stable.bin',
1766
+ firmwareVersion: '1.0',
1767
+ // collapses two old versions into one image
1768
+ installForFirmwareVersion: ['A7F2', 'legacy-b'],
1769
+ modelNames: ['AC-22-Pro'],
1770
+ releaseNotes: [
1771
+ { language: 'en', value: 'Fixes phase rotation detection.' },
1772
+ { language: 'de', value: 'Behebt die Erkennung der Phasenlage.' }
1773
+ ]
1774
+ })
1775
+ ]
1776
+ ```
1777
+
1778
+ `installForFirmwareVersion` is the whole graph. Three entries each naming their predecessor form a chain; two entries naming the same predecessor for *different* models form a branch; one entry naming several predecessors merges old versions into a single image. Omitting it makes an entry a root that is never offered as an update to a known version. Under `'latest'` the field is ignored (and the validator warns if you set it).
1779
+
1780
+ `modelNames` scopes resolution in both modes — an entry without it applies to every model the package supports.
1781
+
1782
+ ### Resolving the next version
1783
+
1784
+ ```typescript
1785
+ const registry = energyApp.useFirmwareRegistry();
1786
+
1787
+ const next = await registry.getNextFirmware(device.reportedVersion, { modelName: 'AC-22-Pro' });
1788
+ if (!next) {
1789
+ // Already up to date — the normal outcome, not an error.
1790
+ return;
1791
+ }
1792
+ console.log(`Update available: ${next.firmwareVersion} (${next.sizeBytes} bytes)`);
1793
+ ```
1794
+
1795
+ Under `'dependent'`, `getNextFirmware()` returns **one hop**, not the destination. After the device installs the image and reports its new version, call again to continue the chain — each step is verified on the device before the next is offered. When you need the whole chain up front:
1796
+
1797
+ ```typescript
1798
+ const path = await registry.getFirmwareUpdatePath(device.reportedVersion, { modelName: 'AC-22-Pro' });
1799
+ const totalMb = path.reduce((sum, file) => sum + file.sizeBytes, 0) / 1_000_000;
1800
+ console.log(`${path.length} updates pending, ${totalMb.toFixed(1)} MB total`);
1801
+ ```
1802
+
1803
+ Under `'latest'` there is no chain to walk: `getNextFirmware()` hands back the last declared entry for the model until the device runs it, and `getFirmwareUpdatePath()` never returns more than one entry.
1804
+
1805
+ ### Downloading a file
1806
+
1807
+ Firmware images are large, so the bytes never cross the app/host boundary. Request a signed, time-limited URL instead — one that many wallboxes and inverters can fetch themselves:
1808
+
1809
+ ```typescript
1810
+ const download = await registry.requestDownloadUrl(next.fileId, { ttlSeconds: 900 });
1811
+
1812
+ // Either hand the URL to the device...
1813
+ await device.installFirmwareFromUrl(download.url, download.sha256);
1814
+
1815
+ // ...or stream it into the app.
1816
+ const response = await fetch(download.url);
1817
+ ```
1818
+
1819
+ Two rules:
1820
+
1821
+ - **Request the URL at the moment of use.** It expires at `download.expiresAt` (epoch ms) and the storage backend then rejects it. Never cache or persist it.
1822
+ - **Always verify `sha256`** against the downloaded bytes before flashing.
1823
+
1824
+ ### Validating the graph
1825
+
1826
+ An ambiguous or cyclic `'dependent'` graph has no correct resolution at runtime, so validate before releasing:
1827
+
1828
+ ```typescript
1829
+ import { validateFirmwareRegistry, assertValidFirmwareRegistry } from '@enyo-energy/energy-app-sdk';
1830
+
1831
+ const result = validateFirmwareRegistry(packageDefinition);
1832
+ if (!result.ok) console.error(result.errors);
1833
+ console.warn(result.warnings);
1834
+
1835
+ assertValidFirmwareRegistry(packageDefinition); // or throw on the first failure
1836
+ ```
1837
+
1838
+ Blocking errors in both modes: duplicate `fileId`s, two entries installing the same version for overlapping models, and declaring firmware without the `FirmwareRegistry` permission. Under `'dependent'` additionally: two entries installing for the same current version on overlapping models, cycles, an entry listing its own version in `installForFirmwareVersion`, and multiple fallbacks per model.
1839
+
1840
+ Warnings: under `'latest'`, entries that declare `installForFirmwareVersion` or `fallbackForUnknownVersion` — both are ignored there, so declaring them usually means `'dependent'` was intended. Under `'dependent'`: a source version matching no declared entry — which is exactly how you attach a chain to firmware that shipped before this registry existed — and entries that are neither reachable nor a fallback. In both modes: models or vendors missing from `compatibility`.
1841
+
1842
+ For local unit tests, `resolveNextFirmware()` and `resolveFirmwareUpdatePath()` run the same resolution the host performs, against a plain array of entries:
1843
+
1844
+ ```typescript
1845
+ resolveNextFirmware(definition.firmware ?? [], currentVersion, {
1846
+ modelName: 'AC-22-Pro',
1847
+ firmwareMode: definition.firmwareMode
1848
+ });
1849
+ ```
1850
+
1570
1851
  ## Retry Framework
1571
1852
 
1572
1853
  `RetryManager` centralises retry / backoff / circuit-breaker logic so polling loops don't have to reinvent it. Register one entry per logical operation, give it a `RetryPolicy`, and run attempts through `execute(id, fn)` — the manager handles attempt counting, exponential backoff, transition into `Open` after repeated failures, and recovery on the next success.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EnergyAppPackageCategory = void 0;
3
+ exports.EnergyAppPackageFirmwareModeEnum = exports.EnergyAppPackageCategory = void 0;
4
4
  exports.defineEnergyAppPackage = defineEnergyAppPackage;
5
5
  const version_js_1 = require("./version.cjs");
6
6
  var EnergyAppPackageCategory;
@@ -20,6 +20,17 @@ var EnergyAppPackageCategory;
20
20
  EnergyAppPackageCategory["HeatingRod"] = "heating-rod";
21
21
  EnergyAppPackageCategory["Other"] = "other";
22
22
  })(EnergyAppPackageCategory || (exports.EnergyAppPackageCategory = EnergyAppPackageCategory = {}));
23
+ /**
24
+ * Enum form of {@link EnergyAppPackageFirmwareMode} for use in package
25
+ * definitions.
26
+ */
27
+ var EnergyAppPackageFirmwareModeEnum;
28
+ (function (EnergyAppPackageFirmwareModeEnum) {
29
+ /** Always offer the last declared firmware entry for the device's model. */
30
+ EnergyAppPackageFirmwareModeEnum["Latest"] = "latest";
31
+ /** Follow the explicit `installForFirmwareVersion` upgrade graph. */
32
+ EnergyAppPackageFirmwareModeEnum["Dependent"] = "dependent";
33
+ })(EnergyAppPackageFirmwareModeEnum || (exports.EnergyAppPackageFirmwareModeEnum = EnergyAppPackageFirmwareModeEnum = {}));
23
34
  /**
24
35
  * Defines an Energy App package with automatic SDK version injection.
25
36
  * This function automatically adds the current SDK version to the package definition
@@ -1,5 +1,6 @@
1
1
  import { EnergyAppPermissionType } from "./energy-app-permission.type.cjs";
2
2
  import { EnergyAppModelFeatureEnum } from "./energy-app-model-feature.enum.cjs";
3
+ import type { EnyoFileTranslation } from "./types/enyo-file.cjs";
3
4
  export type EnergyAppPackageLanguage = 'de' | 'en';
4
5
  export declare enum EnergyAppPackageCategory {
5
6
  Inverter = "inverter",
@@ -219,7 +220,12 @@ export interface EnergyAppPackageCompatibilityModel {
219
220
  displayName?: string;
220
221
  /**
221
222
  * Optional minimum firmware version the package supports for this model.
222
- * Free-form string compared lexicographically by hosts that need it.
223
+ *
224
+ * A free-form, vendor-defined string used for display and internal
225
+ * documentation only. It is **not** an ordering primitive: firmware versions
226
+ * are opaque and are never parsed or compared by the SDK or the host. To
227
+ * express which firmware can be updated to which, declare the explicit
228
+ * upgrade graph in {@link EnergyAppPackageDefinition.firmware} instead.
223
229
  */
224
230
  minimumFirmwareVersion?: string;
225
231
  /** Optional internal note explaining model-specific caveats or limitations */
@@ -243,6 +249,134 @@ export interface EnergyAppPackageCompatibilityVendor {
243
249
  /** Models from this vendor that the package supports */
244
250
  models: EnergyAppPackageCompatibilityModel[];
245
251
  }
252
+ /**
253
+ * How the firmware registry decides which image a device should install next.
254
+ *
255
+ * - `'latest'` — every device is offered the **last declared** firmware entry
256
+ * that applies to its model, whatever version it currently runs. Since
257
+ * firmware versions are opaque strings and cannot be ordered, "latest" means
258
+ * last in the `firmware` array — declaration order *is* the order. Use this
259
+ * for devices that accept any image directly; `installForFirmwareVersion` is
260
+ * ignored.
261
+ * - `'dependent'` — the update order is the explicit graph declared through
262
+ * {@link EnergyAppPackageFirmwareFile.installForFirmwareVersion}: each image
263
+ * names the versions it can be installed on top of, and the registry walks
264
+ * that graph one hop at a time. Use this for devices that must be stepped
265
+ * through intermediate versions.
266
+ */
267
+ export type EnergyAppPackageFirmwareMode = 'latest' | 'dependent';
268
+ /**
269
+ * Enum form of {@link EnergyAppPackageFirmwareMode} for use in package
270
+ * definitions.
271
+ */
272
+ export declare enum EnergyAppPackageFirmwareModeEnum {
273
+ /** Always offer the last declared firmware entry for the device's model. */
274
+ Latest = "latest",
275
+ /** Follow the explicit `installForFirmwareVersion` upgrade graph. */
276
+ Dependent = "dependent"
277
+ }
278
+ /**
279
+ * A firmware image published together with an Energy App package.
280
+ *
281
+ * The file is declared here by its local `path`; the enyo CLI uploads it during
282
+ * `enyo release`, replacing the path with a registry reference, so the released
283
+ * package tarball never carries the bytes. At runtime the app reaches the
284
+ * uploaded image through {@link EnergyAppFirmwareRegistry}.
285
+ *
286
+ * **Firmware versions are opaque strings.** `firmwareVersion` is whatever the
287
+ * vendor calls it — `'2.4.1'`, `'2024-11-rc3'`, `'A7F2'` — and is never parsed,
288
+ * ordered or compared beyond exact string equality. Nothing can therefore be
289
+ * derived from the string itself, which is why the order comes from
290
+ * {@link EnergyAppPackageDefinition.firmwareMode}: declaration order under
291
+ * `'latest'`, or the explicit {@link installForFirmwareVersion} edges under
292
+ * `'dependent'`.
293
+ *
294
+ * Under `'dependent'`, chains, branches and merges all fall out of that one
295
+ * field: three entries each naming their predecessor form a chain; two entries
296
+ * naming the same predecessor for different models form a branch; one entry
297
+ * naming several predecessors collapses old versions into a single image.
298
+ *
299
+ * Validate the declaration with `validateFirmwareRegistry()` before releasing —
300
+ * an ambiguous or cyclic graph has no correct resolution and is rejected rather
301
+ * than silently resolved.
302
+ *
303
+ * @example
304
+ * ```typescript
305
+ * // firmwareMode: 'dependent' — stepped through intermediate versions
306
+ * firmware: [
307
+ * defineFirmwareFile({
308
+ * fileId: 'ac22-baseline',
309
+ * path: './firmware/ac22-2024-11-rc3.bin',
310
+ * firmwareVersion: '2024-11-rc3',
311
+ * modelNames: ['AC-22-Pro'],
312
+ * fallbackForUnknownVersion: true
313
+ * }),
314
+ * defineFirmwareFile({
315
+ * fileId: 'ac22-hotfix-a',
316
+ * path: './firmware/ac22-hotfix-a.bin',
317
+ * firmwareVersion: 'hotfix-a',
318
+ * installForFirmwareVersion: ['2024-11-rc3'],
319
+ * modelNames: ['AC-22-Pro']
320
+ * })
321
+ * ]
322
+ * ```
323
+ */
324
+ export interface EnergyAppPackageFirmwareFile {
325
+ /**
326
+ * Stable, app-chosen identifier for this firmware image. Used as the lookup
327
+ * key at runtime and must be unique within the package.
328
+ */
329
+ fileId: string;
330
+ /**
331
+ * Path to the firmware file relative to the package root, e.g.
332
+ * `'./firmware/wallbox-2.4.1.bin'`. Resolved and uploaded by the enyo CLI on
333
+ * release; the published definition carries a registry reference instead.
334
+ */
335
+ path: string;
336
+ /**
337
+ * The firmware version this file installs. An opaque, vendor-defined string
338
+ * that is only ever equality-matched — never parsed or ordered.
339
+ */
340
+ firmwareVersion: string;
341
+ /**
342
+ * The versions this image is installed for — the incoming edges of this node
343
+ * in the upgrade graph. Each string is matched by exact equality against the
344
+ * version a device reports as currently installed; when it matches, this
345
+ * image is the device's next step.
346
+ *
347
+ * Only meaningful when {@link EnergyAppPackageDefinition.firmwareMode} is
348
+ * `'dependent'`; ignored under `'latest'`.
349
+ *
350
+ * Omit (or leave empty) for a root entry: one that heads a chain and is
351
+ * never offered as an update to a known version.
352
+ */
353
+ installForFirmwareVersion?: string[];
354
+ /**
355
+ * When true, this image is offered to devices whose reported version matches
356
+ * no declared node — a recovery or baseline image. At most one entry per
357
+ * model may set this.
358
+ *
359
+ * Only meaningful under `firmwareMode: 'dependent'`; ignored under
360
+ * `'latest'`, where every unrecognised version already receives the last
361
+ * declared image.
362
+ */
363
+ fallbackForUnknownVersion?: boolean;
364
+ /**
365
+ * Optional vendor this firmware belongs to. Should match a `vendorName` from
366
+ * {@link EnergyAppPackageDefinition.compatibility}.
367
+ */
368
+ vendorName?: string;
369
+ /**
370
+ * Optional models this firmware applies to. Scopes graph resolution: an
371
+ * update is only offered to a device whose model is listed here. Omit when
372
+ * the image applies to every model the package supports.
373
+ */
374
+ modelNames?: string[];
375
+ /** Optional translated release notes shown in the host UI. */
376
+ releaseNotes?: EnyoFileTranslation[];
377
+ /** Optional internal note explaining this image; never shown to users. */
378
+ internalComment?: string;
379
+ }
246
380
  /**
247
381
  * Complete definition for a enyo Energy App package.
248
382
  * This interface defines all the metadata, permissions, and configuration
@@ -277,6 +411,26 @@ export interface EnergyAppPackageDefinition {
277
411
  * single vendor implicitly or has no fixed compatibility surface.
278
412
  */
279
413
  compatibility: EnergyAppPackageCompatibilityVendor[];
414
+ /**
415
+ * Firmware images shipped with this package, declared as local file paths
416
+ * and uploaded by the enyo CLI on release.
417
+ *
418
+ * The app reaches them at runtime through
419
+ * {@link EnergyAppFirmwareRegistry}; how the next image is chosen is set by
420
+ * {@link firmwareMode}. Requires the `FirmwareRegistry` permission. Omit for
421
+ * packages that do not distribute firmware.
422
+ */
423
+ firmware?: EnergyAppPackageFirmwareFile[];
424
+ /**
425
+ * How the registry picks the next image for a device. Defaults to
426
+ * `'latest'`, which always offers the last declared entry for the device's
427
+ * model. Set to `'dependent'` when devices must be stepped through
428
+ * intermediate versions — the order is then taken from each entry's
429
+ * `installForFirmwareVersion` edges.
430
+ *
431
+ * Only relevant when {@link firmware} is declared.
432
+ */
433
+ firmwareMode?: EnergyAppPackageFirmwareMode;
280
434
  }
281
435
  /**
282
436
  * Defines an Energy App package with automatic SDK version injection.
@@ -50,4 +50,6 @@ var EnergyAppPermissionTypeEnum;
50
50
  EnergyAppPermissionTypeEnum["Udp"] = "Udp";
51
51
  EnergyAppPermissionTypeEnum["ProvidedFiles"] = "ProvidedFiles";
52
52
  EnergyAppPermissionTypeEnum["Automation"] = "Automation";
53
+ EnergyAppPermissionTypeEnum["Savings"] = "Savings";
54
+ EnergyAppPermissionTypeEnum["FirmwareRegistry"] = "FirmwareRegistry";
53
55
  })(EnergyAppPermissionTypeEnum || (exports.EnergyAppPermissionTypeEnum = EnergyAppPermissionTypeEnum = {}));
@@ -1,4 +1,4 @@
1
- export type EnergyAppPermissionType = 'RestrictedInternetAccess' | 'NetworkDeviceDiscovery' | 'NetworkDeviceSearch' | 'NetworkDeviceAccess' | 'AllNetworkDeviceAccess' | 'Modbus' | 'Storage' | 'Appliance' | 'AllAppliances' | 'SendDataBusValues' | 'SubscribeDataBus' | 'SendDataBusCommands' | 'OcppServer' | 'ChargingCard' | 'Vehicle' | 'Charge' | 'SecretManager' | 'LocationZipCode' | 'LocationCoordinates' | 'Timeseries' | 'EnergyManagerInfo' | 'ElectricityTariff' | 'WeatherForecastRegister' | 'WeatherForecastUse' | 'PvForecastRegister' | 'PvForecastUse' | 'DynamicPriceForecastRegister' | 'DynamicPriceForecastUse' | 'PvSystemRegister' | 'PvSystemUse' | 'InverterControlCommands' | 'BatteryControlCommands' | 'BatteryStorageState' | 'ChargerControlCommands' | 'ModbusRtu' | 'EnergyPrices' | 'EnergyManager' | 'EebusDeviceManagement' | 'EebusDataAccess' | 'EebusControl' | 'Mqtt' | 'Bluetooth' | 'Wifi' | 'ChildProcess' | 'Udp' | 'ProvidedFiles' | 'Automation';
1
+ export type EnergyAppPermissionType = 'RestrictedInternetAccess' | 'NetworkDeviceDiscovery' | 'NetworkDeviceSearch' | 'NetworkDeviceAccess' | 'AllNetworkDeviceAccess' | 'Modbus' | 'Storage' | 'Appliance' | 'AllAppliances' | 'SendDataBusValues' | 'SubscribeDataBus' | 'SendDataBusCommands' | 'OcppServer' | 'ChargingCard' | 'Vehicle' | 'Charge' | 'SecretManager' | 'LocationZipCode' | 'LocationCoordinates' | 'Timeseries' | 'EnergyManagerInfo' | 'ElectricityTariff' | 'WeatherForecastRegister' | 'WeatherForecastUse' | 'PvForecastRegister' | 'PvForecastUse' | 'DynamicPriceForecastRegister' | 'DynamicPriceForecastUse' | 'PvSystemRegister' | 'PvSystemUse' | 'InverterControlCommands' | 'BatteryControlCommands' | 'BatteryStorageState' | 'ChargerControlCommands' | 'ModbusRtu' | 'EnergyPrices' | 'EnergyManager' | 'EebusDeviceManagement' | 'EebusDataAccess' | 'EebusControl' | 'Mqtt' | 'Bluetooth' | 'Wifi' | 'ChildProcess' | 'Udp' | 'ProvidedFiles' | 'Automation' | 'Savings' | 'FirmwareRegistry';
2
2
  export declare enum EnergyAppPermissionTypeEnum {
3
3
  RestrictedInternetAccess = "RestrictedInternetAccess",
4
4
  NetworkDeviceDiscovery = "NetworkDeviceDiscovery",
@@ -46,5 +46,7 @@ export declare enum EnergyAppPermissionTypeEnum {
46
46
  ChildProcess = "ChildProcess",
47
47
  Udp = "Udp",
48
48
  ProvidedFiles = "ProvidedFiles",
49
- Automation = "Automation"
49
+ Automation = "Automation",
50
+ Savings = "Savings",
51
+ FirmwareRegistry = "FirmwareRegistry"
50
52
  }
@@ -339,6 +339,23 @@ class EnergyApp {
339
339
  useFiles() {
340
340
  return this.energyAppSdk.useFiles();
341
341
  }
342
+ /**
343
+ * Gets the Firmware Registry API for the firmware images published with this
344
+ * package.
345
+ *
346
+ * Firmware versions are opaque vendor strings, so the update order is
347
+ * declared explicitly in the package definition as an upgrade graph. Call
348
+ * `getNextFirmware(currentVersion)` to resolve the single next step for a
349
+ * device — it returns `undefined` when the device is already up to date —
350
+ * then `requestDownloadUrl()` for a signed, time-limited URL the device can
351
+ * fetch itself.
352
+ * @returns The Firmware Registry API instance
353
+ * @throws {EnergyAppPermissionNotGrantedError} If the `FirmwareRegistry`
354
+ * permission is not granted.
355
+ */
356
+ useFirmwareRegistry() {
357
+ return this.energyAppSdk.useFirmwareRegistry();
358
+ }
342
359
  /**
343
360
  * Gets the Automation API for reading user-configured automations and
344
361
  * listening for their creation, update and removal. Apps holding the
@@ -352,6 +369,34 @@ class EnergyApp {
352
369
  useAutomations() {
353
370
  return this.energyAppSdk.useAutomations();
354
371
  }
372
+ /**
373
+ * Gets the Savings API for publishing day-scoped savings reports and reading
374
+ * them back. An energy manager app settles a finished day by replaying its
375
+ * measured environment against an uncontrolled simulation and publishes both
376
+ * worlds here; the platform stores the days and owns every aggregation above
377
+ * them (month, year, lifetime).
378
+ * @returns The Savings API instance
379
+ * @throws {EnergyAppPermissionNotGrantedError} If the `Savings` permission is
380
+ * not granted.
381
+ */
382
+ useSavings() {
383
+ return this.energyAppSdk.useSavings();
384
+ }
385
+ /**
386
+ * Gets the Device Test API for answering the host's requests to test
387
+ * detected network devices. The app registers one handler; the host calls it
388
+ * from an onboarding v2 `device-test` action, from background auto-detection,
389
+ * or from a user-triggered re-test, and branches on whether appliances were
390
+ * found or created.
391
+ *
392
+ * Available to every app — this API is not permission-gated, though reaching
393
+ * a device (`NetworkDeviceAccess`) and creating an appliance (`Appliance`)
394
+ * still are.
395
+ * @returns The Device Test API instance
396
+ */
397
+ useDeviceTest() {
398
+ return this.energyAppSdk.useDeviceTest();
399
+ }
355
400
  /**
356
401
  * Gets the current SDK version.
357
402
  * @returns The semantic version string of the SDK
@@ -38,7 +38,10 @@ import { EnergyAppConfigurationManager } from "./packages/energy-app-configurati
38
38
  import { EnergyAppApplianceEnergyManagerForecast } from "./packages/energy-app-appliance-energy-manager-forecast.cjs";
39
39
  import { EnergyAppBattery } from "./packages/energy-app-battery.cjs";
40
40
  import { EnergyAppFile } from "./packages/energy-app-file.cjs";
41
+ import { EnergyAppFirmwareRegistry } from "./packages/energy-app-firmware-registry.cjs";
41
42
  import { EnergyAppAutomation } from "./packages/energy-app-automation.cjs";
43
+ import { EnergyAppSavings } from "./packages/energy-app-savings.cjs";
44
+ import { EnergyAppDeviceTest } from "./packages/energy-app-device-test.cjs";
42
45
  import { UseFetchOptions } from "./types/enyo-fetch.cjs";
43
46
  /**
44
47
  * Concrete implementation of {@link EnyoEnergyAppSdk} that delegates every call
@@ -270,6 +273,21 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
270
273
  * permission is not granted.
271
274
  */
272
275
  useFiles(): EnergyAppFile;
276
+ /**
277
+ * Gets the Firmware Registry API for the firmware images published with this
278
+ * package.
279
+ *
280
+ * Firmware versions are opaque vendor strings, so the update order is
281
+ * declared explicitly in the package definition as an upgrade graph. Call
282
+ * `getNextFirmware(currentVersion)` to resolve the single next step for a
283
+ * device — it returns `undefined` when the device is already up to date —
284
+ * then `requestDownloadUrl()` for a signed, time-limited URL the device can
285
+ * fetch itself.
286
+ * @returns The Firmware Registry API instance
287
+ * @throws {EnergyAppPermissionNotGrantedError} If the `FirmwareRegistry`
288
+ * permission is not granted.
289
+ */
290
+ useFirmwareRegistry(): EnergyAppFirmwareRegistry;
273
291
  /**
274
292
  * Gets the Automation API for reading user-configured automations and
275
293
  * listening for their creation, update and removal. Apps holding the
@@ -281,6 +299,30 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
281
299
  * not granted.
282
300
  */
283
301
  useAutomations(): EnergyAppAutomation;
302
+ /**
303
+ * Gets the Savings API for publishing day-scoped savings reports and reading
304
+ * them back. An energy manager app settles a finished day by replaying its
305
+ * measured environment against an uncontrolled simulation and publishes both
306
+ * worlds here; the platform stores the days and owns every aggregation above
307
+ * them (month, year, lifetime).
308
+ * @returns The Savings API instance
309
+ * @throws {EnergyAppPermissionNotGrantedError} If the `Savings` permission is
310
+ * not granted.
311
+ */
312
+ useSavings(): EnergyAppSavings;
313
+ /**
314
+ * Gets the Device Test API for answering the host's requests to test
315
+ * detected network devices. The app registers one handler; the host calls it
316
+ * from an onboarding v2 `device-test` action, from background auto-detection,
317
+ * or from a user-triggered re-test, and branches on whether appliances were
318
+ * found or created.
319
+ *
320
+ * Available to every app — this API is not permission-gated, though reaching
321
+ * a device (`NetworkDeviceAccess`) and creating an appliance (`Appliance`)
322
+ * still are.
323
+ * @returns The Device Test API instance
324
+ */
325
+ useDeviceTest(): EnergyAppDeviceTest;
284
326
  /**
285
327
  * Gets the current SDK version.
286
328
  * @returns The semantic version string of the SDK