@enyo-energy/energy-app-sdk 1.17.0 → 1.19.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 +417 -0
  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 +10 -0
  9. package/dist/cjs/energy-app.d.cts +9 -0
  10. package/dist/cjs/enyo-energy-app-sdk.d.cts +3 -0
  11. package/dist/cjs/implementations/calibration/calibration-validators.cjs +292 -0
  12. package/dist/cjs/implementations/calibration/calibration-validators.d.cts +96 -0
  13. package/dist/cjs/implementations/energy-distribution/energy-distribution-progress.cjs +120 -0
  14. package/dist/cjs/implementations/energy-distribution/energy-distribution-progress.d.cts +93 -0
  15. package/dist/cjs/implementations/energy-distribution/energy-distribution-snapshot-builder.cjs +160 -0
  16. package/dist/cjs/implementations/energy-distribution/energy-distribution-snapshot-builder.d.cts +168 -0
  17. package/dist/cjs/implementations/energy-distribution/energy-distribution-validators.cjs +261 -0
  18. package/dist/cjs/implementations/energy-distribution/energy-distribution-validators.d.cts +46 -0
  19. package/dist/cjs/index.cjs +8 -0
  20. package/dist/cjs/index.d.cts +8 -0
  21. package/dist/cjs/packages/energy-app-authentication.d.cts +10 -0
  22. package/dist/cjs/packages/energy-app-calibration.cjs +2 -0
  23. package/dist/cjs/packages/energy-app-calibration.d.cts +226 -0
  24. package/dist/cjs/packages/energy-app-energy-manager.d.cts +121 -0
  25. package/dist/cjs/packages/energy-app-vehicle.d.cts +101 -1
  26. package/dist/cjs/types/enyo-appliance.cjs +15 -0
  27. package/dist/cjs/types/enyo-appliance.d.cts +83 -1
  28. package/dist/cjs/types/enyo-calibration.cjs +218 -0
  29. package/dist/cjs/types/enyo-calibration.d.cts +375 -0
  30. package/dist/cjs/types/enyo-charge.cjs +22 -1
  31. package/dist/cjs/types/enyo-charge.d.cts +98 -1
  32. package/dist/cjs/types/enyo-data-bus-value.cjs +89 -0
  33. package/dist/cjs/types/enyo-data-bus-value.d.cts +207 -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 +9 -0
  56. package/dist/energy-app.js +10 -0
  57. package/dist/enyo-energy-app-sdk.d.ts +3 -0
  58. package/dist/implementations/calibration/calibration-validators.d.ts +96 -0
  59. package/dist/implementations/calibration/calibration-validators.js +286 -0
  60. package/dist/implementations/energy-distribution/energy-distribution-progress.d.ts +93 -0
  61. package/dist/implementations/energy-distribution/energy-distribution-progress.js +115 -0
  62. package/dist/implementations/energy-distribution/energy-distribution-snapshot-builder.d.ts +168 -0
  63. package/dist/implementations/energy-distribution/energy-distribution-snapshot-builder.js +156 -0
  64. package/dist/implementations/energy-distribution/energy-distribution-validators.d.ts +46 -0
  65. package/dist/implementations/energy-distribution/energy-distribution-validators.js +256 -0
  66. package/dist/index.d.ts +8 -0
  67. package/dist/index.js +8 -0
  68. package/dist/packages/energy-app-authentication.d.ts +10 -0
  69. package/dist/packages/energy-app-calibration.d.ts +226 -0
  70. package/dist/packages/energy-app-calibration.js +1 -0
  71. package/dist/packages/energy-app-energy-manager.d.ts +121 -0
  72. package/dist/packages/energy-app-vehicle.d.ts +101 -1
  73. package/dist/types/enyo-appliance.d.ts +83 -1
  74. package/dist/types/enyo-appliance.js +15 -0
  75. package/dist/types/enyo-calibration.d.ts +375 -0
  76. package/dist/types/enyo-calibration.js +215 -0
  77. package/dist/types/enyo-charge.d.ts +98 -1
  78. package/dist/types/enyo-charge.js +21 -0
  79. package/dist/types/enyo-data-bus-value.d.ts +207 -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)
