@enyo-energy/energy-app-sdk 1.18.0 → 1.20.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.
Files changed (96) hide show
  1. package/README.md +407 -2
  2. package/dist/cjs/energy-app-model-feature.enum.cjs +13 -0
  3. package/dist/cjs/energy-app-model-feature.enum.d.cts +12 -0
  4. package/dist/cjs/energy-app-package-definition.cjs +6 -0
  5. package/dist/cjs/energy-app-package-definition.d.cts +17 -1
  6. package/dist/cjs/energy-app-permission.type.cjs +2 -0
  7. package/dist/cjs/energy-app-permission.type.d.cts +3 -1
  8. package/dist/cjs/energy-app.cjs +17 -1
  9. package/dist/cjs/energy-app.d.cts +18 -2
  10. package/dist/cjs/enyo-energy-app-environment.cjs +20 -0
  11. package/dist/cjs/enyo-energy-app-environment.d.cts +16 -0
  12. package/dist/cjs/enyo-energy-app-sdk.d.cts +11 -2
  13. package/dist/cjs/implementations/calibration/calibration-validators.cjs +292 -0
  14. package/dist/cjs/implementations/calibration/calibration-validators.d.cts +96 -0
  15. package/dist/cjs/implementations/energy-distribution/energy-distribution-progress.cjs +120 -0
  16. package/dist/cjs/implementations/energy-distribution/energy-distribution-progress.d.cts +93 -0
  17. package/dist/cjs/implementations/energy-distribution/energy-distribution-snapshot-builder.cjs +160 -0
  18. package/dist/cjs/implementations/energy-distribution/energy-distribution-snapshot-builder.d.cts +168 -0
  19. package/dist/cjs/implementations/energy-distribution/energy-distribution-validators.cjs +261 -0
  20. package/dist/cjs/implementations/energy-distribution/energy-distribution-validators.d.cts +46 -0
  21. package/dist/cjs/index.cjs +9 -0
  22. package/dist/cjs/index.d.cts +9 -0
  23. package/dist/cjs/packages/energy-app-authentication.d.cts +10 -0
  24. package/dist/cjs/packages/energy-app-calibration.cjs +2 -0
  25. package/dist/cjs/packages/energy-app-calibration.d.cts +226 -0
  26. package/dist/cjs/packages/energy-app-energy-manager.d.cts +121 -0
  27. package/dist/cjs/packages/energy-app-vehicle.d.cts +101 -1
  28. package/dist/cjs/types/enyo-appliance.cjs +15 -0
  29. package/dist/cjs/types/enyo-appliance.d.cts +83 -1
  30. package/dist/cjs/types/enyo-calibration.cjs +218 -0
  31. package/dist/cjs/types/enyo-calibration.d.cts +375 -0
  32. package/dist/cjs/types/enyo-data-bus-value.cjs +89 -0
  33. package/dist/cjs/types/enyo-data-bus-value.d.cts +187 -2
  34. package/dist/cjs/types/enyo-energy-distribution.cjs +63 -0
  35. package/dist/cjs/types/enyo-energy-distribution.d.cts +156 -0
  36. package/dist/cjs/types/enyo-energy-manager.cjs +13 -0
  37. package/dist/cjs/types/enyo-energy-manager.d.cts +13 -1
  38. package/dist/cjs/types/enyo-flexibility-announcement.d.cts +51 -0
  39. package/dist/cjs/types/enyo-forecasting.d.cts +26 -1
  40. package/dist/cjs/types/enyo-grid-connection-point.d.cts +20 -0
  41. package/dist/cjs/types/enyo-inverter-appliance.cjs +48 -0
  42. package/dist/cjs/types/enyo-inverter-appliance.d.cts +123 -0
  43. package/dist/cjs/types/enyo-temperature-sensor-appliance.d.cts +14 -0
  44. package/dist/cjs/types/enyo-timeseries.d.cts +17 -1
  45. package/dist/cjs/types/enyo-vehicle.cjs +80 -1
  46. package/dist/cjs/types/enyo-vehicle.d.cts +223 -0
  47. package/dist/cjs/version.cjs +1 -1
  48. package/dist/cjs/version.d.cts +1 -1
  49. package/dist/energy-app-model-feature.enum.d.ts +12 -0
  50. package/dist/energy-app-model-feature.enum.js +13 -0
  51. package/dist/energy-app-package-definition.d.ts +17 -1
  52. package/dist/energy-app-package-definition.js +6 -0
  53. package/dist/energy-app-permission.type.d.ts +3 -1
  54. package/dist/energy-app-permission.type.js +2 -0
  55. package/dist/energy-app.d.ts +18 -2
  56. package/dist/energy-app.js +17 -1
  57. package/dist/enyo-energy-app-environment.d.ts +16 -0
  58. package/dist/enyo-energy-app-environment.js +17 -0
  59. package/dist/enyo-energy-app-sdk.d.ts +11 -2
  60. package/dist/implementations/calibration/calibration-validators.d.ts +96 -0
  61. package/dist/implementations/calibration/calibration-validators.js +286 -0
  62. package/dist/implementations/energy-distribution/energy-distribution-progress.d.ts +93 -0
  63. package/dist/implementations/energy-distribution/energy-distribution-progress.js +115 -0
  64. package/dist/implementations/energy-distribution/energy-distribution-snapshot-builder.d.ts +168 -0
  65. package/dist/implementations/energy-distribution/energy-distribution-snapshot-builder.js +156 -0
  66. package/dist/implementations/energy-distribution/energy-distribution-validators.d.ts +46 -0
  67. package/dist/implementations/energy-distribution/energy-distribution-validators.js +256 -0
  68. package/dist/index.d.ts +9 -0
  69. package/dist/index.js +9 -0
  70. package/dist/packages/energy-app-authentication.d.ts +10 -0
  71. package/dist/packages/energy-app-calibration.d.ts +226 -0
  72. package/dist/packages/energy-app-calibration.js +1 -0
  73. package/dist/packages/energy-app-energy-manager.d.ts +121 -0
  74. package/dist/packages/energy-app-vehicle.d.ts +101 -1
  75. package/dist/types/enyo-appliance.d.ts +83 -1
  76. package/dist/types/enyo-appliance.js +15 -0
  77. package/dist/types/enyo-calibration.d.ts +375 -0
  78. package/dist/types/enyo-calibration.js +215 -0
  79. package/dist/types/enyo-data-bus-value.d.ts +187 -2
  80. package/dist/types/enyo-data-bus-value.js +89 -0
  81. package/dist/types/enyo-energy-distribution.d.ts +156 -0
  82. package/dist/types/enyo-energy-distribution.js +60 -0
  83. package/dist/types/enyo-energy-manager.d.ts +13 -1
  84. package/dist/types/enyo-energy-manager.js +13 -0
  85. package/dist/types/enyo-flexibility-announcement.d.ts +51 -0
  86. package/dist/types/enyo-forecasting.d.ts +26 -1
  87. package/dist/types/enyo-grid-connection-point.d.ts +20 -0
  88. package/dist/types/enyo-inverter-appliance.d.ts +123 -0
  89. package/dist/types/enyo-inverter-appliance.js +47 -1
  90. package/dist/types/enyo-temperature-sensor-appliance.d.ts +14 -0
  91. package/dist/types/enyo-timeseries.d.ts +17 -1
  92. package/dist/types/enyo-vehicle.d.ts +223 -0
  93. package/dist/types/enyo-vehicle.js +79 -0
  94. package/dist/version.d.ts +1 -1
  95. package/dist/version.js +1 -1
  96. package/package.json +1 -1