@@ -830,6 +836,126 @@ That message is the only write path for a state of charge; it needs the
830
836
  `SendDataBusValues` permission. `getSoc()` resolves to `undefined` when nothing
831
837
  is known — an ordinary answer for a car with no SoC source, not an error.
832
838
 
839
+ ##### Car integrations: pairing a vehicle
840
+
841
+ A package in the `vehicle` category talks to a manufacturer's cloud and links
842
+ one of its cars to a vehicle the user already created. It does not invent
843
+ vehicles and it does not go looking for them — the flow starts with the user:
844
+
845
+ 1. The user signs into the vendor account (`useAuthentication()`, OAuth or
846
+ credentials).
847
+ 2. The user picks one of their vehicles in the enyo app and chooses this
848
+ package to link it to. Which packages are offered comes from the brand
849
+ declared in `compatibility` — see below.
850
+ 3. The host calls the handler registered with `onPairVehicle()`.
851
+ 4. The handler finds the matching car in the account and answers.
852
+
853
+ ```typescript
854
+ const vehicles = energyApp.useVehicle();
855
+
856
+ vehicles.onPairVehicle(async (vehicle, selection) => {
857
+ const cars = await vendorApi.listVehicles();
858
+
859
+ // Several cars in the account and nothing to tell them apart —
860
+ // hand the choice back to the user instead of guessing
861
+ if (cars.length > 1 && !selection) {
862
+ return {
863
+ paired: false,
864
+ failureReason: EnyoVehiclePairFailureReasonEnum.SelectionRequired,
865
+ candidates: cars.map(c => ({
866
+ externalId: c.id,
867
+ displayName: c.name,
868
+ vin: c.vin,
869
+ })),
870
+ };
871
+ }
872
+
873
+ const car = selection
874
+ ? cars.find(c => c.id === selection.externalId)
875
+ : cars[0];
876
+
877
+ if (!car) {
878
+ return {
879
+ paired: false,
880
+ failureReason: EnyoVehiclePairFailureReasonEnum.NoMatchingVehicle,
881
+ };
882
+ }
883
+
884
+ startPolling(car.id, vehicle.id);
885
+ return {
886
+ paired: true,
887
+ externalId: car.id,
888
+ vin: car.vin,
889
+ displayName: car.name,
890
+ // only what you actually read — an invented figure is planned against
891
+ batterySizeKwh: car.batteryKwh,
892
+ maxChargingPowerKw: car.maxChargeKw,
893
+ };
894
+ });
895
+ ```
896
+
897
+ The handler is called a second time with `selection` after the user picks from
898
+ `candidates`. A package serving single-car accounts can ignore the parameter.
899
+
900
+ Once paired, the package has the `vehicleId` it was always missing and publishes
901
+ readings the normal way — `VehicleSocUpdateV1`, keyed by that id. Nothing about
902
+ the write path changes.
903
+
904
+ **Unlinking works from both sides.** The user can unlink one car or sign out of
905
+ the vendor account; the package can drop a link it can no longer serve:
906
+
907
+ ```typescript
908
+ // The user unlinked a car, deleted the vehicle, or signed out.
909
+ // The link is already gone — this is a notification, not a veto.
910
+ vehicles.onUnpairVehicle(async (link, reason) => {
911
+ stopPolling(link.externalId);
912
+ if (reason !== EnyoVehicleUnpairReasonEnum.SignOut) {
913
+ await vendorApi.revokeVehicleAccess(link.externalId);
914
+ }
915
+ });
916
+
917
+ // The car vanished from the vendor account, or the subscription lapsed.
918
+ // Does NOT call onUnpairVehicle — the package already knows.
919
+ await vehicles.unpairVehicle(vehicleId);
920
+
921
+ // Handlers only fire on change, so pick the links back up after a restart
922
+ for (const link of await vehicles.listLinkedVehicles()) {
923
+ startPolling(link.externalId, link.vehicleId);
924
+ }
925
+ ```
926
+
927
+ `listLinkedVehicles()` is not a convenience: without it a package that restarted
928
+ has no idea which cars it is responsible for, and sits idle on a vehicle the
929
+ user believes is connected.
930
+
931
+ **`signOut()` cascades.** Signing out drops every link the package holds, with
932
+ one `onUnpairVehicle` call per link carrying
933
+ `EnyoVehicleUnpairReasonEnum.SignOut`. The vendor session is already gone at
934
+ that point, so stop polling rather than call the vendor's API.
935
+
936
+ Pairing needs the `VehicleIntegration` permission; reading vehicles needs only
937
+ `Vehicle`.
938
+
939
+ ##### Declaring supported brands
940
+
941
+ Car integrations declare brands, not model lines — the vendor's cloud serves
942
+ every car in the account and the line-up changes yearly. An empty `models`
943
+ array with `default: true` reads as "every car of this brand":
944
+
945
+ ```typescript
946
+ categories: [EnergyAppPackageCategory.Vehicle],
947
+ compatibility: [{
948
+ vendorName: 'Tesla',
949
+ default: true,
950
+ models: [],
951
+ }],
952
+ ```
953
+
954
+ Declare per-model entries only where support genuinely differs, and set
955
+ `features` honestly — a brand whose API only reports SoC
956
+ (`VehicleSocReadout`) should not look like one that can also start a charge
957
+ (`VehicleChargeStartStop`). See `EnergyAppModelFeatureEnum`'s vehicle group.
958
+
833
959
  ##### Charge modes