package/README.md CHANGED
@@ -64,6 +64,12 @@ The official TypeScript SDK for building Energy Apps on the enyo platform. Creat
64
64
  - [BatteryCommandForecast](#batterycommandforecast)
65
65
  - [HeatpumpForecast](#heatpumpforecast)
66
66
  - [Validators](#validators)
67
+ - [Energy Distribution Snapshot](#energy-distribution-snapshot)
68
+ - [What the snapshot states](#what-the-snapshot-states)
69
+ - [Stating the goal on an announcement](#stating-the-goal-on-an-announcement)
70
+ - [Publishing a snapshot](#publishing-a-snapshot)
71
+ - [Consuming a snapshot](#consuming-a-snapshot)
72
+ - [Helpers and validation](#helpers-and-validation)
67
73
  - [Dynamic Grid Fees & Tariff Bonuses](#dynamic-grid-fees--tariff-bonuses)
68
74
  - [The rule that shapes this API](#the-rule-that-shapes-this-api)
69
75
  - [Publishing a dynamic grid fee](#publishing-a-dynamic-grid-fee)
@@ -206,8 +212,14 @@ Energy Apps follow a specific lifecycle managed by the enyo system:
206
212
  const energyApp = new EnergyApp();
207
213
 
208
214
  // Register startup callback
209
- energyApp.register((packageName, version) => {
210
- console.log(`${packageName} v${version} started`);
215
+ energyApp.register((packageName, version, channel, deviceId, environment) => {
216
+ console.log(`${packageName} v${version} started on ${deviceId} (${channel}, ${environment})`);
217
+
218
+ // e.g. skip hardware access when running in the developer portal simulation
219
+ if (environment === EnyoEnergyAppEnvironment.DeveloperPortalSimulation) {
220
+ console.log('Running in simulation - using mocked devices');
221
+ }
222
+
211
223
  energyApp.updateEnergyAppState(EnergyAppStateEnum.Running);
212
224
  });
213
225
 
@@ -830,6 +842,126 @@ That message is the only write path for a state of charge; it needs the
830
842
  `SendDataBusValues` permission. `getSoc()` resolves to `undefined` when nothing
831
843
  is known — an ordinary answer for a car with no SoC source, not an error.
832
844
 
845
+ ##### Car integrations: pairing a vehicle
846
+
847
+ A package in the `vehicle` category talks to a manufacturer's cloud and links
848
+ one of its cars to a vehicle the user already created. It does not invent
849
+ vehicles and it does not go looking for them — the flow starts with the user:
850
+
851
+ 1. The user signs into the vendor account (`useAuthentication()`, OAuth or
852
+ credentials).
853
+ 2. The user picks one of their vehicles in the enyo app and chooses this
854
+ package to link it to. Which packages are offered comes from the brand
855
+ declared in `compatibility` — see below.
856
+ 3. The host calls the handler registered with `onPairVehicle()`.
857
+ 4. The handler finds the matching car in the account and answers.
858
+
859
+ ```typescript
860
+ const vehicles = energyApp.useVehicle();
861
+
862
+ vehicles.onPairVehicle(async (vehicle, selection) => {
863
+ const cars = await vendorApi.listVehicles();
864
+
865
+ // Several cars in the account and nothing to tell them apart —
866
+ // hand the choice back to the user instead of guessing
867
+ if (cars.length > 1 && !selection) {
868
+ return {
869
+ paired: false,
870
+ failureReason: EnyoVehiclePairFailureReasonEnum.SelectionRequired,
871
+ candidates: cars.map(c => ({
872
+ externalId: c.id,
873
+ displayName: c.name,
874
+ vin: c.vin,
875
+ })),
876
+ };
877
+ }
878
+
879
+ const car = selection
880
+ ? cars.find(c => c.id === selection.externalId)
881
+ : cars[0];
882
+
883
+ if (!car) {
884
+ return {
885
+ paired: false,
886
+ failureReason: EnyoVehiclePairFailureReasonEnum.NoMatchingVehicle,
887
+ };
888
+ }
889
+
890
+ startPolling(car.id, vehicle.id);
891
+ return {
892
+ paired: true,
893
+ externalId: car.id,
894
+ vin: car.vin,
895
+ displayName: car.name,
896
+ // only what you actually read — an invented figure is planned against
897
+ batterySizeKwh: car.batteryKwh,
898
+ maxChargingPowerKw: car.maxChargeKw,
899
+ };
900
+ });
901
+ ```
902
+
903
+ The handler is called a second time with `selection` after the user picks from
904
+ `candidates`. A package serving single-car accounts can ignore the parameter.
905
+
906
+ Once paired, the package has the `vehicleId` it was always missing and publishes
907
+ readings the normal way — `VehicleSocUpdateV1`, keyed by that id. Nothing about
908
+ the write path changes.
909
+
910
+ **Unlinking works from both sides.** The user can unlink one car or sign out of
911
+ the vendor account; the package can drop a link it can no longer serve:
912
+
913
+ ```typescript
914
+ // The user unlinked a car, deleted the vehicle, or signed out.
915
+ // The link is already gone — this is a notification, not a veto.
916
+ vehicles.onUnpairVehicle(async (link, reason) => {
917
+ stopPolling(link.externalId);
918
+ if (reason !== EnyoVehicleUnpairReasonEnum.SignOut) {
919
+ await vendorApi.revokeVehicleAccess(link.externalId);
920
+ }
921
+ });
922
+
923
+ // The car vanished from the vendor account, or the subscription lapsed.
924
+ // Does NOT call onUnpairVehicle — the package already knows.
925
+ await vehicles.unpairVehicle(vehicleId);
926
+
927
+ // Handlers only fire on change, so pick the links back up after a restart
928
+ for (const link of await vehicles.listLinkedVehicles()) {
929
+ startPolling(link.externalId, link.vehicleId);
930
+ }
931
+ ```
932
+
933
+ `listLinkedVehicles()` is not a convenience: without it a package that restarted
934
+ has no idea which cars it is responsible for, and sits idle on a vehicle the
935
+ user believes is connected.
936
+
937
+ **`signOut()` cascades.** Signing out drops every link the package holds, with
938
+ one `onUnpairVehicle` call per link carrying
939
+ `EnyoVehicleUnpairReasonEnum.SignOut`. The vendor session is already gone at
940
+ that point, so stop polling rather than call the vendor's API.
941
+
942
+ Pairing needs the `VehicleIntegration` permission; reading vehicles needs only
943
+ `Vehicle`.
944
+
945
+ ##### Declaring supported brands
946
+
947
+ Car integrations declare brands, not model lines — the vendor's cloud serves
948
+ every car in the account and the line-up changes yearly. An empty `models`
949
+ array with `default: true` reads as "every car of this brand":
950
+
951
+ ```typescript
952
+ categories: [EnergyAppPackageCategory.Vehicle],
953
+ compatibility: [{
954
+ vendorName: 'Tesla',
955
+ default: true,
956
+ models: [],
957
+ }],
958
+ ```
959
+
960
+ Declare per-model entries only where support genuinely differs, and set
961
+ `features` honestly — a brand whose API only reports SoC
962
+ (`VehicleSocReadout`) should not look like one that can also start a charge
963
+ (`VehicleChargeStartStop`). See `EnergyAppModelFeatureEnum`'s vehicle group.
964
+
833
965
  ##### Charge modes
834
966
 
835
967
  Three modes, one meaning each, identical with and without a dynamic tariff — a
@@ -1206,6 +1338,120 @@ await learningPhase.completeLearningPhase(heatpumpPhaseId);
1206
1338
  await learningPhase.removeLearningPhase(phaseId);
1207
1339
  ```
1208
1340
 
1341
+ #### `useCalibration(): EnergyAppCalibration`
1342
+
1343
+ Report what an appliance was **proven** to do. An appliance's `availableFeatures`
1344
+ is a claim the app writes from a model database or a register map; a calibration
1345
+ run is the proof. A wallbox that advertises phase switching and fails to switch,
1346
+ or a heat pump wired for SG Ready with nothing on the terminals, is exactly what
1347
+ the claim cannot catch. Both are kept: the claim tells onboarding what to expect,
1348
+ the run tells an energy manager what it may rely on.
1349
+
1350
+ Supported for batteries, wallboxes, inverters, heat pumps and heating rods.
1351
+
1352
+ **A run is a lifecycle, not a call** — unlike `useDeviceTest()`, where the
1353
+ handler's promise is the whole protocol. A battery calibration is a
1354
+ charge/discharge cycle measured in hours, so the app opens a run, reports
1355
+ progress against it, and closes it with a verdict:
1356
+
1357
+ ```typescript
1358
+ const calibration = energyApp.useCalibration();
1359
+
1360
+ const runId = await calibration.startRun({ applianceId: 'battery-1' });
1361
+
1362
+ await calibration.reportProgress(runId, {
1363
+ progressPercent: 40,
1364
+ currentStep: [
1365
+ { language: 'de', value: 'Batterie wird entladen' },
1366
+ { language: 'en', value: 'Discharging the battery' },
1367
+ ],
1368
+ });
1369
+
1370
+ await calibration.completeRun(runId, {
1371
+ confirmedFeatures: [
1372
+ EnyoCalibratedFeatureEnum.BatteryGridCharging,
1373
+ EnyoCalibratedFeatureEnum.BatteryUsableCapacity,
1374
+ ],
1375
+ });
1376
+ ```
1377
+
1378
+ Always close a run you opened. An app that crashes mid-run leaves it `running`
1379
+ forever, which reads to a user as a device that has been calibrating for three
1380
+ days — report `Interrupted` on restart when you find one of your own runs still
1381
+ open.
1382
+
1383
+ Read it back, or react to anyone's run:
1384
+
1385
+ ```typescript
1386
+ const status = await calibration.getStatus('battery-1'); // undefined when never reported
1387
+ if (status?.status === EnyoCalibrationStatusEnum.Succeeded) {
1388
+ console.log(status.confirmedFeatures);
1389
+ }
1390
+
1391
+ const id = calibration.listenForCalibrationStatusChange(async (run) => { /* … */ });
1392
+ ```
1393
+
1394
+ **Statuses**
1395
+
1396
+ | Status | Meaning |
1397
+ |---|---|
1398
+ | `not-supported` | This appliance has no run to offer. Stop asking; don't show a control. |
1399
+ | `not-calibrated` | Possible, but something is missing — see `requirements`. |
1400
+ | `ready` | Every prerequisite holds; a run could start now. |
1401
+ | `running` | In progress. See `progressPercent` / `currentStep`. |
1402
+ | `succeeded` | Finished; `confirmedFeatures` holds what it proved. |
1403
+ | `failed` | Did not finish; `failureReason` says whether retrying helps. |
1404
+ | `stale` | A past success that no longer describes the device (firmware, hardware, rewiring). |
1405
+
1406
+ Four things to get right:
1407
+
1408
+ - **`undefined` is not `not-supported`.** No run at all means nobody has said anything; `not-supported` means someone said no. Only the second justifies hiding the feature.
1409
+ - **Succeeding and confirming nothing is a real result.** `confirmedFeatures: []` says the run completed and proved nothing — quite different from the field being absent, which means it never got far enough to say.
1410
+ - **Declare prerequisites even when you never run.** `reportRequirements()` is what turns "not calibrated" into "needs at least 30 % state of charge", which a user can act on.
1411
+ - **Omit `progressPercent` rather than guessing.** A bar that sits at 10 % for three hours is worse than a spinner and an honest `currentStep`.
1412
+
1413
+ An app that can be asked to calibrate — from the cockpit, or an onboarding step —
1414
+ registers a handler. Answer promptly with an *acceptance*, not a result; refusing
1415
+ with unsatisfied `requirements` is a normal answer and more useful than starting a
1416
+ run that is bound to fail:
1417
+
1418
+ ```typescript
1419
+ calibration.listenForCalibrationRequest(async (request) => {
1420
+ if (soc < 30) {
1421
+ return {
1422
+ requestId: request.requestId,
1423
+ accepted: false,
1424
+ requirements: [{
1425
+ key: 'min-soc',
1426
+ satisfied: false,
1427
+ description: [
1428
+ { language: 'de', value: 'Mindestens 30 % Ladestand nötig' },
1429
+ { language: 'en', value: 'Needs at least 30 % state of charge' },
1430
+ ],
1431
+ }],
1432
+ };
1433
+ }
1434
+ return { requestId: request.requestId, accepted: true, runId: await calibration.startRun({
1435
+ applianceId: request.applianceId,
1436
+ requestId: request.requestId,
1437
+ }) };
1438
+ });
1439
+ ```
1440
+
1441
+ `EnyoCalibratedFeatureEnum` is deliberately its own flat vocabulary rather than
1442
+ the per-type `availableFeatures` enums — those describe claims and change for
1443
+ reasons unrelated to what a run can prove. Members are prefixed by device class,
1444
+ and `validateCalibrationRun()` rejects a run reporting a feature from another
1445
+ class, along with the self-contradictory states the optional fields otherwise
1446
+ allow (features on a failed run, a failure reason on a successful one, a run
1447
+ that ended before it began).
1448
+
1449
+ Not to be confused with a **learning phase**: that is open-ended data gathering
1450
+ with no pass/fail and no feature list. The two compose — a calibration run may
1451
+ open a learning phase while it settles.
1452
+
1453
+ Writing requires the `Calibration` permission; reading status requires none.
1454
+
1209
1455
  ### Networking & Protocols
1210
1456
 
1211
1457
  #### `useEebus(): EnergyAppEebus`
@@ -2107,6 +2353,30 @@ await applianceManager.updateApplianceState(
2107
2353
 
2108
2354
  Identifier strategies are exported from the package — typical choices match on serial number, hostname, or a composite of `manufacturer + model + sn`.
2109
2355
 
2356
+ ### Battery-powered appliances
2357
+
2358
+ An appliance that runs on its own cell — a wireless room sensor, a radio button, a battery-backed gateway — declares `EnyoApplianceAvailableFeaturesEnum.BatteryPowered` and reports `EnyoAppliance.batteryState`:
2359
+
2360
+ ```typescript
2361
+ await appliances.save({
2362
+ // …
2363
+ availableFeatures: [EnyoApplianceAvailableFeaturesEnum.BatteryPowered],
2364
+ batteryState: {
2365
+ levelPercent: 62,
2366
+ low: false,
2367
+ replaceable: true,
2368
+ measuredAtIso: new Date().toISOString(),
2369
+ },
2370
+ });
2371
+ ```
2372
+
2373
+ Publish changes on `ApplianceStateUpdateV1` (`data.batteryState`) when the level moves meaningfully or `low` flips — not on every reading, or a sensor reporting hourly fills the bus with a number that changes once a month.
2374
+
2375
+ - **This is not a home storage battery.** `EnyoApplianceBatteryState` is about keeping a sensor alive; `EnyoBatteryState` is the runtime state of a `Storage` appliance, in kWh and priced. The names are close and the concepts are unrelated.
2376
+ - **`low` is the load-bearing field, not `levelPercent`.** A primary cell's voltage barely moves until it is nearly flat, so many devices report only a coarse level or none at all. Set `low` from whatever the device actually says rather than leaving a consumer to pick a threshold it cannot calibrate.
2377
+ - **Declare the feature even before the first reading.** It is how a maintenance view knows to watch for a flat battery, and it keeps a device whose reporting is intermittent from appearing and disappearing from that list.
2378
+ - **Always set `measuredAtIso`.** These devices report rarely, so a level without an age says nothing about whether the device is still alive — which is the question a low battery is usually asked alongside.
2379
+
2110
2380
  ## Network Devices & Access Recovery
2111
2381
 
2112
2382
  Packages that talk to local hardware over TCP (Modbus, SunSpec, EEBUS over SHIP, REST) must deal with two failure modes the `useNetworkDevices()` API exposes only at a low level:
@@ -3269,6 +3539,141 @@ try {
3269
3539
  }
3270
3540
  ```
3271
3541
 
3542
+ ## Energy Distribution Snapshot
3543
+
3544
+ Answers the question an owner actually asks in front of the cockpit: **who is getting the power right now, in what order, how far along are they, and why?**
3545
+
3546
+ An energy manager publishes one snapshot per allocation cycle (and on every slot boundary). Each row is a participant: the appliances it steers, plus the household draw and the feed-in that no plan owns. Every row carries a signed power, a reason ready to render in the user's language, and — where the participant has a goal — how far toward that goal it is.
3547
+
3548
+ **Required permission:** `EnergyManager` (to publish). Reading is open to any app.
3549
+
3550
+ ### What the snapshot states
3551
+
3552
+ The model has one rule, and everything else follows from it: **every number is stated by whoever owns it, and carried through verbatim.**
3553
+
3554
+ | Fact | Owned by | Never |
3555
+ |---|---|---|
3556
+ | `rank` — who was served first | the component that ordered the allocation | re-derived from a category precedence the consumer knows |
3557
+ | `progress` — the goal and where the run stands | the appliance manager that owns the goal | back-computed from granted watts, or from a remaining-energy figure |
3558
+ | `state: Complete` | a stated `SessionComplete` reason | inferred from `percent >= 100` |
3559
+ | `reason` + its translation | whoever took the decision | invented per consumer |
3560
+
3561
+ The two that bite hardest:
3562
+
3563
+ **A remaining energy is not a goal.** An announcement states what is *still needed*, and that shrinks every cycle as the appliance fills. A required energy falling from 20 kWh to 2 kWh looks exactly like a target that was always 2 kWh — so a bar built on it stays flat for a session that is nearly done. That is why the goal is stated separately, on the announcement, by the manager that knows it.
3564
+
3565
+ **A full bar is not a finished run.** A measured delivery overshoots a target that was revised mid-session, and a session can be complete while its last meter reading still lags. `Complete` comes from the stated `SessionComplete` cause, and `validateEnergyDistributionSnapshot()` rejects a snapshot that claims otherwise.
3566
+
3567
+ Rows with **no** goal — household draw, feed-in, a charger with no car plugged in — simply carry no `progress`. That is an answer, not missing data: the card renders them without a bar.
3568
+
3569
+ ### Stating the goal on an announcement
3570
+
3571
+ Managers put the goal on their flexibility announcement, in the unit the owner sees:
3572
+
3573
+ ```typescript
3574
+ // Charger: 8.4 kWh delivered of the 22 kWh this session needs.
3575
+ context: {
3576
+ progress: {
3577
+ unit: EnyoDistributionProgressUnitEnum.Energy,
3578
+ start: 0,
3579
+ current: session.deliveredWh,
3580
+ target: session.requiredWh,
3581
+ },
3582
+ },
3583
+
3584
+ // Battery: 58 % now, heading for the 70 % ceiling the owner set, from 52 % at the start.
3585
+ context: {
3586
+ progress: {
3587
+ unit: EnyoDistributionProgressUnitEnum.StateOfCharge,
3588
+ start: 52,
3589
+ current: 58,
3590
+ target: 70,
3591
+ },
3592
+ },
3593
+
3594
+ // Heat pump: a DHW tank at 31 °C that started at 20 °C and is heading for 48 °C.
3595
+ context: {
3596
+ progress: {
3597
+ unit: EnyoDistributionProgressUnitEnum.Temperature,
3598
+ start: 20,
3599
+ current: 31,
3600
+ target: 48,
3601
+ },
3602
+ },
3603
+ ```
3604
+
3605
+ `start` is the bar's zero, and it matters everywhere except energy: without it, a cold tank at 20 °C heading for 48 °C draws a 42 % bar before anything has happened.
3606
+
3607
+ ### Publishing a snapshot
3608
+
3609
+ ```typescript
3610
+ const em = energyApp.useEnergyManager();
3611
+ em.registerFeatures([EnergyManagerFeatureEnum.EnergyDistributionView]);
3612
+
3613
+ const snapshot = new EnergyDistributionSnapshotBuilder()
3614
+ .addAppliance({
3615
+ rank: 0, // the order the allocation actually used
3616
+ applianceId: 'charger-1',
3617
+ applianceType: EnyoApplianceTypeEnum.Charger,
3618
+ name: 'Wallbox Garage',
3619
+ state: EnyoDistributionParticipantStateEnum.Drawing,
3620
+ powerW: 7400,
3621
+ reason: enrichedPvSurplusReason,
3622
+ progress: makeProgress({
3623
+ unit: EnyoDistributionProgressUnitEnum.Energy,
3624
+ current: 8400,
3625
+ target: 22000,
3626
+ }),
3627
+ })
3628
+ .addAppliance({
3629
+ rank: 1,
3630
+ applianceId: 'charger-2',
3631
+ applianceType: EnyoApplianceTypeEnum.Charger,
3632
+ name: 'Wallbox Hof',
3633
+ state: EnyoDistributionParticipantStateEnum.NotAsking,
3634
+ powerW: 0,
3635
+ reason: enrich({type: EnyoDataBusCommandReasonTypeEnum.NothingConnected}),
3636
+ // no car, so no goal, so no bar
3637
+ })
3638
+ .addHousehold({powerW: 620, name: 'Haushalt'})
3639
+ .addFeedIn({powerW: -1300, name: 'Einspeisung'})
3640
+ .build({slotStartMs: slotStart});
3641
+
3642
+ em.publishEnergyDistribution(snapshot);
3643
+ ```
3644
+
3645
+ Include **every** appliance the energy manager knows about, including the ones that asked for nothing — with `NotAsking` or `Skipped` and a reason. A row that silently disappears reads, to an owner, as an appliance that stopped existing.
3646
+
3647
+ The publisher validates before sending, so an invalid snapshot throws rather than going out half-true.
3648
+
3649
+ ### Consuming a snapshot
3650
+
3651
+ Read once for the cold start, then follow:
3652
+
3653
+ ```typescript
3654
+ const em = energyApp.useEnergyManager();
3655
+
3656
+ const current = await em.getEnergyDistribution(); // null: none configured, or none published yet
3657
+ if (current) render(current);
3658
+
3659
+ const id = em.listenForEnergyDistribution(render);
3660
+ energyApp.onShutdown(() => em.unsubscribeEnergyDistribution(id));
3661
+ ```
3662
+
3663
+ Each event carries the **complete** picture of the slot — render it as it arrives rather than merging it into what you had. To tell "will never come" apart from "not yet", check `EnergyManagerInfo.features` for `EnergyDistributionView`.
3664
+
3665
+ ### Helpers and validation
3666
+
3667
+ | Helper | What it does |
3668
+ |---|---|
3669
+ | `makeProgress({unit, start?, current, target})` | Builds an `EnyoDistributionProgress` with `percent` computed — `(current − start) / (target − start)`, clamped to 0–100. The single definition of how full a bar is, so no two consumers disagree by a rounding rule. |
3670
+ | `progressPercent(input)` | The same arithmetic on its own, for a caller that already holds a stated triple. |
3671
+ | `progressFromAnnouncement(stated)` | Turns a manager's announced goal into a participant's progress, adding nothing but the percentage. |
3672
+ | `EnergyDistributionSnapshotBuilder` | Assembles the snapshot: orders rows by stated rank, stamps the timestamps, and keeps the measured rows structurally free of a bar. |
3673
+ | `validateEnergyDistributionSnapshot(snapshot)` | Throws `EnergyDistributionValidationError` on the first violated invariant — duplicate ranks, a bar on a household row, a `Complete` without a stated `SessionComplete`, a percentage that does not follow from its own numbers, a "skipped" row still drawing power. |
3674
+
3675
+ 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.
3676
+
3272
3677
  ## Dynamic Grid Fees & Tariff Bonuses
3273
3678
 
3274
3679
  An electricity price is rarely one number. It is the energy price, plus the grid operator's network
@@ -51,6 +51,19 @@ var EnergyAppModelFeatureEnum;
51
51
  EnergyAppModelFeatureEnum["GridPowerReadout"] = "grid-power-readout";
52
52
  /** The accumulated energy consumption can be read out */
53
53
  EnergyAppModelFeatureEnum["EnergyConsumptionReadout"] = "energy-consumption-readout";
54
+ // Vehicle
55
+ /** The car's state of charge can be read remotely (manufacturer cloud, ISO 15118) */
56
+ EnergyAppModelFeatureEnum["VehicleSocReadout"] = "vehicle-soc-readout";
57
+ /** Charging can be started and stopped through the car rather than the wallbox */
58
+ EnergyAppModelFeatureEnum["VehicleChargeStartStop"] = "vehicle-charge-start-stop";
59
+ /** The car's target state of charge or charging current can be set remotely */
60
+ EnergyAppModelFeatureEnum["VehicleChargeLimit"] = "vehicle-charge-limit";
61
+ /** The car's position can be read, so "is it at home?" can be answered */
62
+ EnergyAppModelFeatureEnum["VehicleLocationReadout"] = "vehicle-location-readout";
63
+ /** The car's odometer reading can be read */
64
+ EnergyAppModelFeatureEnum["VehicleOdometerReadout"] = "vehicle-odometer-readout";
65
+ /** Cabin or battery preconditioning can be triggered remotely */
66
+ EnergyAppModelFeatureEnum["VehiclePreconditioning"] = "vehicle-preconditioning";
54
67
  // Smart Plug
55
68
  /** The plug can be switched on and off */
56
69
  EnergyAppModelFeatureEnum["SwitchOnOff"] = "switch-on-off";
@@ -41,6 +41,18 @@ export declare enum EnergyAppModelFeatureEnum {
41
41
  GridPowerReadout = "grid-power-readout",
42
42
  /** The accumulated energy consumption can be read out */
43
43
  EnergyConsumptionReadout = "energy-consumption-readout",
44
+ /** The car's state of charge can be read remotely (manufacturer cloud, ISO 15118) */
45
+ VehicleSocReadout = "vehicle-soc-readout",
46
+ /** Charging can be started and stopped through the car rather than the wallbox */
47
+ VehicleChargeStartStop = "vehicle-charge-start-stop",
48
+ /** The car's target state of charge or charging current can be set remotely */
49
+ VehicleChargeLimit = "vehicle-charge-limit",
50
+ /** The car's position can be read, so "is it at home?" can be answered */
51
+ VehicleLocationReadout = "vehicle-location-readout",
52
+ /** The car's odometer reading can be read */
53
+ VehicleOdometerReadout = "vehicle-odometer-readout",
54
+ /** Cabin or battery preconditioning can be triggered remotely */
55
+ VehiclePreconditioning = "vehicle-preconditioning",
44
56
  /** The plug can be switched on and off */
45
57
  SwitchOnOff = "switch-on-off",
46
58
  /** The plug can measure the connected load's power */
@@ -19,6 +19,12 @@ var EnergyAppPackageCategory;
19
19
  EnergyAppPackageCategory["SmartPlug"] = "smart-plug";
20
20
  EnergyAppPackageCategory["HeatingRod"] = "heating-rod";
21
21
  EnergyAppPackageCategory["GridOperator"] = "grid-operator";
22
+ /**
23
+ * Integrations that talk to a car — typically a manufacturer cloud API —
24
+ * and link it to one of the user's vehicles. See `EnergyAppVehicle`'s
25
+ * `onPairVehicle` for how the link is made.
26
+ */
27
+ EnergyAppPackageCategory["Vehicle"] = "vehicle";
22
28
  EnergyAppPackageCategory["Other"] = "other";
23
29
  })(EnergyAppPackageCategory || (exports.EnergyAppPackageCategory = EnergyAppPackageCategory = {}));
24
30
  /**
@@ -17,6 +17,12 @@ export declare enum EnergyAppPackageCategory {
17
17
  SmartPlug = "smart-plug",
18
18
  HeatingRod = "heating-rod",
19
19
  GridOperator = "grid-operator",
20
+ /**
21
+ * Integrations that talk to a car — typically a manufacturer cloud API —
22
+ * and link it to one of the user's vehicles. See `EnergyAppVehicle`'s
23
+ * `onPairVehicle` for how the link is made.
24
+ */
25
+ Vehicle = "vehicle",
20
26
  Other = "other"
21
27
  }
22
28
  /**
@@ -259,7 +265,17 @@ export interface EnergyAppPackageCompatibilityModel {
259
265
  export interface EnergyAppPackageCompatibilityVendor {
260
266
  /** Human-readable vendor name (e.g. "SolarEdge", "Fronius") */
261
267
  vendorName: string;
262
- /** Models from this vendor that the package supports */
268
+ /**
269
+ * Models from this vendor that the package supports.
270
+ *
271
+ * May be empty for a package whose support is brand-wide rather than
272
+ * model-by-model — the usual case for a
273
+ * {@link EnergyAppPackageCategory.Vehicle} integration, where the vendor's
274
+ * cloud API serves every car in the account and the model lines change
275
+ * yearly. An empty array together with `default: true` reads as "every
276
+ * device of this brand"; listing models you have not actually tested is
277
+ * worse than listing none.
278
+ */
263
279
  models: EnergyAppPackageCompatibilityModel[];
264
280
  /**
265
281
  * Marks this package as the default Energy App for the vendor when no
@@ -63,4 +63,6 @@ var EnergyAppPermissionTypeEnum;
63
63
  EnergyAppPermissionTypeEnum["GridFeeRegister"] = "GridFeeRegister";
64
64
  EnergyAppPermissionTypeEnum["GridFeeUse"] = "GridFeeUse";
65
65
  EnergyAppPermissionTypeEnum["CommandLog"] = "CommandLog";
66
+ EnergyAppPermissionTypeEnum["Calibration"] = "Calibration";
67
+ EnergyAppPermissionTypeEnum["VehicleIntegration"] = "VehicleIntegration";
66
68
  })(EnergyAppPermissionTypeEnum || (exports.EnergyAppPermissionTypeEnum = EnergyAppPermissionTypeEnum = {}));
@@ -58,7 +58,9 @@ export declare enum EnergyAppPermissionTypeEnum {
58
58
  FirmwareRegistry = "FirmwareRegistry",
59
59
  GridFeeRegister = "GridFeeRegister",
60
60
  GridFeeUse = "GridFeeUse",
61
- CommandLog = "CommandLog"
61
+ CommandLog = "CommandLog",
62
+ Calibration = "Calibration",
63
+ VehicleIntegration = "VehicleIntegration"
62
64
  }
63
65
  /**
64
66
  * String union of every permission name, derived from
@@ -14,7 +14,7 @@ const version_js_1 = require("./version.cjs");
14
14
  * @example
15
15
  * ```ts
16
16
  * const app = new EnergyApp();
17
- * app.register((packageName, version, channel, deviceId) => {
17
+ * app.register((packageName, version, channel, deviceId, environment) => {
18
18
  * // perform initialization
19
19
  * });
20
20
  * ```
@@ -51,6 +51,12 @@ class EnergyApp {
51
51
  updateEnergyAppState(state) {
52
52
  this.energyAppSdk.updateEnergyAppState(state);
53
53
  }
54
+ /**
55
+ * Registers the package with the enyo system.
56
+ * @param callback - Invoked once the package is initialized with the package name,
57
+ * the installed package version, the {@link EnyoPackageChannel} it was installed from,
58
+ * the id of the device it runs on and the {@link EnyoEnergyAppEnvironment} it is executed in
59
+ */
54
60
  register(callback) {
55
61
  // This registers the package with the enyo system
56
62
  this.energyAppSdk.register(callback);
@@ -293,6 +299,16 @@ class EnergyApp {
293
299
  useLearningPhase() {
294
300
  return this.energyAppSdk.useLearningPhase();
295
301
  }
302
+ /**
303
+ * Gets the Calibration API for reporting what an appliance was proven to do.
304
+ * Provides methods to open, advance and close calibration runs for
305
+ * batteries, wallboxes, inverters, heat pumps and heating rods, to declare
306
+ * their prerequisites, and to answer host requests to calibrate.
307
+ * @returns The Calibration API instance
308
+ */
309
+ useCalibration() {
310
+ return this.energyAppSdk.useCalibration();
311
+ }
296
312
  /**
297
313
  * Gets the WiFi API for scanning and listing known WiFi networks (SSIDs).
298
314
  * Provides methods to discover saved/known SSIDs that are currently
@@ -19,6 +19,7 @@ import { EnergyAppOnboarding } from "./packages/energy-app-onboarding.cjs";
19
19
  import { EnergyAppOnboardingV2 } from "./packages/energy-app-onboarding-v2.cjs";
20
20
  import { EnergyAppTimeseries } from "./packages/energy-app-timeseries.cjs";
21
21
  import { EnyoPackageChannel } from "./enyo-package-channel.cjs";
22
+ import { EnyoEnergyAppEnvironment } from "./enyo-energy-app-environment.cjs";
22
23
  import { EnergyAppEnergyManager } from "./packages/energy-app-energy-manager.cjs";
23
24
  import { EnergyAppElectricityTariff } from "./packages/energy-app-electricity-tariff.cjs";
24
25
  import { EnergyAppWeatherForecasting } from "./packages/energy-app-weather-forecasting.cjs";
@@ -33,6 +34,7 @@ import { EnergyAppMqtt } from "./packages/energy-app-mqtt.cjs";
33
34
  import { EnergyAppBluetooth } from "./packages/energy-app-bluetooth.cjs";
34
35
  import { EnergyAppDiagnostics } from "./packages/energy-app-diagnostics.cjs";
35
36
  import { EnergyAppLearningPhase } from "./packages/energy-app-learning-phase.cjs";
37
+ import { EnergyAppCalibration } from "./packages/energy-app-calibration.cjs";
36
38
  import { EnergyAppWifi } from "./packages/energy-app-wifi.cjs";
37
39
  import { EnergyAppUdp } from "./packages/energy-app-udp.cjs";
38
40
  import { EnergyAppGridConnectionPoint } from "./packages/energy-app-grid-connection-point.cjs";
@@ -60,7 +62,7 @@ import { UseFetchOptions } from "./types/enyo-fetch.cjs";
60
62
  * @example
61
63
  * ```ts
62
64
  * const app = new EnergyApp();
63
- * app.register((packageName, version, channel, deviceId) => {
65
+ * app.register((packageName, version, channel, deviceId, environment) => {
64
66
  * // perform initialization
65
67
  * });
66
68
  * ```
@@ -78,7 +80,13 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
78
80
  */
79
81
  onNetworkStatusChanged(listener: (online: boolean) => void | Promise<void>): string;
80
82
  updateEnergyAppState(state: EnergyAppStateEnum): void;
81
- register(callback: (packageName: string, version: number, channel: EnyoPackageChannel, deviceId: string) => void | Promise<void>): void;
83
+ /**
84
+ * Registers the package with the enyo system.
85
+ * @param callback - Invoked once the package is initialized with the package name,
86
+ * the installed package version, the {@link EnyoPackageChannel} it was installed from,
87
+ * the id of the device it runs on and the {@link EnyoEnergyAppEnvironment} it is executed in
88
+ */
89
+ register(callback: (packageName: string, version: number, channel: EnyoPackageChannel, deviceId: string, environment: EnyoEnergyAppEnvironment) => void | Promise<void>): void;
82
90
  onShutdown(callback: () => void | Promise<void>): void;
83
91
  /**
84
92
  * Returns a `fetch` implementation provided by the runtime.
@@ -242,6 +250,14 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
242
250
  * @returns The Learning Phase API instance
243
251
  */
244
252
  useLearningPhase(): EnergyAppLearningPhase;
253
+ /**
254
+ * Gets the Calibration API for reporting what an appliance was proven to do.
255
+ * Provides methods to open, advance and close calibration runs for
256
+ * batteries, wallboxes, inverters, heat pumps and heating rods, to declare
257
+ * their prerequisites, and to answer host requests to calibrate.
258
+ * @returns The Calibration API instance
259
+ */
260
+ useCalibration(): EnergyAppCalibration;
245
261
  /**
246
262
  * Gets the WiFi API for scanning and listing known WiFi networks (SSIDs).
247
263
  * Provides methods to discover saved/known SSIDs that are currently
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EnyoEnergyAppEnvironment = void 0;
4
+ /**
5
+ * Runtime environment an energy app is executed in.
6
+ *
7
+ * The environment is handed to the callback passed to `register()` so an app can
8
+ * adapt its behaviour, e.g. use verbose logging or mocked hardware access while
9
+ * developing, and skip anything that requires real hardware when it runs inside
10
+ * the developer portal simulation.
11
+ */
12
+ var EnyoEnergyAppEnvironment;
13
+ (function (EnyoEnergyAppEnvironment) {
14
+ /** Running on a real enyo device in development mode (e.g. a developer's test device). */
15
+ EnyoEnergyAppEnvironment["OnDeviceDevelopment"] = "on-device-development";
16
+ /** Running on a real enyo device in production, i.e. at a customer's site. */
17
+ EnyoEnergyAppEnvironment["OnDeviceProduction"] = "on-device-production";
18
+ /** Running inside the developer portal simulation, without real hardware attached. */
19
+ EnyoEnergyAppEnvironment["DeveloperPortalSimulation"] = "developer-portal-simulation";
20
+ })(EnyoEnergyAppEnvironment || (exports.EnyoEnergyAppEnvironment = EnyoEnergyAppEnvironment = {}));