834
960
 
835
961
  Three modes, one meaning each, identical with and without a dynamic tariff — a
@@ -928,6 +1054,24 @@ StartTransaction returns the running session instead of opening a second one.
928
1054
  `save()` remains a whole-object update for a session that already exists; it
929
1055
  offers no uniqueness guarantee, so do not open sessions with it.
930
1056
 
1057
+ **A session records the answers it ran under**, so a listener sees what the
1058
+ customer actually chose rather than only what the charger measured:
1059
+
1060
+ | Field | Meaning |
1061
+ |---|---|
1062
+ | `startSocPercent` / `targetSocPercent` | The SoC the session started from and was to reach. Fixed for the session. |
1063
+ | `priceLimitMode` | Which ceiling applied — `ct-per-kwh`, `cheapest-share`, or absent for none. |
1064
+ | `priceLimitCtPerKwh` / `priceLimitSharePercent` | The ceiling itself, read according to the mode. |
1065
+ | `maxChargingPowerW` | The power the user dialled, in **Watts**. `Immediate` only. |
1066
+ | `vehicleAssignment` | `detected` \| `manual` \| `unknown` — how the session found its car. |
1067
+
1068
+ These mirror the fields on `StartChargeV1`: the command says what a session was
1069
+ asked for, the charge says what it ran with. `maxChargingPowerW` is the opening
1070
+ figure only — the energy manager's `SetChargerAvailablePowerV2` envelope still
1071
+ bounds the session and overrides it. Watch the units: this one is watts, while
1072
+ `EnyoChargeScheduleEntry.limitAmpere` is amperes and the superseded
1073
+ `ChangeChargingPowerV1` is kW.
1074
+
931
1075
  React to charging sessions as they happen:
932
1076
 
933
1077
  ```typescript
@@ -1188,6 +1332,120 @@ await learningPhase.completeLearningPhase(heatpumpPhaseId);
1188
1332
  await learningPhase.removeLearningPhase(phaseId);
1189
1333
  ```
1190
1334
 
1335
+ #### `useCalibration(): EnergyAppCalibration`
1336
+
1337
+ Report what an appliance was **proven** to do. An appliance's `availableFeatures`
1338
+ is a claim the app writes from a model database or a register map; a calibration
1339
+ run is the proof. A wallbox that advertises phase switching and fails to switch,
1340
+ or a heat pump wired for SG Ready with nothing on the terminals, is exactly what
1341
+ the claim cannot catch. Both are kept: the claim tells onboarding what to expect,
1342
+ the run tells an energy manager what it may rely on.
1343
+
1344
+ Supported for batteries, wallboxes, inverters, heat pumps and heating rods.
1345
+
1346
+ **A run is a lifecycle, not a call** — unlike `useDeviceTest()`, where the
1347
+ handler's promise is the whole protocol. A battery calibration is a
1348
+ charge/discharge cycle measured in hours, so the app opens a run, reports
1349
+ progress against it, and closes it with a verdict:
1350
+
1351
+ ```typescript
1352
+ const calibration = energyApp.useCalibration();
1353
+
1354
+ const runId = await calibration.startRun({ applianceId: 'battery-1' });
1355
+
1356
+ await calibration.reportProgress(runId, {
1357
+ progressPercent: 40,
1358
+ currentStep: [
1359
+ { language: 'de', value: 'Batterie wird entladen' },
1360
+ { language: 'en', value: 'Discharging the battery' },
1361
+ ],
1362
+ });
1363
+
1364
+ await calibration.completeRun(runId, {
1365
+ confirmedFeatures: [
1366
+ EnyoCalibratedFeatureEnum.BatteryGridCharging,
1367
+ EnyoCalibratedFeatureEnum.BatteryUsableCapacity,
1368
+ ],
1369
+ });
1370
+ ```
1371
+
1372
+ Always close a run you opened. An app that crashes mid-run leaves it `running`
1373
+ forever, which reads to a user as a device that has been calibrating for three
1374
+ days — report `Interrupted` on restart when you find one of your own runs still
1375
+ open.
1376
+
1377
+ Read it back, or react to anyone's run:
1378
+
1379
+ ```typescript
1380
+ const status = await calibration.getStatus('battery-1'); // undefined when never reported
1381
+ if (status?.status === EnyoCalibrationStatusEnum.Succeeded) {
1382
+ console.log(status.confirmedFeatures);
1383
+ }
1384
+
1385
+ const id = calibration.listenForCalibrationStatusChange(async (run) => { /* … */ });
1386
+ ```
1387
+
1388
+ **Statuses**
1389
+
1390
+ | Status | Meaning |
1391
+ |---|---|
1392
+ | `not-supported` | This appliance has no run to offer. Stop asking; don't show a control. |
1393
+ | `not-calibrated` | Possible, but something is missing — see `requirements`. |
1394
+ | `ready` | Every prerequisite holds; a run could start now. |
1395
+ | `running` | In progress. See `progressPercent` / `currentStep`. |
1396
+ | `succeeded` | Finished; `confirmedFeatures` holds what it proved. |
1397
+ | `failed` | Did not finish; `failureReason` says whether retrying helps. |
1398
+ | `stale` | A past success that no longer describes the device (firmware, hardware, rewiring). |
1399
+
1400
+ Four things to get right:
1401
+
1402
+ - **`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.
1403
+ - **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.
1404
+ - **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.
1405
+ - **Omit `progressPercent` rather than guessing.** A bar that sits at 10 % for three hours is worse than a spinner and an honest `currentStep`.
1406
+
1407
+ An app that can be asked to calibrate — from the cockpit, or an onboarding step —
1408
+ registers a handler. Answer promptly with an *acceptance*, not a result; refusing
1409
+ with unsatisfied `requirements` is a normal answer and more useful than starting a
1410
+ run that is bound to fail:
1411
+
1412
+ ```typescript
1413
+ calibration.listenForCalibrationRequest(async (request) => {
1414
+ if (soc < 30) {
1415
+ return {
1416
+ requestId: request.requestId,
1417
+ accepted: false,
1418
+ requirements: [{
1419
+ key: 'min-soc',
1420
+ satisfied: false,
1421
+ description: [
1422
+ { language: 'de', value: 'Mindestens 30 % Ladestand nötig' },
1423
+ { language: 'en', value: 'Needs at least 30 % state of charge' },
1424
+ ],
1425
+ }],
1426
+ };
1427
+ }
1428
+ return { requestId: request.requestId, accepted: true, runId: await calibration.startRun({
1429
+ applianceId: request.applianceId,
1430
+ requestId: request.requestId,
1431
+ }) };
1432
+ });
1433
+ ```
1434
+
1435
+ `EnyoCalibratedFeatureEnum` is deliberately its own flat vocabulary rather than
1436
+ the per-type `availableFeatures` enums — those describe claims and change for
1437
+ reasons unrelated to what a run can prove. Members are prefixed by device class,
1438
+ and `validateCalibrationRun()` rejects a run reporting a feature from another
1439
+ class, along with the self-contradictory states the optional fields otherwise
1440
+ allow (features on a failed run, a failure reason on a successful one, a run
1441
+ that ended before it began).
1442
+
1443
+ Not to be confused with a **learning phase**: that is open-ended data gathering
1444
+ with no pass/fail and no feature list. The two compose — a calibration run may
1445
+ open a learning phase while it settles.
1446
+
1447
+ Writing requires the `Calibration` permission; reading status requires none.
1448
+
1191
1449
  ### Networking & Protocols
1192
1450
 
1193
1451
  #### `useEebus(): EnergyAppEebus`
@@ -2089,6 +2347,30 @@ await applianceManager.updateApplianceState(
2089
2347
 
2090
2348
  Identifier strategies are exported from the package — typical choices match on serial number, hostname, or a composite of `manufacturer + model + sn`.
2091
2349
 
2350
+ ### Battery-powered appliances
2351
+
2352
+ 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`:
2353
+
2354
+ ```typescript
2355
+ await appliances.save({
2356
+ // …
2357
+ availableFeatures: [EnyoApplianceAvailableFeaturesEnum.BatteryPowered],
2358
+ batteryState: {
2359
+ levelPercent: 62,
2360
+ low: false,
2361
+ replaceable: true,
2362
+ measuredAtIso: new Date().toISOString(),
2363
+ },
2364
+ });
2365
+ ```
2366
+
2367
+ 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.
2368
+
2369
+ - **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.
2370
+ - **`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.
2371
+ - **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.
2372
+ - **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.
2373
+
2092
2374
  ## Network Devices & Access Recovery
2093
2375
 
2094
2376
  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:
@@ -3251,6 +3533,141 @@ try {
3251
3533
  }
3252
3534
  ```
3253
3535
 
3536
+ ## Energy Distribution Snapshot
3537
+
3538
+ 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?**
3539
+
3540
+ 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.
3541
+
3542
+ **Required permission:** `EnergyManager` (to publish). Reading is open to any app.
3543
+
3544
+ ### What the snapshot states
3545
+
3546
+ The model has one rule, and everything else follows from it: **every number is stated by whoever owns it, and carried through verbatim.**
3547
+
3548
+ | Fact | Owned by | Never |
3549
+ |---|---|---|
3550
+ | `rank` — who was served first | the component that ordered the allocation | re-derived from a category precedence the consumer knows |
3551
+ | `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 |
3552
+ | `state: Complete` | a stated `SessionComplete` reason | inferred from `percent >= 100` |
3553
+ | `reason` + its translation | whoever took the decision | invented per consumer |
3554
+
3555
+ The two that bite hardest:
3556
+
3557
+ **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.
3558
+
3559
+ **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.
3560
+
3561
+ 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.
3562
+
3563
+ ### Stating the goal on an announcement
3564
+
3565
+ Managers put the goal on their flexibility announcement, in the unit the owner sees:
3566
+
3567
+ ```typescript
3568
+ // Charger: 8.4 kWh delivered of the 22 kWh this session needs.
3569
+ context: {
3570
+ progress: {
3571
+ unit: EnyoDistributionProgressUnitEnum.Energy,
3572
+ start: 0,
3573
+ current: session.deliveredWh,
3574
+ target: session.requiredWh,
3575
+ },
3576
+ },
3577
+
3578
+ // Battery: 58 % now, heading for the 70 % ceiling the owner set, from 52 % at the start.
3579
+ context: {
3580
+ progress: {
3581
+ unit: EnyoDistributionProgressUnitEnum.StateOfCharge,
3582
+ start: 52,
3583
+ current: 58,
3584
+ target: 70,
3585
+ },
3586
+ },
3587
+
3588
+ // Heat pump: a DHW tank at 31 °C that started at 20 °C and is heading for 48 °C.
3589
+ context: {
3590
+ progress: {
3591
+ unit: EnyoDistributionProgressUnitEnum.Temperature,
3592
+ start: 20,
3593
+ current: 31,
3594
+ target: 48,
3595
+ },
3596
+ },
3597
+ ```
3598
+
3599
+ `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.
3600
+
3601
+ ### Publishing a snapshot
3602
+
3603
+ ```typescript
3604
+ const em = energyApp.useEnergyManager();
3605
+ em.registerFeatures([EnergyManagerFeatureEnum.EnergyDistributionView]);
3606
+
3607
+ const snapshot = new EnergyDistributionSnapshotBuilder()
3608
+ .addAppliance({
3609
+ rank: 0, // the order the allocation actually used
3610
+ applianceId: 'charger-1',
3611
+ applianceType: EnyoApplianceTypeEnum.Charger,
3612
+ name: 'Wallbox Garage',
3613
+ state: EnyoDistributionParticipantStateEnum.Drawing,
3614
+ powerW: 7400,
3615
+ reason: enrichedPvSurplusReason,
3616
+ progress: makeProgress({
3617
+ unit: EnyoDistributionProgressUnitEnum.Energy,
3618
+ current: 8400,
3619
+ target: 22000,
3620
+ }),
3621
+ })
3622
+ .addAppliance({
3623
+ rank: 1,
3624
+ applianceId: 'charger-2',
3625
+ applianceType: EnyoApplianceTypeEnum.Charger,
3626
+ name: 'Wallbox Hof',
3627
+ state: EnyoDistributionParticipantStateEnum.NotAsking,
3628
+ powerW: 0,
3629
+ reason: enrich({type: EnyoDataBusCommandReasonTypeEnum.NothingConnected}),
3630
+ // no car, so no goal, so no bar
3631
+ })
3632
+ .addHousehold({powerW: 620, name: 'Haushalt'})
3633
+ .addFeedIn({powerW: -1300, name: 'Einspeisung'})
3634
+ .build({slotStartMs: slotStart});
3635
+
3636
+ em.publishEnergyDistribution(snapshot);
3637
+ ```
3638
+
3639
+ 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.
3640
+
3641
+ The publisher validates before sending, so an invalid snapshot throws rather than going out half-true.
3642
+
3643
+ ### Consuming a snapshot
3644
+
3645
+ Read once for the cold start, then follow:
3646
+
3647
+ ```typescript
3648
+ const em = energyApp.useEnergyManager();
3649
+
3650
+ const current = await em.getEnergyDistribution(); // null: none configured, or none published yet
3651
+ if (current) render(current);
3652
+
3653
+ const id = em.listenForEnergyDistribution(render);
3654
+ energyApp.onShutdown(() => em.unsubscribeEnergyDistribution(id));
3655
+ ```
3656
+
3657
+ 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`.
3658
+
3659
+ ### Helpers and validation
3660
+
3661
+ | Helper | What it does |
3662
+ |---|---|
3663
+ | `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. |
3664
+ | `progressPercent(input)` | The same arithmetic on its own, for a caller that already holds a stated triple. |
3665
+ | `progressFromAnnouncement(stated)` | Turns a manager's announced goal into a participant's progress, adding nothing but the percentage. |
3666
+ | `EnergyDistributionSnapshotBuilder` | Assembles the snapshot: orders rows by stated rank, stamps the timestamps, and keeps the measured rows structurally free of a bar. |
3667
+ | `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. |
3668
+
3669
+ 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.
3670
+
3254
3671
  ## Dynamic Grid Fees & Tariff Bonuses
3255
3672
 
3256
3673
  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
@@ -293,6 +293,16 @@ class EnergyApp {
293
293
  useLearningPhase() {
294
294
  return this.energyAppSdk.useLearningPhase();
295
295
  }
296
+ /**
297
+ * Gets the Calibration API for reporting what an appliance was proven to do.
298
+ * Provides methods to open, advance and close calibration runs for
299
+ * batteries, wallboxes, inverters, heat pumps and heating rods, to declare
300
+ * their prerequisites, and to answer host requests to calibrate.
301
+ * @returns The Calibration API instance
302
+ */
303
+ useCalibration() {
304
+ return this.energyAppSdk.useCalibration();
305
+ }
296
306
  /**
297
307
  * Gets the WiFi API for scanning and listing known WiFi networks (SSIDs).
298
308
  * Provides methods to discover saved/known SSIDs that are currently
@@ -33,6 +33,7 @@ import { EnergyAppMqtt } from "./packages/energy-app-mqtt.cjs";
33
33
  import { EnergyAppBluetooth } from "./packages/energy-app-bluetooth.cjs";
34
34
  import { EnergyAppDiagnostics } from "./packages/energy-app-diagnostics.cjs";
35
35
  import { EnergyAppLearningPhase } from "./packages/energy-app-learning-phase.cjs";
36
+ import { EnergyAppCalibration } from "./packages/energy-app-calibration.cjs";
36
37
  import { EnergyAppWifi } from "./packages/energy-app-wifi.cjs";
37
38
  import { EnergyAppUdp } from "./packages/energy-app-udp.cjs";
38
39
  import { EnergyAppGridConnectionPoint } from "./packages/energy-app-grid-connection-point.cjs";
@@ -242,6 +243,14 @@ export declare class EnergyApp implements EnyoEnergyAppSdk {
242
243
  * @returns The Learning Phase API instance
243
244
  */
244
245
  useLearningPhase(): EnergyAppLearningPhase;
246
+ /**
247
+ * Gets the Calibration API for reporting what an appliance was proven to do.
248
+ * Provides methods to open, advance and close calibration runs for
249
+ * batteries, wallboxes, inverters, heat pumps and heating rods, to declare
250
+ * their prerequisites, and to answer host requests to calibrate.
251
+ * @returns The Calibration API instance
252
+ */
253
+ useCalibration(): EnergyAppCalibration;
245
254
  /**
246
255
  * Gets the WiFi API for scanning and listing known WiFi networks (SSIDs).
247
256
  * Provides methods to discover saved/known SSIDs that are currently
@@ -32,6 +32,7 @@ import { EnergyAppMqtt } from "./packages/energy-app-mqtt.cjs";
32
32
  import { EnergyAppBluetooth } from "./packages/energy-app-bluetooth.cjs";
33
33
  import { EnergyAppDiagnostics } from "./packages/energy-app-diagnostics.cjs";
34
34
  import { EnergyAppLearningPhase } from "./packages/energy-app-learning-phase.cjs";
35
+ import { EnergyAppCalibration } from "./packages/energy-app-calibration.cjs";
35
36
  import { EnergyAppWifi } from "./packages/energy-app-wifi.cjs";
36
37
  import { EnergyAppUdp } from "./packages/energy-app-udp.cjs";
37
38
  import { EnergyAppGridConnectionPoint } from "./packages/energy-app-grid-connection-point.cjs";
@@ -141,6 +142,8 @@ export interface EnyoEnergyAppSdk {
141
142
  useDiagnostics: () => EnergyAppDiagnostics;
142
143
  /** Get the Learning Phase API for registering and tracking learning phases */
143
144
  useLearningPhase: () => EnergyAppLearningPhase;
145
+ /** Get the Calibration API for reporting what an appliance was proven to do */
146
+ useCalibration: () => EnergyAppCalibration;
144
147
  /** Get the WiFi API for scanning and listing known SSIDs */
145
148
  useWifi: () => EnergyAppWifi;
146
149
  /** Get the UDP communication API for binding sockets and exchanging datagrams */